"""Punchlist's OWN routes — the product (plan §1-§3).

Protected by the default-deny middleware automatically. Authorization split:
  * PUNCHLIST access (client or team) — kit instance visibility; an unknown OR
    invisible punchlist is 404, never 403 (a 403 would confirm the id exists).
  * TEAM capability — `items.act_team` (queue, team steps, resolving flags).
  * COMPOSE — `items.compose` (creating items); AUTHOR — `workflows.author`.
The engine (services/items.py) enforces workflow legality; routers translate.
"""

from fastapi import APIRouter, Body, Depends, File, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse

from app import accounts, bw_config
from app import bw_accounts as bwa
from app.db import get_session_factory
from app.identity import optional_user, real_user
from app.models import Item, Punchlist
from app.schemas import SpecError, validate_spec
from app.services import items as items_svc
from app.config import get_settings
from app.services import attachments as att_svc
from app.services import interaction
from app.services import punchlists as pl_svc
from app.services import sets as sets_svc
from app.services import workflows as wf_svc

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

_STATUS = {"FORBIDDEN": 403, "NOT_YOUR_STEP": 403,
           "NO_SUCH_TEMPLATE": 404, "NO_SUCH_STEP": 404, "NO_SUCH_TARGET": 404,
           "NO_SUCH_ACTION": 404, "NO_SUCH_SET": 404,
           "EXISTS": 409, "ITEM_DONE": 409, "TARGET_DONE": 409, "NO_FLAG": 409,
           "BAD_INPUT": 400, "MISSING_VARIABLES": 400, "MESSAGE_REQUIRED": 400,
           "FIELD_REQUIRED": 400, "FIELD_INVALID": 400, "NO_INSTANCES": 400,
           "NOT_ASSIGNABLE": 403, "NO_SUCH_INSTANCE": 404,
           "UNSUPPORTED_TYPE": 400, "TOO_LARGE": 413, "CONTENT_MISMATCH": 400,
           "ITEM_FULL": 409, "NO_SUCH_ATTACHMENT": 404,
           "INCOMPATIBLE_VERSION": 409}


def _raise(exc):
    code = getattr(exc, "code", "BAD_INPUT")
    raise HTTPException(status_code=_STATUS.get(code, 400),
                        detail={"error_code": code, "summary": str(exc)})


def _not_found():
    return HTTPException(status_code=404,
                         detail={"error_code": "NO_SUCH_INSTANCE",
                                 "summary": f"No such {bw_config.INSTANCE_NOUN}."})


def _spec_invalid(exc: SpecError):
    raise HTTPException(status_code=422,
                        detail={"error_code": "SPEC_INVALID",
                                "summary": f"{len(exc.errors)} problem(s) in the workflow spec.",
                                "errors": exc.errors})


def _is_team(user: str) -> bool:
    return accounts.app_can(user, accounts.PERM_ACT_TEAM)


def _require(cond: bool, code="FORBIDDEN", summary="You can't do that here."):
    if not cond:
        raise HTTPException(status_code=_STATUS.get(code, 403),
                            detail={"error_code": code, "summary": summary})


def _visible_punchlist(session, user: str, punchlist_id: str) -> Punchlist:
    row = pl_svc.get(session, punchlist_id)
    if row is None or not accounts.has_instance_access(user, punchlist_id):
        raise _not_found()
    return row


def _item_and_punchlist(session, user: str, item_id: str) -> tuple[Item, Punchlist]:
    item = session.get(Item, item_id)
    if item is None:
        raise _not_found()
    return item, _visible_punchlist(session, user, item.punchlist_id)


@router.get("/whoami")
def whoami(request: Request, user: str | None = Depends(optional_user)) -> dict:
    return {"username": user, "real_user": real_user(request),
            "is_owner": accounts.is_owner(user or "")}


# ------------------------------------------------------------------ punchlists

@router.get("/punchlists")
def list_punchlists(user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    with get_session_factory()() as session:
        rows = pl_svc.visible_to(session, u)
        out = []
        for r in rows:
            counts = {"todo": 0, "waiting_on_us": 0, "done": 0}
            for it in session.query(Item).filter(Item.punchlist_id == r.id):
                if it.status == "done":
                    counts["done"] += 1
                elif it.status == "waiting_on_team":
                    counts["waiting_on_us"] += 1
                else:
                    counts["todo"] += 1
            out.append({"id": r.id, "title": r.title, "state": r.state,
                        "client_label": r.client_label, "counts": counts})
        return {"punchlists": out, "is_team": _is_team(u)}


@router.post("/punchlists", status_code=201)
def create_punchlist(user: str | None = Depends(optional_user),
                     body: dict = Body(...)) -> dict:
    u = user or ""
    with get_session_factory()() as session:
        try:
            # kit enforces instances.create inside create_instance
            row = pl_svc.create(session, u, body.get("title") or "",
                                client_label=body.get("client_label") or "")
        except (pl_svc.PunchlistError, bwa.AccountsError) as exc:
            _raise(exc)
        session.commit()
    _report_instances()
    return {"success": True, "id": row.id, "title": row.title}


@router.get("/punchlists/{punchlist_id}")
def get_punchlist(punchlist_id: str,
                  user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    team = _is_team(u)
    with get_session_factory()() as session:
        row = _visible_punchlist(session, u, punchlist_id)
        seen = pl_svc.last_seen(session, punchlist_id, u)
        items = [items_svc.render_item(session, it, for_team=team, last_seen=seen)
                 for it in session.query(Item).filter(Item.punchlist_id == row.id)
                                              .order_by(Item.position, Item.created_at)]
        return {"id": row.id, "title": row.title, "state": row.state,
                "client_label": row.client_label, "is_team": team, "items": items}


@router.post("/punchlists/{punchlist_id}/seen")
def punchlist_seen(punchlist_id: str,
                   user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    with get_session_factory()() as session:
        _visible_punchlist(session, u, punchlist_id)
        pl_svc.mark_seen(session, punchlist_id, u)
        session.commit()
    return {"success": True}


# ------------------------------------------------------------------ items

@router.post("/punchlists/{punchlist_id}/items", status_code=201)
def create_item(punchlist_id: str, user: str | None = Depends(optional_user),
                body: dict = Body(...)) -> dict:
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_COMPOSE), "FORBIDDEN",
             "Creating items needs the compose permission.")
    with get_session_factory()() as session:
        _visible_punchlist(session, u, punchlist_id)
        try:
            item = items_svc.instantiate(
                session, u, punchlist_id,
                template_key=body.get("template_key"),
                spec_inline=body.get("spec"),
                variables=body.get("variables") or {})
        except SpecError as exc:
            _spec_invalid(exc)
        except items_svc.ItemError as exc:
            _raise(exc)
        session.commit()
        return {"success": True, "id": item.id,
                "item": items_svc.render_item(session, item, for_team=True)}


@router.post("/items/{item_id}/actions")
def act_on_item(item_id: str, user: str | None = Depends(optional_user),
                body: dict = Body(...)) -> dict:
    u = user or ""
    team = _is_team(u)
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        try:
            items_svc.act(
                session, item,
                actor=u, actor_kind=("team" if team else "client"),
                action=(body.get("action") or ""),
                target=body.get("target"),
                fields=body.get("fields"),
                message=body.get("message"),
                to=body.get("to"))
        except items_svc.ItemError as exc:
            _raise(exc)
        session.commit()
        seen = pl_svc.last_seen(session, item.punchlist_id, u)
        return {"success": True,
                "item": items_svc.render_item(session, item, for_team=team, last_seen=seen)}


@router.get("/items/{item_id}/events")
def item_events(item_id: str, user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    _require(_is_team(u), "FORBIDDEN", "The event timeline is team-only.")
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        return {"events": [
            {"seq": e.seq, "at": e.created_at.isoformat(), "by": e.actor_username,
             "kind": e.actor_kind, "action": e.action_key, "step": e.step_id,
             "to": e.to_step, "target": e.target, "fields": e.field_values,
             "message": e.message}
            for e in items_svc.events_of(session, item)]}


@router.post("/items/{item_id}/adopt-latest-template")
def adopt_latest(item_id: str, user: str | None = Depends(optional_user)) -> dict:
    """Move ONE item onto the newest published version of its workflow."""
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_ACT_TEAM), "FORBIDDEN",
             "Adopting a newer workflow version is a team action.")
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        try:
            result = items_svc.adopt_latest_template(session, item, u)
        except items_svc.ItemError as exc:
            _raise(exc)
        session.commit()
        return {"success": True, **result,
                "item": items_svc.render_item(session, item, for_team=True)}


@router.post("/punchlists/{punchlist_id}/adopt-latest-templates")
def adopt_latest_all(punchlist_id: str, user: str | None = Depends(optional_user)) -> dict:
    """Same, for every library-backed item on a punchlist. Items that cannot
    move (incompatible version, one-off spec) are REPORTED, never silently
    skipped — a partial upgrade you don't know about is worse than none."""
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_ACT_TEAM), "FORBIDDEN",
             "Adopting newer workflow versions is a team action.")
    with get_session_factory()() as session:
        _visible_punchlist(session, u, punchlist_id)
        moved, held = [], []
        for item in items_svc.items_of(session, punchlist_id):
            try:
                r = items_svc.adopt_latest_template(session, item, u)
                if r.get("changed"):
                    moved.append({"id": item.id, "title": item.title, **r})
            except items_svc.ItemError as exc:
                held.append({"id": item.id, "title": item.title,
                             "reason": str(exc), "error_code": exc.code})
        session.commit()
        return {"success": True, "moved": moved, "held": held}


# ------------------------------------------------------------------ conversation

@router.post("/items/{item_id}/comments", status_code=201)
def add_comment(item_id: str, user: str | None = Depends(optional_user),
                body: dict = Body(...)) -> dict:
    u = user or ""
    team = _is_team(u)
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        text = (body.get("body") or "").strip()
        if not text:
            raise HTTPException(status_code=400,
                                detail={"error_code": "BAD_INPUT",
                                        "summary": "Say something first."})
        interaction.add_comment(session, item, author=u,
                                author_kind=("team" if team else "client"), body=text)
        session.commit()
        seen = pl_svc.last_seen(session, item.punchlist_id, u)
        return {"success": True,
                "item": items_svc.render_item(session, item, for_team=team, last_seen=seen)}


@router.get("/items/{item_id}/comments")
def item_comments(item_id: str, user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        return {"comments": [
            {"by": c.author, "kind": c.author_kind, "text": c.body,
             "at": c.created_at.isoformat()}
            for c in interaction.comments_of(session, item)]}


@router.post("/items/{item_id}/attachments", status_code=201)
def upload_attachment(item_id: str, user: str | None = Depends(optional_user),
                      file: UploadFile = File(...)) -> dict:
    """A file for this item's conversation. Same visibility rule as
    commenting; the response's `markdown` goes straight into the composer."""
    u = user or ""
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        iid = item.id
    data = file.file.read(att_svc.MAX_BYTES + 1)
    try:
        saved = att_svc.save(get_settings().data_dir / "uploads", iid,
                             file.filename or "", data)
    except att_svc.AttachmentError as exc:
        _raise(exc)
    return {"success": True, "attachment": saved}


@router.get("/items/{item_id}/attachments/{name}")
def get_attachment(item_id: str, name: str,
                   user: str | None = Depends(optional_user)) -> FileResponse:
    """Serve a stored file. Auth re-checked through the owning item on every
    read; content-type comes from our allow-list, never the stored bytes."""
    u = user or ""
    with get_session_factory()() as session:
        item, _ = _item_and_punchlist(session, u, item_id)
        iid = item.id
    try:
        path, ctype = att_svc.resolve(get_settings().data_dir / "uploads", iid, name)
    except att_svc.AttachmentError as exc:
        _raise(exc)
    return FileResponse(path, media_type=ctype, headers={
        "X-Content-Type-Options": "nosniff",
        "Content-Disposition": f'inline; filename="{name}"',
        "Cache-Control": "private, max-age=3600",
    })


@router.get("/notifications")
def my_notifications(user: str | None = Depends(optional_user)) -> dict:
    with get_session_factory()() as session:
        return interaction.inbox(session, user or "")


@router.post("/notifications/read")
def read_notifications(user: str | None = Depends(optional_user),
                       body: dict = Body(default={})) -> dict:
    with get_session_factory()() as session:
        n = interaction.mark_read(session, user or "", body.get("ids"))
        session.commit()
        return {"success": True, "marked": n}


@router.patch("/punchlists/{punchlist_id}")
def update_punchlist(punchlist_id: str, user: str | None = Depends(optional_user),
                     body: dict = Body(...)) -> dict:
    """Archive/reactivate (compose permission). Archived lists drop out of the
    queue and the home default."""
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_COMPOSE), "FORBIDDEN",
             "Managing punchlists needs the compose permission.")
    with get_session_factory()() as session:
        row = _visible_punchlist(session, u, punchlist_id)
        state = body.get("state")
        if state not in ("active", "archived"):
            raise HTTPException(status_code=400,
                                detail={"error_code": "BAD_INPUT",
                                        "summary": "state must be active or archived."})
        row.state = state
        session.commit()
        return {"success": True, "id": row.id, "state": row.state}


# ------------------------------------------------------------------ team queue

@router.get("/team/queue")
def team_queue(user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    _require(_is_team(u), "FORBIDDEN", "The queue is team-only.")
    with get_session_factory()() as session:
        rows = (session.query(Item, Punchlist)
                .join(Punchlist, Punchlist.id == Item.punchlist_id)
                .filter(Item.status.in_(["needs_attention", "waiting_on_team"]),
                        Punchlist.state == "active")
                .order_by(Item.updated_at)
                .all())
        queue = []
        for item, pl in rows:
            rendered = items_svc.render_item(session, item, for_team=True)
            rendered["punchlist"] = {"id": pl.id, "title": pl.title,
                                     "client_label": pl.client_label}
            queue.append(rendered)
        # attention first, then oldest-in-state (query is already oldest-first)
        queue.sort(key=lambda r: (r["status"] != "needs_attention",))
        return {"queue": queue}


# ------------------------------------------------------------------ sets

@router.get("/sets")
def list_sets(user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    _require(_is_team(u) or accounts.app_can(u, accounts.PERM_COMPOSE))
    with get_session_factory()() as session:
        return {"sets": sets_svc.library(session)}


@router.post("/sets", status_code=201)
def publish_set(user: str | None = Depends(optional_user),
                body: dict = Body(...)) -> dict:
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_AUTHOR), "FORBIDDEN",
             "Publishing sets needs the author permission.")
    with get_session_factory()() as session:
        try:
            row = sets_svc.publish(session, u, body.get("set") or {})
        except sets_svc.SetError as exc:
            _raise(exc)
        session.commit()
        return {"success": True, "key": row.key, "version": row.version}


@router.post("/punchlists/{punchlist_id}/set-runs", status_code=201)
def run_set(punchlist_id: str, user: str | None = Depends(optional_user),
            body: dict = Body(...)) -> dict:
    """Fill the shared variables once -> every item in the set (manager hat)."""
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_COMPOSE), "FORBIDDEN",
             "Running a set needs the compose permission.")
    with get_session_factory()() as session:
        _visible_punchlist(session, u, punchlist_id)
        try:
            run, created = sets_svc.instantiate(
                session, u, punchlist_id,
                (body.get("set_key") or ""), body.get("variables") or {})
        except (sets_svc.SetError, items_svc.ItemError) as exc:
            _raise(exc)
        session.commit()
        return {"success": True, "set_run_id": run.id,
                "items": [items_svc.render_item(session, i, for_team=True)
                          for i in created]}


# ------------------------------------------------------------------ workflows

@router.get("/workflows")
def list_workflows(user: str | None = Depends(optional_user)) -> dict:
    u = user or ""
    _require(_is_team(u) or accounts.app_can(u, accounts.PERM_COMPOSE))
    with get_session_factory()() as session:
        return {"workflows": wf_svc.library(session)}


@router.post("/workflows/sync-seeds")
def sync_seed_workflows(user: str | None = Depends(optional_user)) -> dict:
    """Publish a new version of any shipped workflow whose file has moved on.
    Never mutates a published spec; live items stay pinned until they adopt."""
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_AUTHOR), "FORBIDDEN",
             "Publishing workflows needs the author permission.")
    with get_session_factory()() as session:
        try:
            published = wf_svc.sync_seeds(session, u)
        except SpecError as exc:
            _spec_invalid(exc)
        session.commit()
        return {"success": True, "published": published}


@router.post("/workflows/validate")
def validate_workflow(user: str | None = Depends(optional_user),
                      body: dict = Body(...)) -> dict:
    """Dry-run for builders (human or AI): full error list, nothing written."""
    u = user or ""
    _require(_is_team(u) or accounts.app_can(u, accounts.PERM_AUTHOR)
             or accounts.app_can(u, accounts.PERM_COMPOSE))
    try:
        spec = validate_spec(body.get("spec") or {})
    except SpecError as exc:
        return {"ok": False, "errors": exc.errors}
    return {"ok": True, "key": spec.key, "steps": [s.id for s in spec.steps]}


@router.post("/workflows", status_code=201)
def publish_workflow(user: str | None = Depends(optional_user),
                     body: dict = Body(...)) -> dict:
    u = user or ""
    _require(accounts.app_can(u, accounts.PERM_AUTHOR), "FORBIDDEN",
             "Publishing workflows needs the author permission.")
    with get_session_factory()() as session:
        try:
            row = wf_svc.publish(session, u, body.get("spec") or {})
        except SpecError as exc:
            _spec_invalid(exc)
        session.commit()
        return {"success": True, "key": row.key, "version": row.version}


def _report_instances() -> None:
    """Republish the punchlist catalog to central (display-only, best-effort)."""
    from app.config import get_settings

    if not get_settings().has_bw_client:
        return
    try:
        import app.bw_auth as bw_auth

        bw_auth.report_instances(bwa.instances())
    except Exception:  # noqa: BLE001 - visibility must never break a request
        pass
