"""Approvals: the workflow steps waiting on the signed-in person — an open
instance with a branch at a person's step that names them as an approver,
directly (approver_users, or the user in its assignee_field) or through a
role they hold (approver_roles), or that waits on a colleague they stand in
for (app/routers/workflow_delegations.py) — and acting on one: one of the
step's next steps, with a comment; claiming it; handing it to someone.
Acting needs no Workflows permission; being the step's approver is the
permission. The engine (app/crud/workflow_engine.py) makes the move."""

import uuid
from datetime import datetime

from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.core.database import get_db
from app.core.security import AuthContext, get_auth_context
from app.crud import workflow_engine as engine
from app.crud.access_policy import user_roles
from app.models.identity import User
from app.schemas.workflow import WorkflowInstanceRead

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

# Open instances looked at per request, newest first.
MAX_OPEN = 1000


class ApprovalChoice(BaseModel):
    to: str
    label: str
    # Moving there finishes the workflow.
    complete: bool


class ApprovalVote(BaseModel):
    user_id: str
    user_name: str | None
    to: str
    comment: str | None = None
    at: datetime | None = None


class ApprovalTask(BaseModel):
    instance_id: uuid.UUID
    token_id: str | None
    workflow_id: uuid.UUID
    workflow_name: str
    step: str
    step_label: str
    step_type: str
    started_at: datetime
    started_by_name: str | None
    # This step's own deadline, else the instance's.
    sla_due_at: datetime | None
    escalated: bool = False
    context: dict
    choices: list[ApprovalChoice]
    # "any" | "all" | "majority" — with the votes cast so far.
    approval_mode: str = "any"
    votes: list[ApprovalVote] = []
    voters: int = 0
    claimed_by_me: bool = False
    # Set when you're standing in for this colleague.
    on_behalf_of: str | None = None
    on_behalf_of_name: str | None = None


class ApprovalAct(BaseModel):
    to: str = Field(min_length=1, max_length=100)
    comment: str | None = Field(default=None, max_length=2000)
    token_id: str | None = Field(default=None, max_length=40)
    # Values the person enters as they decide (the step's output params).
    context: dict = {}


class ApprovalToken(BaseModel):
    token_id: str = Field(min_length=1, max_length=40)


class ApprovalDelegate(ApprovalToken):
    user_id: uuid.UUID
    comment: str | None = Field(default=None, max_length=2000)


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


async def _actor(db: AsyncSession, ctx: AuthContext) -> engine.Actor:
    roles, _ = await user_roles(db, ctx.tenant_id, ctx.user_id)
    return engine.Actor(user_id=ctx.user_id, roles=roles, vendor_id=ctx.vendor_id)


@router.get("", response_model=list[ApprovalTask])
async def my_approvals(ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)) -> list[ApprovalTask]:
    tasks = await engine.open_tasks_for(db, ctx.tenant_id, await _actor(db, ctx), MAX_OPEN)
    people: set[uuid.UUID] = {i.started_by for i, *_ in tasks if i.started_by}
    for _i, _w, _s, token, as_user in tasks:
        people |= {uuid.UUID(v["user_id"]) for v in token.get("votes") or []}
        people.add(uuid.UUID(as_user))
    names = {uid: name or email for uid, name, email in (await db.execute(select(User.id, User.name, User.email).where(User.id.in_(people)))).all()} if people else {}
    me = str(ctx.user_id)
    out = []
    for instance, workflow, step, token, as_user in tasks:
        context = instance.context or {}
        choices = []
        for t in engine.open_choices(step, context):
            target = engine.find_step(workflow, t["to"])
            if target is not None:
                choices.append(ApprovalChoice(to=target["key"], label=target.get("label") or target["key"], complete=target.get("type") == "end"))
        due = engine._parse_dt(token.get("due_at")) or instance.sla_due_at
        out.append(
            ApprovalTask(
                instance_id=instance.id,
                token_id=token.get("id"),
                workflow_id=instance.workflow_id,
                workflow_name=workflow.name,
                step=step["key"],
                step_label=step.get("label") or step["key"],
                step_type=step.get("type") or "approval",
                started_at=instance.started_at,
                started_by_name=names.get(instance.started_by),
                sla_due_at=due,
                escalated=bool(token.get("escalated")),
                context=context,
                choices=choices,
                approval_mode=step.get("approval_mode") or "any",
                votes=[
                    ApprovalVote(user_id=v["user_id"], user_name=names.get(uuid.UUID(v["user_id"])), to=v["to"], comment=v.get("comment"), at=engine._parse_dt(v.get("at")))
                    for v in token.get("votes") or []
                ],
                voters=len(token.get("voters") or []),
                claimed_by_me=token.get("claimed_by") == me,
                on_behalf_of=None if as_user == me else as_user,
                on_behalf_of_name=None if as_user == me else names.get(uuid.UUID(as_user)),
            )
        )
    return out


@router.post("/{instance_id}/act", response_model=WorkflowInstanceRead)
async def act_on_approval(
    instance_id: uuid.UUID,
    payload: ApprovalAct,
    ctx: AuthContext = Depends(get_auth_context),
    db: AsyncSession = Depends(get_db),
):
    try:
        return await engine.act(
            db,
            ctx.tenant_id,
            await _actor(db, ctx),
            instance_id,
            to=payload.to,
            token_id=payload.token_id,
            comment=(payload.comment or "").strip() or None,
            values=payload.context or None,
        )
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/claim", response_model=WorkflowInstanceRead)
async def claim_approval(instance_id: uuid.UUID, payload: ApprovalToken, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)):
    """Takes a step that waits on a group for yourself: the others stop seeing it."""
    try:
        return await engine.claim(db, ctx.tenant_id, await _actor(db, ctx), instance_id, payload.token_id)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/release", response_model=WorkflowInstanceRead)
async def release_approval(instance_id: uuid.UUID, payload: ApprovalToken, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)):
    """Gives a claimed step back to the group."""
    try:
        return await engine.claim(db, ctx.tenant_id, await _actor(db, ctx), instance_id, payload.token_id, release=True)
    except engine.EngineError as exc:
        raise _http(exc) from exc


@router.post("/{instance_id}/delegate", response_model=WorkflowInstanceRead)
async def delegate_approval(instance_id: uuid.UUID, payload: ApprovalDelegate, ctx: AuthContext = Depends(get_auth_context), db: AsyncSession = Depends(get_db)):
    """Hands this one step to a colleague, with a note."""
    try:
        return await engine.delegate(db, ctx.tenant_id, await _actor(db, ctx), instance_id, payload.token_id, payload.user_id, (payload.comment or "").strip() or None)
    except engine.EngineError as exc:
        raise _http(exc) from exc
