"""Workflow instances: starting them, following them, and an administrator's
controls. The engine (app/crud/workflow_engine.py) does the moving; this
router checks permissions and turns its refusals into HTTP errors. The
people a step waits on act through app/routers/approvals.py."""

import uuid
from datetime import datetime, timezone

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy import and_, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.permissions import deny_vendor, require_permission, require_run_access
from app.core.security import AuthContext, get_auth_context
from app.crud import workflow_engine as engine
from app.crud.base import CRUDBase
from app.models.identity import User
from app.models.workflow import StepHistory, WorkflowInstance
from app.schemas.workflow import (
    InstanceCancel,
    StepHistoryRead,
    TokenAction,
    TokenMove,
    TokenReassign,
    WorkflowInstanceAdvance,
    WorkflowInstanceCreate,
    WorkflowInstanceRead,
)

# Kept for callers that computed deadlines through this module.
_add_hours = engine.add_hours

router = APIRouter(prefix="/workflow-instances", tags=["workflow-instances"])
crud = CRUDBase[WorkflowInstance, WorkflowInstanceCreate, WorkflowInstanceCreate](WorkflowInstance)


def _http(exc: engine.EngineError) -> HTTPException:
    return HTTPException(status_code=exc.status_code, detail=exc.detail)


@router.get("", response_model=list[WorkflowInstanceRead])
async def list_workflow_instances(
    skip: int = Query(0, ge=0),
    limit: int = Query(100, ge=1, le=500),
    workflow_id: uuid.UUID | None = None,
    parent_instance_id: uuid.UUID | None = None,
    status_: str | None = Query(None, alias="status", pattern="^(in_progress|completed|cancelled|suspended)$"),
    incident: bool | None = None,
    escalated: bool | None = None,
    overdue: bool | None = None,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> list[WorkflowInstance]:
    """Newest first. status "suspended" means running but on hold; incident,
    escalated and overdue narrow to instances with a failed branch, a step
    past its SLA, or the whole instance past its deadline."""
    wi = WorkflowInstance
    stmt = select(wi).where(wi.tenant_id == ctx.tenant_id)
    if workflow_id is not None:
        stmt = stmt.where(wi.workflow_id == workflow_id)
    if parent_instance_id is not None:
        stmt = stmt.where(wi.parent_instance_id == parent_instance_id)
    if status_ == "suspended":
        stmt = stmt.where(wi.status == "in_progress", wi.suspended_at.isnot(None))
    elif status_:
        stmt = stmt.where(wi.status == status_)
    if incident is not None:
        stmt = stmt.where(wi.has_incident.is_(incident))
    if escalated is not None:
        stmt = stmt.where(wi.has_escalation.is_(escalated))
    if overdue:
        stmt = stmt.where(wi.status == "in_progress", wi.sla_paused_at.is_(None), wi.sla_due_at < datetime.now(timezone.utc))
    stmt = stmt.order_by(wi.started_at.desc()).offset(skip).limit(limit)
    return list((await db.execute(stmt)).scalars().all())


@router.get("/summary")
async def instance_summary(
    workflow_id: uuid.UUID | None = None,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> dict:
    """Counts for the monitor's tiles."""
    wi = WorkflowInstance
    scope = [wi.tenant_id == ctx.tenant_id] + ([wi.workflow_id == workflow_id] if workflow_id else [])
    running = and_(*scope, wi.status == "in_progress")
    now = datetime.now(timezone.utc)

    async def count(*where) -> int:
        return int((await db.execute(select(func.count()).select_from(wi).where(*where))).scalar_one())

    return {
        "in_progress": await count(running),
        "suspended": await count(running, wi.suspended_at.isnot(None)),
        "incidents": await count(running, wi.has_incident.is_(True)),
        "escalated": await count(running, wi.has_escalation.is_(True)),
        "overdue": await count(running, wi.sla_paused_at.is_(None), wi.sla_due_at < now),
        "completed": await count(*scope, wi.status == "completed"),
        "cancelled": await count(*scope, wi.status == "cancelled"),
    }


@router.get("/{instance_id}", response_model=WorkflowInstanceRead)
async def get_workflow_instance(
    instance_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> WorkflowInstance:
    return await crud.get(db, ctx.tenant_id, instance_id)


@router.post("", response_model=WorkflowInstanceRead, status_code=status.HTTP_201_CREATED)
async def start_workflow_instance(
    payload: WorkflowInstanceCreate,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_run_access("workflows", "create")),
) -> WorkflowInstance:
    """A request is submitted against the workflow's current PUBLISHED
    version (FR-PUB-004) — starting instances from a draft would let an
    unfinished, unreviewed process run live. Reachable by a Public Portal
    (vendor) caller (require_run_access) since this is the normal way a
    citizen-facing Form/Page's trigger_workflow action actually runs — the
    engine's own published-only check is what keeps that safe. The instance
    runs on from its Start step until something waits."""
    if payload.parent_instance_id is not None:
        await crud.get(db, ctx.tenant_id, payload.parent_instance_id)  # 404s if not in this tenant
    try:
        return await engine.start_instance(
            db,
            ctx.tenant_id,
            ctx.user_id,
            workflow_id=payload.workflow_id,
            initial_step=payload.initial_step,
            vendor_id=payload.vendor_id if payload.vendor_id is not None else ctx.vendor_id,
            sla_due_at=payload.sla_due_at,
            context=payload.context,
            parent_instance_id=payload.parent_instance_id,
            sla_business_days=payload.sla_business_days,
        )
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/pause-sla", response_model=WorkflowInstanceRead)
async def pause_instance_sla(
    instance_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
) -> WorkflowInstance:
    instance = await crud.get(db, ctx.tenant_id, instance_id)
    if instance.sla_due_at is None:
        raise HTTPException(status_code=409, detail="This instance has no SLA clock to pause")
    if instance.sla_paused_at is not None:
        raise HTTPException(status_code=409, detail="Already paused")
    instance.sla_paused_at = datetime.now(timezone.utc)
    await db.flush()
    await db.refresh(instance)
    return instance


@router.post("/{instance_id}/resume-sla", response_model=WorkflowInstanceRead)
async def resume_instance_sla(
    instance_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
) -> WorkflowInstance:
    instance = await crud.get(db, ctx.tenant_id, instance_id)
    if instance.sla_paused_at is None:
        raise HTTPException(status_code=409, detail="Not currently paused")
    instance.sla_paused_seconds += int((datetime.now(timezone.utc) - instance.sla_paused_at).total_seconds())
    instance.sla_paused_at = None
    await db.flush()
    await db.refresh(instance)
    return instance


@router.get("/{instance_id}/history", response_model=list[StepHistoryRead])
async def get_instance_history(
    instance_id: uuid.UUID,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "view")),
) -> list[StepHistoryRead]:
    await crud.get(db, ctx.tenant_id, instance_id)  # 404s if not in this tenant
    rows = (
        await db.execute(
            select(StepHistory, User.name)
            .outerjoin(User, User.id == StepHistory.actor_id)
            .where(StepHistory.tenant_id == ctx.tenant_id, StepHistory.instance_id == instance_id)
            .order_by(StepHistory.occurred_at, StepHistory.id)
        )
    ).all()
    return [StepHistoryRead.model_validate(h).model_copy(update={"actor_name": name}) for h, name in rows]


@router.post("/{instance_id}/advance", response_model=WorkflowInstanceRead)
async def advance_workflow_instance(
    instance_id: uuid.UUID,
    payload: WorkflowInstanceAdvance,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
    _: AuthContext = Depends(require_permission("workflows", "edit")),
) -> WorkflowInstance:
    """Moves a branch to the step named — the Workflow Designer's test run
    and an administrator's correction. The people a step waits on use
    /approvals/{id}/act instead, which allows only the step's own choices."""
    try:
        return await engine.admin_move(
            db,
            ctx.tenant_id,
            ctx.user_id,
            instance_id,
            to=payload.next_step,
            token_id=payload.token_id,
            action=payload.action,
            comment=payload.comment,
            values=payload.context,
            complete=payload.complete,
            spawn=payload.spawn,
        )
    except engine.EngineError as exc:
        raise _http(exc) from exc


# ---- administrators -------------------------------------------------------------------------------------

_admin = [Depends(require_permission("workflows", "edit")), Depends(deny_vendor())]


@router.post("/{instance_id}/cancel", response_model=WorkflowInstanceRead, dependencies=_admin)
async def cancel_instance(instance_id: uuid.UUID, payload: InstanceCancel, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """Stops the instance for good — and any sub-workflows it's waiting on."""
    try:
        return await engine.admin_cancel(db, ctx.tenant_id, ctx.user_id, instance_id, payload.reason.strip())
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/suspend", response_model=WorkflowInstanceRead, dependencies=_admin)
async def suspend_instance(instance_id: uuid.UUID, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """Puts it on hold: nobody can act on it, and its timers and SLAs wait."""
    try:
        return await engine.admin_suspend(db, ctx.tenant_id, ctx.user_id, instance_id, resume=False)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/resume", response_model=WorkflowInstanceRead, dependencies=_admin)
async def resume_instance(instance_id: uuid.UUID, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    try:
        return await engine.admin_suspend(db, ctx.tenant_id, ctx.user_id, instance_id, resume=True)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/reassign", response_model=WorkflowInstanceRead, dependencies=_admin)
async def reassign_token(instance_id: uuid.UUID, payload: TokenReassign, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """The step now waits on these people instead of its approvers."""
    try:
        return await engine.admin_reassign(db, ctx.tenant_id, ctx.user_id, instance_id, payload.token_id, payload.user_ids, payload.role_ids, payload.comment)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/retry", response_model=WorkflowInstanceRead, dependencies=_admin)
async def retry_token(instance_id: uuid.UUID, payload: TokenAction, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """Runs a failed step again."""
    try:
        return await engine.admin_retry(db, ctx.tenant_id, ctx.user_id, instance_id, payload.token_id, payload.comment)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/skip", response_model=WorkflowInstanceRead, dependencies=_admin)
async def skip_token(instance_id: uuid.UUID, payload: TokenAction, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """Moves a waiting or failed step on as if it had finished."""
    try:
        return await engine.admin_skip(db, ctx.tenant_id, ctx.user_id, instance_id, payload.token_id, payload.comment)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/move", response_model=WorkflowInstanceRead, dependencies=_admin)
async def move_token(instance_id: uuid.UUID, payload: TokenMove, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> WorkflowInstance:
    """Moves a branch to any step of the workflow."""
    try:
        return await engine.admin_move(db, ctx.tenant_id, ctx.user_id, instance_id, to=payload.to, token_id=payload.token_id, comment=payload.comment)
    except engine.EngineError as exc:
        raise _http(exc) from exc
