"""The client's to-do page: items, completion, and file hand-in.

Sources of truth: app/models/client.py, app/services/uploads.py. Enabled by
settings.feature_client_todos; production keeps it OFF until the app-owned login exists,
because the only protection on the demo host is the id-auth gate in front of it (build
plan §10 #4). The download route serves from the upload root with a pinned content type
and nosniff, never from a static mount.
"""

from datetime import UTC, datetime

from fastapi import APIRouter, Depends, File, Form, HTTPException, Request, UploadFile
from fastapi.responses import FileResponse
from pydantic import BaseModel
from sqlalchemy import select
from sqlalchemy.orm import Session, selectinload

from app.db import get_db
from app.models.client import ClientTodo, ClientUpload
from app.services import identity
from app.services import uploads as up

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

OWNERS = ("adam", "mark", "both")


class UploadOut(BaseModel):
    id: int
    original_name: str
    content_type: str
    kind: str
    bytes: int
    uploaded_by: str
    note: str | None
    created_at: datetime


class TodoOut(BaseModel):
    id: int
    owner: str
    kind: str
    accepts_files: bool
    title: str
    detail: str | None
    due: str | None
    status: str
    completed_by: str | None
    completed_at: datetime | None
    uploads: list[UploadOut]


def _out(t: ClientTodo) -> TodoOut:
    return TodoOut(
        id=t.id, owner=t.owner, kind=t.kind, accepts_files=t.accepts_files, title=t.title, detail=t.detail, due=t.due, status=t.status,
        completed_by=t.completed_by, completed_at=t.completed_at,
        uploads=[UploadOut(id=u.id, original_name=u.original_name, content_type=u.content_type, kind=u.kind,
                           bytes=u.bytes, uploaded_by=u.uploaded_by, note=u.note, created_at=u.created_at)
                 for u in t.uploads],
    )


def _get(db: Session, todo_id: int) -> ClientTodo:
    todo = db.scalar(select(ClientTodo).options(selectinload(ClientTodo.uploads)).where(ClientTodo.id == todo_id))
    if todo is None:
        raise HTTPException(status_code=404, detail="no such item")
    return todo


@router.get("", response_model=list[TodoOut])
def list_todos(db: Session = Depends(get_db)) -> list[TodoOut]:
    rows = db.scalars(
        select(ClientTodo).options(selectinload(ClientTodo.uploads)).order_by(ClientTodo.sort, ClientTodo.id)
    ).all()
    return [_out(t) for t in rows]


@router.post("/{todo_id}/complete", response_model=TodoOut)
def complete(todo_id: int, request: Request, db: Session = Depends(get_db)) -> TodoOut:
    who = identity.actor(request)
    todo = _get(db, todo_id)
    todo.status = "done"
    todo.completed_by = who.display_name
    todo.completed_by_id = who.id
    todo.completed_at = datetime.now(UTC)
    db.commit()
    return _out(_get(db, todo_id))


@router.post("/{todo_id}/reopen", response_model=TodoOut)
def reopen(todo_id: int, db: Session = Depends(get_db)) -> TodoOut:
    todo = _get(db, todo_id)
    todo.status = "open"
    todo.completed_by = None
    todo.completed_by_id = None
    todo.completed_at = None
    db.commit()
    return _out(_get(db, todo_id))


@router.post("/{todo_id}/uploads", response_model=TodoOut, status_code=201)
async def add_upload(
    todo_id: int,
    request: Request,
    note: str | None = Form(default=None, max_length=1000),
    file: UploadFile = File(...),
    db: Session = Depends(get_db),
) -> TodoOut:
    who = identity.actor(request)
    todo = _get(db, todo_id)
    if not todo.accepts_files:
        raise HTTPException(status_code=409, detail="This item takes comments, not files.")
    data = await file.read(up.MAX_BYTES + 1)
    if len(data) > up.MAX_BYTES:
        raise HTTPException(status_code=413, detail="That file is over 25 MB. Please send a smaller version.")
    sniffed = up.sniff(data, file.filename or "")
    if sniffed is None:
        raise HTTPException(
            status_code=415,
            detail="Only images (JPEG, PNG, WebP, GIF), PDF, Word (.docx), Markdown and plain text are accepted.",
        )
    if sniffed.kind == "image":
        data = up.strip_image_metadata(data, sniffed.content_type)
    stored = up.stored_name_for(sniffed.ext)
    up.store(todo.id, stored, data)
    db.add(ClientUpload(
        todo_id=todo.id, original_name=up.safe_original_name(file.filename or ""), stored_name=stored,
        content_type=sniffed.content_type, kind=sniffed.kind, bytes=len(data),
        uploaded_by=who.display_name, uploaded_by_id=who.id, note=(note or "").strip() or None,
    ))
    db.commit()
    return _out(_get(db, todo_id))


@router.get("/uploads/{upload_id}")
def download(upload_id: int, db: Session = Depends(get_db)) -> FileResponse:
    u = db.get(ClientUpload, upload_id)
    if u is None:
        raise HTTPException(status_code=404, detail="no such file")
    path = up.UPLOAD_ROOT / str(u.todo_id) / u.stored_name
    if not path.is_file():
        raise HTTPException(status_code=404, detail="file missing on disk")
    return FileResponse(
        path, media_type=u.content_type, filename=u.original_name,
        headers={"X-Content-Type-Options": "nosniff", "Cache-Control": "private, no-store"},
    )
