import uuid from fastapi import APIRouter, Depends, HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from api.database import get_session from api.models.task import Task from api.models.workstream import Workstream from api.schemas.reconciliation import StateChangeRequest, StateChangeResponse from api.services.lifecycle import status_value from api.services.reconciliation import ( classify_task_status_change, classify_workstream_status_change, ) from api.workplan_status import normalize_workstream_status router = APIRouter(prefix="/reconciliation", tags=["reconciliation"]) def _bool_or_default(value: bool | None, default: bool) -> bool: return default if value is None else value async def _workstream_tasks_terminal(session: AsyncSession, workstream_id: uuid.UUID) -> bool: result = await session.execute(select(Task.status).where(Task.workstream_id == workstream_id)) statuses = [status_value(row[0]) for row in result.all()] return bool(statuses) and all(status in {"done", "cancelled"} for status in statuses) @router.post("/state-change", response_model=StateChangeResponse) async def classify_state_change( body: StateChangeRequest, session: AsyncSession = Depends(get_session), ) -> StateChangeResponse: if body.target_type == "workstream": ws = await session.get(Workstream, body.target_id) if ws is None: raise HTTPException(status_code=404, detail="Workstream not found") file_backed = _bool_or_default(body.file_backed, ws.repo_id is not None) archived_file = _bool_or_default(body.archived_file, False) tasks_terminal = ( body.tasks_terminal if body.tasks_terminal is not None else await _workstream_tasks_terminal(session, ws.id) ) classification = classify_workstream_status_change( current_status=ws.status, target_status=body.target_status, file_backed=file_backed, archived_file=archived_file, tasks_terminal=tasks_terminal, ) return StateChangeResponse( target_type=body.target_type, target_id=body.target_id, actor=body.actor, intent=body.intent, current_status=normalize_workstream_status(ws.status), target_status=normalize_workstream_status(body.target_status), file_backed=file_backed, archived_file=archived_file, tasks_terminal=tasks_terminal, reconciliation_class=classification.reconciliation_class, reason=classification.reason, follow_up=classification.follow_up, ) task = await session.get(Task, body.target_id) if task is None: raise HTTPException(status_code=404, detail="Task not found") ws = await session.get(Workstream, task.workstream_id) file_backed = _bool_or_default(body.file_backed, bool(ws and ws.repo_id)) archived_file = _bool_or_default(body.archived_file, False) task_linked = _bool_or_default(body.task_linked, True) classification = classify_task_status_change( current_status=task.status, target_status=body.target_status, file_backed=file_backed, archived_file=archived_file, task_linked=task_linked, blocking_reason=body.blocking_reason, ) return StateChangeResponse( target_type=body.target_type, target_id=body.target_id, actor=body.actor, intent=body.intent, current_status=status_value(task.status), target_status=status_value(body.target_status), file_backed=file_backed, archived_file=archived_file, task_linked=task_linked, reconciliation_class=classification.reconciliation_class, reason=classification.reason, follow_up=classification.follow_up, )