"""Approvals router: the human-in-the-loop inbox (build plan Section 7.2).

Approvers (holders of the ``approvals:decide`` permission) see pending sensitive
actions and approve or reject them. Approving resumes the run and executes the
action; rejecting resumes it so the agent can replan. Expired approvals cannot be
decided. Every decision is audited.
"""

from __future__ import annotations

import uuid
from datetime import UTC, datetime

from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.orm import Session, sessionmaker

from legate.agent.enums import ApprovalStatus, RunStatus
from legate.agent.service import launch_resume
from legate.api.deps import Principal, require
from legate.api.schemas import ApprovalDecision, ApprovalDecisionResponse, ApprovalOut
from legate.audit.service import record as audit_record
from legate.db.models.run import Run
from legate.db.session import get_db, get_session_factory
from legate.policy.rbac import Permission
from legate.repositories import approvals as approvals_repo

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


def _expire_if_needed(db: Session, approval) -> bool:
    """Mark a pending-but-expired approval as expired. Returns True if it expired."""
    if approval.status != ApprovalStatus.PENDING or approval.expires_at is None:
        return False
    # SQLite returns naive datetimes; treat a naive expiry as UTC.
    expires_at = approval.expires_at
    if expires_at.tzinfo is None:
        expires_at = expires_at.replace(tzinfo=UTC)
    if expires_at < datetime.now(UTC):
        approval.status = ApprovalStatus.EXPIRED
        db.commit()
        return True
    return False


@router.get("", response_model=list[ApprovalOut])
def list_approvals(
    status_filter: ApprovalStatus | None = Query(default=None, alias="status"),
    limit: int = Query(default=100, ge=1, le=500),
    offset: int = Query(default=0, ge=0),
    principal: Principal = Depends(require(Permission.VIEW)),
    db: Session = Depends(get_db),
) -> list[ApprovalOut]:
    approvals = approvals_repo.list_for_workspace(
        db, workspace_id=principal.workspace_id, status=status_filter, limit=limit, offset=offset
    )
    for approval in approvals:
        _expire_if_needed(db, approval)
    return [ApprovalOut.model_validate(a) for a in approvals]


@router.get("/{approval_id}", response_model=ApprovalOut)
def get_approval(
    approval_id: uuid.UUID,
    principal: Principal = Depends(require(Permission.VIEW)),
    db: Session = Depends(get_db),
) -> ApprovalOut:
    approval = approvals_repo.get(db, workspace_id=principal.workspace_id, approval_id=approval_id)
    if approval is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="approval not found")
    _expire_if_needed(db, approval)
    return ApprovalOut.model_validate(approval)


async def _decide(
    approval_id: uuid.UUID,
    *,
    approved: bool,
    reason: str,
    principal: Principal,
    db: Session,
    session_factory: sessionmaker,
) -> ApprovalDecisionResponse:
    approval = approvals_repo.get(db, workspace_id=principal.workspace_id, approval_id=approval_id)
    if approval is None:
        raise HTTPException(status.HTTP_404_NOT_FOUND, detail="approval not found")

    if _expire_if_needed(db, approval):
        raise HTTPException(status.HTTP_409_CONFLICT, detail="approval has expired")
    if approval.status != ApprovalStatus.PENDING:
        raise HTTPException(
            status.HTTP_409_CONFLICT, detail=f"approval already {approval.status.value}"
        )
    # Optional per-approval role gate (in addition to the approvals:decide permission).
    if approval.required_role is not None and principal.role != approval.required_role:
        raise HTTPException(
            status.HTTP_403_FORBIDDEN,
            detail=f"this approval requires the {approval.required_role.value} role",
        )

    approval.status = ApprovalStatus.APPROVED if approved else ApprovalStatus.REJECTED
    approval.decided_by = principal.user.id if principal.user else None
    approval.decided_at = datetime.now(UTC)
    approval.decision_reason = reason or None
    audit_record(
        db,
        workspace_id=principal.workspace_id,
        actor=principal.actor,
        action="approval.approve" if approved else "approval.reject",
        target=str(approval.id),
        metadata={"tool": approval.requested_tool, "run_id": str(approval.run_id)},
    )
    db.commit()

    run_id = approval.run_id
    await launch_resume(run_id, approval.id, approved, session_factory)

    db.expire_all()
    run = db.get(Run, run_id)
    approval = approvals_repo.get(db, workspace_id=principal.workspace_id, approval_id=approval_id)
    return ApprovalDecisionResponse(
        approval=ApprovalOut.model_validate(approval),
        run_status=run.status.value if run else RunStatus.FAILED.value,
    )


@router.post("/{approval_id}/approve", response_model=ApprovalDecisionResponse)
async def approve(
    approval_id: uuid.UUID,
    body: ApprovalDecision | None = None,
    principal: Principal = Depends(require(Permission.APPROVE)),
    db: Session = Depends(get_db),
    session_factory: sessionmaker = Depends(get_session_factory),
) -> ApprovalDecisionResponse:
    return await _decide(
        approval_id,
        approved=True,
        reason=body.reason if body else "",
        principal=principal,
        db=db,
        session_factory=session_factory,
    )


@router.post("/{approval_id}/reject", response_model=ApprovalDecisionResponse)
async def reject(
    approval_id: uuid.UUID,
    body: ApprovalDecision | None = None,
    principal: Principal = Depends(require(Permission.APPROVE)),
    db: Session = Depends(get_db),
    session_factory: sessionmaker = Depends(get_session_factory),
) -> ApprovalDecisionResponse:
    return await _decide(
        approval_id,
        approved=False,
        reason=body.reason if body else "",
        principal=principal,
        db=db,
        session_factory=session_factory,
    )
