"""Action management API routes — CRUD, execute, dry-run, executions."""

import logging
import re

from fastapi import APIRouter, Depends, HTTPException, Query, status
from pydantic import BaseModel
from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession

from ..auth.dependencies import get_current_user
from ..database.engine import get_session
from ..database.models import (
    Action,
    ActionExecution,
    ActionRule,
    ServiceProvider,
    User,
)

_LOGGER = logging.getLogger(__name__)

router = APIRouter(prefix="/api/actions", tags=["actions"])


# ---------------------------------------------------------------------------
# Request / response schemas
# ---------------------------------------------------------------------------


class ActionCreate(BaseModel):
    provider_id: int
    name: str
    icon: str = ""
    action_type: str
    config: dict = {}
    schedule_type: str = "interval"
    schedule_interval: int = 3600
    schedule_cron: str = ""
    enabled: bool = False


class ActionUpdate(BaseModel):
    name: str | None = None
    icon: str | None = None
    config: dict | None = None
    schedule_type: str | None = None
    schedule_interval: int | None = None
    schedule_cron: str | None = None
    enabled: bool | None = None


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


# Allowlist of fields a CRUD client may set on Action. Mirrors ActionCreate /
# ActionUpdate but enforced server-side so a tampered request body cannot
# overwrite ``user_id``, ``last_run_at``, ``created_at``, etc. via ``**dump``.
_ALLOWED_ACTION_CREATE_FIELDS = frozenset({
    "provider_id", "name", "icon", "action_type", "config",
    "schedule_type", "schedule_interval", "schedule_cron", "enabled",
})
_ALLOWED_ACTION_UPDATE_FIELDS = frozenset({
    "name", "icon", "config",
    "schedule_type", "schedule_interval", "schedule_cron", "enabled",
})

# 6 fields = standard cron, 7 fields = with seconds (Quartz-style). Reject
# the 7-field form whose first column allows fires more often than once per
# minute. Also reject ``*/N`` minute patterns where N<1 (so ``*/0``) and the
# bare ``*`` minute used together with ``*`` second.
_DISALLOWED_CRON_PATTERNS = (
    re.compile(r"^\s*\*/0\s+"),  # */0 in any leading position
)


def _validate_cron(expr: str) -> None:
    """Reject schedule_cron strings that fire more often than once per minute.

    Without croniter as a hard dep we apply a conservative regex check: a
    valid 5-field cron's first column is the minute, so anything other than
    ``*``/digits/comma/dash/slash there is bogus, and a sub-minute cadence
    requires a 6+ field expression with seconds. Reject both shapes.
    """
    if not expr or not expr.strip():
        return
    parts = expr.split()
    if len(parts) >= 6:
        # Seconds field present (Quartz-style or 6-field). Forbid
        # second-level fires entirely; minute-cadence is the floor.
        seconds_field = parts[0]
        if seconds_field != "0":
            raise HTTPException(
                status_code=400,
                detail=(
                    "schedule_cron with a sub-minute cadence is not allowed; "
                    "set the seconds field to 0 or use a standard 5-field cron"
                ),
            )
    for pattern in _DISALLOWED_CRON_PATTERNS:
        if pattern.search(expr):
            raise HTTPException(
                status_code=400,
                detail="schedule_cron contains a disallowed pattern",
            )


async def _action_response(session: AsyncSession, action: Action) -> dict:
    """Build response dict with rules inlined."""
    result = await session.exec(
        select(ActionRule)
        .where(ActionRule.action_id == action.id)
        .order_by(ActionRule.order)
    )
    rules = result.all()
    return {
        "id": action.id,
        "user_id": action.user_id,
        "provider_id": action.provider_id,
        "name": action.name,
        "icon": action.icon,
        "action_type": action.action_type,
        "config": action.config,
        "schedule_type": action.schedule_type,
        "schedule_interval": action.schedule_interval,
        "schedule_cron": action.schedule_cron,
        "enabled": action.enabled,
        "last_run_at": action.last_run_at.isoformat() if action.last_run_at else None,
        "last_run_status": action.last_run_status,
        "created_at": action.created_at.isoformat() if action.created_at else None,
        "rules": [
            {
                "id": r.id,
                "action_id": r.action_id,
                "name": r.name,
                "rule_config": r.rule_config,
                "enabled": r.enabled,
                "order": r.order,
                "created_at": r.created_at.isoformat() if r.created_at else None,
            }
            for r in rules
        ],
    }


# ---------------------------------------------------------------------------
# CRUD
# ---------------------------------------------------------------------------


@router.get("")
async def list_actions(
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    result = await session.exec(
        select(Action).where(Action.user_id == user.id)
    )
    actions = result.all()
    return [await _action_response(session, a) for a in actions]


@router.post("", status_code=status.HTTP_201_CREATED)
async def create_action(
    body: ActionCreate,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    provider = await session.get(ServiceProvider, body.provider_id)
    if not provider or provider.user_id != user.id:
        raise HTTPException(status_code=404, detail="Provider not found")

    # Validate action_type against the registry
    from notify_bridge_core.providers.actions import get_action_type
    if not get_action_type(provider.type, body.action_type):
        raise HTTPException(
            status_code=400,
            detail=f"Invalid action type '{body.action_type}' for provider type '{provider.type}'",
        )

    _validate_cron(body.schedule_cron)

    # Project only allowlisted fields so a tampered body can't write
    # ``user_id``, ``id``, ``last_run_at``, etc. via ``**dump``.
    payload = {
        k: v for k, v in body.model_dump().items()
        if k in _ALLOWED_ACTION_CREATE_FIELDS
    }
    action = Action(user_id=user.id, **payload)
    session.add(action)
    await session.commit()
    await session.refresh(action)

    if action.enabled:
        from ..services.scheduler import schedule_action
        await schedule_action(
            action.id,
            action.schedule_type,
            action.schedule_interval,
            action.schedule_cron,
        )

    return await _action_response(session, action)


@router.get("/{action_id}")
async def get_action(
    action_id: int,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")
    return await _action_response(session, action)


@router.put("/{action_id}")
async def update_action(
    action_id: int,
    body: ActionUpdate,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")

    updates = body.model_dump(exclude_unset=True)
    if "schedule_cron" in updates:
        _validate_cron(updates["schedule_cron"] or "")
    # Drop any field outside the update allowlist so a tampered request
    # can't mutate ``user_id`` / ``provider_id`` / ``action_type`` etc.
    for key, value in updates.items():
        if key not in _ALLOWED_ACTION_UPDATE_FIELDS:
            continue
        setattr(action, key, value)
    session.add(action)
    await session.commit()
    await session.refresh(action)

    # Reschedule or unschedule based on enabled state
    from ..services.scheduler import schedule_action, unschedule_action
    if action.enabled:
        await schedule_action(
            action.id,
            action.schedule_type,
            action.schedule_interval,
            action.schedule_cron,
        )
    else:
        await unschedule_action(action.id)

    return await _action_response(session, action)


@router.delete("/{action_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_action(
    action_id: int,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")

    # Unschedule
    from ..services.scheduler import unschedule_action
    await unschedule_action(action.id)

    # Bulk delete rules and executions
    from sqlalchemy import delete
    await session.exec(delete(ActionRule).where(ActionRule.action_id == action_id))
    await session.exec(delete(ActionExecution).where(ActionExecution.action_id == action_id))

    await session.delete(action)
    await session.commit()


# ---------------------------------------------------------------------------
# Execute / Dry-run
# ---------------------------------------------------------------------------


@router.post("/{action_id}/execute")
async def execute_action(
    action_id: int,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")

    from ..services.action_runner import run_action
    result = await run_action(action_id, trigger="manual")
    return result.to_dict()


@router.post("/{action_id}/dry-run")
async def dry_run_action(
    action_id: int,
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")

    from ..services.action_runner import dry_run_action as _dry_run
    result = await _dry_run(action_id)
    return result.to_dict()


# ---------------------------------------------------------------------------
# Execution history
# ---------------------------------------------------------------------------


@router.get("/{action_id}/executions")
async def list_executions(
    action_id: int,
    limit: int = Query(default=20, le=100),
    offset: int = Query(default=0, ge=0),
    user: User = Depends(get_current_user),
    session: AsyncSession = Depends(get_session),
):
    action = await session.get(Action, action_id)
    if not action or action.user_id != user.id:
        raise HTTPException(status_code=404, detail="Action not found")

    result = await session.exec(
        select(ActionExecution)
        .where(ActionExecution.action_id == action_id)
        .order_by(ActionExecution.started_at.desc())
        .offset(offset)
        .limit(limit)
    )
    executions = result.all()
    return [
        {
            "id": e.id,
            "action_id": e.action_id,
            "started_at": e.started_at.isoformat() if e.started_at else None,
            "finished_at": e.finished_at.isoformat() if e.finished_at else None,
            "status": e.status,
            "rules_processed": e.rules_processed,
            "rules_succeeded": e.rules_succeeded,
            "rules_failed": e.rules_failed,
            "total_items_affected": e.total_items_affected,
            "summary": e.summary,
            "error": e.error,
            "trigger": e.trigger,
        }
        for e in executions
    ]
