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.agent_message import AgentMessage
from api.models.managed_repo import ManagedRepo
from api.models.task import Task
from api.models.task import TaskStatus
from api.models.workplan import Workplan
from api.schemas.reconciliation import StateChangeRequest, StateChangeResponse
from api.services.lifecycle import (
    should_activate_parent_for_task_start,
    status_value,
    transition_task_status,
    transition_workplan_status,
)
from api.task_status import TERMINAL_TASK_STATUSES
from api.services.reconciliation import (
    ReconciliationClass,
    StateChangeClassification,
    classify_task_status_change,
    classify_workplan_status_change,
)
from api.services.workplan_files import (
    find_workplan_for_workplan,
    patch_task_status,
    patch_workplan_status,
    resolve_repo_path,
    task_block_status,
    task_block_linked,
    workplan_status,
)
from api.workplan_status import normalize_workplan_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


def _conflict(reason: str, follow_up: str) -> StateChangeClassification:
    return StateChangeClassification(
        ReconciliationClass.DEFERRED,
        reason,
        follow_up,
    )


async def _workstream_tasks_terminal(session: AsyncSession, workstream_id: uuid.UUID) -> bool:
    result = await session.execute(select(Task.status).where(Task.workplan_id == workstream_id))
    statuses = [status_value(row[0]) for row in result.all()]
    return bool(statuses) and all(status in TERMINAL_TASK_STATUSES for status in statuses)


def _deferred_message(
    *,
    body: StateChangeRequest,
    current_status: str,
    target_status: str,
    classification: ReconciliationClass,
    reason: str,
    follow_up: str,
    workplan_path: str | None,
    conflict: bool = False,
) -> AgentMessage:
    subject = f"Reconcile {body.target_type} state change: {current_status} -> {target_status}"
    lines = [
        "A UI-originated state change could not be written through immediately.",
        "",
        f"target_type: {body.target_type}",
        f"target_id: {body.target_id}",
        f"actor: {body.actor}",
        f"intent: {body.intent or ''}",
        f"expected_current_status: {body.expected_current_status or ''}",
        f"current_status: {current_status}",
        f"target_status: {target_status}",
        f"reconciliation_class: {classification.value}",
        f"conflict: {str(conflict).lower()}",
        f"reason: {reason}",
        f"follow_up: {follow_up}",
        f"workplan_path: {workplan_path or ''}",
    ]
    return AgentMessage(
        from_agent=body.actor,
        to_agent="state-hub",
        subject=subject,
        body="\n".join(lines),
    )


@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(Workplan, body.target_id)
        if ws is None:
            raise HTTPException(status_code=404, detail="Workplan not found")

        repo = await session.get(ManagedRepo, ws.repo_id) if ws.repo_id else None
        repo_path = resolve_repo_path(repo)
        workplan_ref = find_workplan_for_workplan(repo, ws.id) if repo_path else None
        actual_file_backed = workplan_ref is not None
        actual_archived_file = bool(workplan_ref and workplan_ref.archived)
        file_backed = (
            actual_file_backed
            if body.apply
            else _bool_or_default(body.file_backed, actual_file_backed)
        )
        archived_file = (
            actual_archived_file
            if body.apply
            else _bool_or_default(body.archived_file, actual_archived_file)
        )
        tasks_terminal = (
            body.tasks_terminal
            if body.tasks_terminal is not None
            else await _workstream_tasks_terminal(session, ws.id)
        )
        current_status = normalize_workplan_status(ws.status)
        target_status = normalize_workplan_status(body.target_status)
        classification = classify_workplan_status_change(
            current_status=current_status,
            target_status=target_status,
            file_backed=file_backed,
            archived_file=archived_file,
            tasks_terminal=tasks_terminal,
        )
        write_result = "not_attempted"
        reconciliation_record_id = None
        conflict = False
        if body.apply:
            expected_status = (
                normalize_workplan_status(body.expected_current_status)
                if body.expected_current_status is not None
                else None
            )
            if expected_status is not None and expected_status != current_status:
                classification = _conflict(
                    f"cached workstream status changed from expected {expected_status!r} to {current_status!r}",
                    "refresh the dashboard and retry the state change if it is still intended",
                )
                conflict = True
            elif repo is not None and repo_path is None:
                classification = _conflict(
                    "repo host path is unavailable for this State Hub host",
                    "register a host path for this machine or retry from a host with the repo checkout",
                )
                conflict = True
            elif classification.reconciliation_class == ReconciliationClass.WRITE_THROUGH and workplan_ref:
                file_status = normalize_workplan_status(workplan_status(workplan_ref.path))
                if file_status and file_status != current_status:
                    classification = _conflict(
                        f"workplan file status {file_status!r} differs from cached DB status {current_status!r}",
                        "run consistency repair or refresh State Hub from files before retrying",
                    )
                    conflict = True
                else:
                    try:
                        patch_workplan_status(workplan_ref.path, target_status)
                        patched_status = normalize_workplan_status(workplan_status(workplan_ref.path))
                    except OSError as exc:
                        classification = _conflict(
                            f"workplan file write failed: {exc}",
                            "fix repo file access and retry the reconciliation",
                        )
                        conflict = True
                    else:
                        if patched_status != target_status:
                            classification = _conflict(
                                f"workplan file status could not be patched to {target_status!r}",
                                "inspect the workplan frontmatter format before retrying",
                            )
                            conflict = True
                        else:
                            transition_workplan_status(ws, target_status)
                            await session.commit()
                            write_result = "applied"

            if write_result != "applied":
                msg = _deferred_message(
                    body=body,
                    current_status=current_status,
                    target_status=target_status,
                    classification=classification.reconciliation_class,
                    reason=classification.reason,
                    follow_up=classification.follow_up,
                    workplan_path=workplan_ref.relative_path if workplan_ref else None,
                    conflict=conflict,
                )
                session.add(msg)
                await session.commit()
                await session.refresh(msg)
                reconciliation_record_id = msg.id
                write_result = "not_applicable"
        return StateChangeResponse(
            target_type=body.target_type,
            target_id=body.target_id,
            actor=body.actor,
            intent=body.intent,
            current_status=current_status,
            target_status=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,
            write_through_result=write_result,
            workplan_path=workplan_ref.relative_path if workplan_ref else None,
            reconciliation_record_id=reconciliation_record_id,
            conflict=conflict,
        )

    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(Workplan, task.workplan_id)
    repo = await session.get(ManagedRepo, ws.repo_id) if ws and ws.repo_id else None
    repo_path = resolve_repo_path(repo)
    workplan_ref = find_workplan_for_workplan(repo, ws.id) if ws and repo_path else None
    actual_file_backed = workplan_ref is not None
    actual_archived_file = bool(workplan_ref and workplan_ref.archived)
    file_backed = (
        actual_file_backed
        if body.apply
        else _bool_or_default(body.file_backed, actual_file_backed)
    )
    archived_file = (
        actual_archived_file
        if body.apply
        else _bool_or_default(body.archived_file, actual_archived_file)
    )
    actual_task_linked = bool(workplan_ref and task_block_linked(workplan_ref.path, task.id))
    task_linked = (
        actual_task_linked
        if body.apply
        else _bool_or_default(body.task_linked, actual_task_linked)
    )
    current_status = status_value(task.status)
    target_status = status_value(body.target_status)
    classification = classify_task_status_change(
        current_status=current_status,
        target_status=target_status,
        file_backed=file_backed,
        archived_file=archived_file,
        task_linked=task_linked,
        blocking_reason=body.blocking_reason,
    )
    write_result = "not_attempted"
    reconciliation_record_id = None
    conflict = False
    if body.apply:
        expected_status = (
            status_value(body.expected_current_status)
            if body.expected_current_status is not None
            else None
        )
        if expected_status is not None and expected_status != current_status:
            classification = _conflict(
                f"cached task status changed from expected {expected_status!r} to {current_status!r}",
                "refresh the dashboard and retry the state change if it is still intended",
            )
            conflict = True
        elif repo is not None and repo_path is None:
            classification = _conflict(
                "repo host path is unavailable for this State Hub host",
                "register a host path for this machine or retry from a host with the repo checkout",
            )
            conflict = True
        elif (
            classification.reconciliation_class == ReconciliationClass.WRITE_THROUGH
            and workplan_ref
            and actual_task_linked
        ):
            file_status = status_value(task_block_status(workplan_ref.path, task.id))
            if file_status and file_status != current_status:
                classification = _conflict(
                    f"workplan task status {file_status!r} differs from cached DB status {current_status!r}",
                    "run consistency repair or refresh State Hub from files before retrying",
                )
                conflict = True
            else:
                original_text = None
                parent_will_activate = should_activate_parent_for_task_start(
                    previous_task_status=current_status,
                    new_task_status=target_status,
                    parent_workplan_status=ws.status if ws else None,
                )
                try:
                    original_text = workplan_ref.path.read_text(encoding="utf-8")
                    patch_task_status(workplan_ref.path, task.id, target_status)
                    patched_status = status_value(task_block_status(workplan_ref.path, task.id))
                    if parent_will_activate:
                        patch_workplan_status(workplan_ref.path, "active")
                        parent_status = normalize_workplan_status(workplan_status(workplan_ref.path))
                        if parent_status != "active":
                            if original_text is not None:
                                workplan_ref.path.write_text(original_text, encoding="utf-8")
                            classification = _conflict(
                                "parent workplan status could not be patched to 'active'",
                                "inspect the workplan frontmatter format before retrying",
                            )
                            conflict = True
                except OSError as exc:
                    if original_text is not None:
                        workplan_ref.path.write_text(original_text, encoding="utf-8")
                    classification = _conflict(
                        f"workplan task write failed: {exc}",
                        "fix repo file access and retry the reconciliation",
                    )
                    conflict = True
                else:
                    if conflict:
                        pass
                    elif patched_status != target_status:
                        if original_text is not None:
                            workplan_ref.path.write_text(original_text, encoding="utf-8")
                        classification = _conflict(
                            f"workplan task block could not be patched to {target_status!r}",
                            "inspect the task block format before retrying",
                        )
                        conflict = True
                    else:
                        transition = transition_task_status(
                            task,
                            target_status,
                            parent_workstream=ws,
                            previous_task_status=current_status,
                            status_coercer=TaskStatus,
                        )
                        if body.blocking_reason is not None:
                            task.blocking_reason = body.blocking_reason
                        await session.commit()
                        write_result = "applied"

        if write_result != "applied":
            msg = _deferred_message(
                body=body,
                current_status=current_status,
                target_status=target_status,
                classification=classification.reconciliation_class,
                reason=classification.reason,
                follow_up=classification.follow_up,
                workplan_path=workplan_ref.relative_path if workplan_ref else None,
                conflict=conflict,
            )
            session.add(msg)
            await session.commit()
            await session.refresh(msg)
            reconciliation_record_id = msg.id
            write_result = "not_applicable"
    return StateChangeResponse(
        target_type=body.target_type,
        target_id=body.target_id,
        actor=body.actor,
        intent=body.intent,
        current_status=current_status,
        target_status=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,
        write_through_result=write_result,
        workplan_path=workplan_ref.relative_path if workplan_ref else None,
        reconciliation_record_id=reconciliation_record_id,
        conflict=conflict,
    )
