"""API задач M1/M2: быстрый захват, стек, детализация (LLM-предложение), CRUD."""

from typing import Any, cast

from fastapi import APIRouter, BackgroundTasks, HTTPException, Query, Response
from sqlalchemy import Select, or_, select

from app.dependencies import DbDep, UserDep
from app.models import CoinEvent, Project, Tag, Task, utcnow
from app.realtime import publish
from app.schemas import (
    DEADLINE_PERIODS,
    RECUR_KINDS,
    ApproveIn,
    SuggestIn,
    TaskCreate,
    TaskOut,
    TaskUpdate,
)
from app.services.closing import handle_task_closed
from app.services.detailing import apply_proposal, detail_task
from app.services.options import pick_options
from app.services.xp import (
    coins_for_create,
    grant_create_xp,
)

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

VALID_STATUSES = {"to_do", "in_progress", "done", "cancelled", "deferred"}


def _get_task_or_404(db: Any, task_id: int) -> Task:
    task = cast(Task | None, db.get(Task, task_id))
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return task


def _get_task_for_update(db: Any, task_id: int) -> Task:
    """Задача с блокировкой строки (Postgres FOR UPDATE; в SQLite игнорируется).

    Параллельные PATCH одной задачи сериализуются: двойное закрытие не гонит
    двух наград (уникальный XpEvent.task_id — вторая линия обороны).
    """
    task = cast(
        Task | None,
        # of=Task: запрос тянет project joined-ом — Postgres запрещает FOR UPDATE
        # на nullable-стороне outer join, блокируем только строку задачи
        db.scalar(select(Task).where(Task.id == task_id).with_for_update(of=Task)),
    )
    if task is None:
        raise HTTPException(status_code=404, detail="Task not found")
    return task


def _archived_project_ids(db: Any) -> Select[tuple[int]]:
    """id проектов в архиве — их задачи в рабочих видах не показываются."""
    return select(Project.id).where(Project.is_archived.is_(True))


def _validate_parent(db: Any, task: Task | None, parent_id: int | None) -> None:
    """Родитель должен существовать; циклы в дереве запрещены."""
    if parent_id is None:
        return
    if task is not None and parent_id == task.id:
        raise HTTPException(status_code=400, detail="Task cannot be its own parent")
    parent = cast(Task | None, db.get(Task, parent_id))
    if parent is None:
        raise HTTPException(status_code=400, detail="Unknown parent task")
    if task is not None:
        ancestor: Task | None = parent
        while ancestor is not None:
            if ancestor.id == task.id:
                raise HTTPException(status_code=400, detail="Cycle in task tree")
            ancestor = ancestor.parent


@router.post("")
async def create_task(
    schema: TaskCreate, db: DbDep, user: UserDep, background: BackgroundTasks, response: Response
) -> dict[str, int]:
    """Быстрый захват: достаточно title — задача попадает в стек (raw, to_do).

    С parent_task_id — создание подзадачи (M3). Сразу в фоне запускается
    автодетализация (LLM-предложение метаданных).
    """
    _validate_parent(db, None, schema.parent_task_id)
    task = Task(
        title=schema.title, description=schema.description, parent_task_id=schema.parent_task_id
    )
    db.add(task)
    db.flush()
    # Микронаграда за создание (ТЗ 3.13): без конфетти, просто тост —
    # заголовки X-Created-* фронт буферизует отдельно от награды за закрытие
    grant_create_xp(db, "create_task")
    db.add(CoinEvent(source="create_task", amount=coins_for_create("create_task")))
    response.headers["X-Created-XP"] = "2"
    response.headers["X-Created-Coins"] = "1"
    # Коммитим до планирования фоновой работы: BackgroundTasks выполняются ДО
    # teardown-коммита зависимости get_db, а detail_task открывает свою сессию.
    db.commit()
    publish(str(user["user_id"]), "task.changed", {"id": task.id})
    publish(str(user["user_id"]), "xp.changed", {"celebrate": False})
    background.add_task(detail_task, task.id)
    return {"id": task.id}


@router.get("", response_model=list[TaskOut])
async def list_tasks(
    db: DbDep,
    user: UserDep,
    detail_state: str | None = Query(None),
    status: str | None = Query(None),
    project_id: int | None = Query(None),
    tag_id: int | None = Query(None),
    parent_id: int | None = Query(None),
    include_archived: bool = Query(False),
) -> list[Task]:
    stmt = select(Task).order_by(Task.created_at.desc())
    if detail_state:
        stmt = stmt.where(Task.detail_state == detail_state)
    if status:
        stmt = stmt.where(Task.status == status)
    if project_id:
        stmt = stmt.where(Task.project_id == project_id)
    elif not include_archived:
        # задачи архивных проектов — только в истории (ТЗ 3.11)
        stmt = stmt.where(
            or_(Task.project_id.is_(None), Task.project_id.not_in(_archived_project_ids(db)))
        )
    if parent_id is not None:
        stmt = stmt.where(Task.parent_task_id == parent_id)
    if tag_id:
        stmt = stmt.where(Task.tags.any(Tag.id == tag_id))
    return list(db.scalars(stmt).all())


@router.get("/{task_id}", response_model=TaskOut)
async def get_task(task_id: int, db: DbDep, user: UserDep) -> Task:
    return _get_task_or_404(db, task_id)


@router.patch("/{task_id}", response_model=TaskOut)
async def update_task(
    task_id: int, schema: TaskUpdate, db: DbDep, user: UserDep, response: Response
) -> Task:
    task = _get_task_for_update(db, task_id)
    data = schema.model_dump(exclude_unset=True)

    if "title" in data and not str(data["title"]).strip():
        raise HTTPException(status_code=422, detail="title cannot be empty")
    if "status" in data and data["status"] not in VALID_STATUSES:
        raise HTTPException(status_code=422, detail=f"Unknown status: {data['status']}")
    if "deadline_period" in data and data["deadline_period"] not in (None,) + DEADLINE_PERIODS:
        raise HTTPException(
            status_code=422, detail=f"Unknown deadline period: {data['deadline_period']}"
        )
    if "recur_kind" in data and data["recur_kind"] not in (None,) + RECUR_KINDS:
        raise HTTPException(
            status_code=422, detail=f"Unknown recurrence rule: {data['recur_kind']}"
        )
    if "task_type" in data and data["task_type"] not in ("one_time", "recurring"):
        raise HTTPException(status_code=422, detail=f"Unknown task type: {data['task_type']}")
    if "project_id" in data and data["project_id"] is not None:
        if db.get(Project, data["project_id"]) is None:
            raise HTTPException(status_code=400, detail="Unknown project")
    if "parent_task_id" in data:
        _validate_parent(db, task, data["parent_task_id"])
    if "tag_ids" in data:
        tag_ids = data.pop("tag_ids") or []
        tags = db.scalars(select(Tag).where(Tag.id.in_(tag_ids))).all()
        if len(tags) != len(set(tag_ids)):
            raise HTTPException(status_code=400, detail="Unknown tag id in tag_ids")
        task.tags = list(tags)
    # Пометка источника закрытия (геймификация): не поле задачи, в setattr не идёт
    earned_via = data.pop("earned_via", None)
    earned_event = None

    for field, value in data.items():
        setattr(task, field, value)
    if data.get("status") == "done" and task.done_at is None:
        task.done_at = utcnow()
        # Регулярная (ТЗ 3.5) + геймификация (ТЗ 3.13) — общий путь с MCP:
        # закрытие агентом даёт те же спавн/XP/монеты, что и из UI
        outcome = handle_task_closed(db, task, via_options=earned_via == "options")
        event = outcome.event
        if event is not None:
            response.headers["X-Earned-XP"] = str(event.amount)
            response.headers["X-Plant-Rarity"] = event.rarity
            if event.via_options:
                response.headers["X-Options-Close"] = "1"
            if task.mentally_hard:
                # «Ментально сложная» — фронт украшает праздник («сила воли»)
                response.headers["X-Mentally-Hard"] = "1"
            response.headers["X-Earned-Coins"] = str(outcome.coins)
            earned_event = event
    elif data.get("status") not in (None, "done") and task.done_at is not None:
        # Выход из done: отметка выполнения сбрасывается. Повторное закрытие
        # снова пройдёт через ветку наград, но XP grant_task_xp повторно не
        # даст, а спавн удержит spawned_at — дубликатов не будет
        task.done_at = None

    db.flush()
    db.refresh(task)  # перечитать связи (project/tags) после обновления
    # События после коммита: подписчик SSE может сразу перечитать данные
    db.commit()
    publish(str(user["user_id"]), "task.changed", {"id": task.id})
    if earned_event is not None:
        publish(
            str(user["user_id"]),
            "xp.changed",
            {"amount": earned_event.amount, "rarity": earned_event.rarity, "celebrate": True},
        )
        publish(str(user["user_id"]), "garden.changed", {"reason": "task"})
    return task


@router.post("/{task_id}/approve", response_model=TaskOut)
async def approve_task(
    task_id: int, db: DbDep, user: UserDep, schema: ApproveIn | None = None
) -> Task:
    """Утверждение детализации: raw → approved.

    schema.apply_proposal=True — принять предложение автодетализации перед
    утверждением (кнопка «Да, всё верно»); иначе предложенные метаданные
    игнорируются (после ручной правки их уже применил PATCH).
    """
    task = _get_task_or_404(db, task_id)
    if schema and schema.apply_proposal and task.ai_proposal:
        apply_proposal(db, task, task.ai_proposal)
    task.detail_state = "approved"
    task.approved_at = utcnow()
    db.flush()
    db.refresh(task)
    db.commit()
    publish(str(user["user_id"]), "task.changed", {"id": task.id})
    return task


@router.post("/{task_id}/redetail", response_model=TaskOut)
async def redetail_task(
    task_id: int, db: DbDep, user: UserDep, background: BackgroundTasks
) -> Task:
    """Перезапустить автодетализацию (например, после появления новых тегов/проектов)."""
    task = _get_task_or_404(db, task_id)
    if task.detail_state != "raw":
        # Утверждённая задача уже детализирована: повтор — no-op, предложение
        # не сбрасываем, фоновую LLM не будим (worker всё равно работает
        # только с raw)
        return task
    task.ai_proposal = None
    # Коммит до фоновой работы, чтобы worker не прочитал старое состояние
    # и его запись не перезаписалась teardown-коммитом (см. create_task).
    db.commit()
    publish(str(user["user_id"]), "task.changed", {"id": task.id})
    background.add_task(detail_task, task.id)
    return task


@router.post("/suggest", response_model=list[TaskOut])
async def suggest_tasks(schema: SuggestIn, db: DbDep, user: UserDep) -> list[Task]:
    """Режим «3 варианта»: до трёх задач под доступное время."""
    return pick_options(db, schema.available_minutes)


@router.delete("/{task_id}")
async def delete_task(task_id: int, db: DbDep, user: UserDep) -> dict[str, bool]:
    db.delete(_get_task_or_404(db, task_id))
    db.commit()
    publish(str(user["user_id"]), "task.deleted", {"id": task_id})
    return {"ok": True}
