"""The board's journey: the project's stages (caddie's shape, held here until
the hub owns them), the client's "Feedback complete", the conversation about a
concept as a whole, and concept thumbnails."""

import logging

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

from app import accounts, schemas
from app.config import get_settings
from app.db import get_session_factory
from sqlalchemy import select

from app.models import Pin, ProjectStage, Thread, WalkthroughStep
from app.routers.pins import _pin_out
from app.routers.projects import (
    _app_url, _not_found, _require_manage, _require_view, _user,
)
from app.services import attachments as att_svc
from app.services import interaction, outbound
from app.services import projects as svc
from app.services import stages as stage_svc

log = logging.getLogger(__name__)
router = APIRouter(tags=["board"])


def _option_ctx(db, option_id: int, user: str):
    try:
        option, project_id = svc.option_project(db, option_id)
    except svc.DomainError as exc:
        raise HTTPException(status_code=exc.status, detail={
            "error_code": exc.code, "summary": str(exc)}) from exc
    # Hiding an option in the project view is not merely cosmetic: a client
    # cannot reach a fileless option by id either.
    if not svc.option_visible_to(db, user, option):
        raise HTTPException(status_code=404, detail={
            "error_code": "NO_SUCH_OPTION", "summary": "No such option."})
    return option, project_id


def _comment_out(c, project_id: str) -> schemas.CommentOut:
    return schemas.CommentOut(
        id=c.id, body_md="" if c.deleted_at else c.body_md,
        author_username=c.author_username,
        author_side=svc.side_of(c.author_username, project_id) or "client",
        created_at=c.created_at, edited_at=c.edited_at,
        deleted=c.deleted_at is not None,
        attachment_ids=[a.id for a in c.attachments],
            reactions=interaction.reactions_of(c))


# ------------------------------------------------------------------ stages

@router.get("/api/projects/{project_id}/stages", response_model=schemas.StagesOut)
def list_stages(project_id: str, user: str = Depends(_user)) -> schemas.StagesOut:
    """The roadmap as this side sees it, seeded from the template on first
    read. The same list rides on the project detail; this is for the board's
    stage editor and for tests."""
    _require_view(user, project_id)
    side = svc.side_of(user, project_id)
    sf = get_session_factory()
    with sf() as db:
        rows = stage_svc.visible(stage_svc.stages_of(db, project_id), side)
        out = [schemas.StageOut(**stage_svc.stage_out(s)) for s in rows]
        db.commit()
    return schemas.StagesOut(stages=out)


@router.patch("/api/projects/{project_id}/stages/{stage_id}",
              response_model=schemas.StageOut)
def update_stage(project_id: str, stage_id: int, body: schemas.UpdateStageIn,
                 user: str = Depends(_user)) -> schemas.StageOut:
    """A manager writes what a stage holds and moves it along. The timestamps
    follow the status (services/stages.set_status), so the timeline can say
    when without anyone typing a date."""
    _require_manage(user, project_id)
    sf = get_session_factory()
    with sf() as db:
        stage = db.get(ProjectStage, stage_id)
        if stage is None or stage.instance_id != project_id:
            raise _not_found()
        if body.title is not None:
            stage.title = body.title.strip()
        if body.body_md is not None:
            stage.body_md = body.body_md.strip()
        if body.client_visible is not None:
            stage.client_visible = body.client_visible
        if body.status is not None and body.status != stage.status:
            stage_svc.set_status(stage, body.status)
        svc.record(db, project_id, user, "stage_updated",
                   {"stage_id": stage.id, "key": stage.key, "status": stage.status})
        out = schemas.StageOut(**stage_svc.stage_out(stage))
        db.commit()
    return out


# --------------------------------------------------------- feedback complete

@router.post("/api/projects/{project_id}/feedback-complete", response_model=schemas.OkOut)
def feedback_complete(project_id: str, user: str = Depends(_user)) -> schemas.OkOut:
    """The client says "I have looked, and I have said what I have to say" —
    the round is theirs to close. It marks the project as the team's turn,
    tells every manager (bell + mail), and stands until the next present."""
    _require_view(user, project_id)
    inst = accounts.instance_obj(project_id)
    sf = get_session_factory()
    with sf() as db:
        details = svc.ensure_details(db, project_id)
        details.feedback_completed_at = svc.utcnow()
        details.feedback_completed_by = user
        db.flush()
        stamp = details.feedback_completed_at.isoformat()
        svc.record(db, project_id, user, "feedback_complete", None)
        interaction.notify_project_side(
            db, project_id=project_id, actor=user, side="manager", kind="turn",
            category="review", context_label=inst["label"],
            body=f"{user} has finished their feedback on the concepts.",
            url=f"/p/{project_id}", source_type="project", source_id=project_id,
            dedupe_prefix=f"feedback-complete:{project_id}:{stamp}")
        # Moment two of two (the first is "presented"): the team may not be
        # looking at easel when the client finishes, and this is the message
        # they are waiting for.
        for who in svc.manager_side(project_id):
            if who == user:
                continue
            outbound.queue(
                db, recipient=who, kind="feedback_complete", project_id=project_id,
                subject=f"{user} has finished their feedback on {inst['label']}",
                url=f"{_app_url()}/p/{project_id}",
                body=(f"{user} pressed Feedback complete on {inst['label']}.\n\n"
                      "Their notes, preference and approvals are on the board."))
        db.commit()
    return schemas.OkOut()


# ------------------------------------------- conversation about a concept

@router.get("/api/options/{option_id}/thread", response_model=schemas.OptionThreadOut)
def get_option_thread(option_id: int,
                      user: str = Depends(_user)) -> schemas.OptionThreadOut:
    """The conversation about a concept as a whole — the general remark that
    has no spot on the design. One thread per concept, like a screen's."""
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        thread = svc.option_thread(db, option)
        if thread is None:
            return schemas.OptionThreadOut()
        return schemas.OptionThreadOut(
            thread_id=thread.id, resolved=thread.resolved,
            comments=[_comment_out(c, project_id) for c in thread.comments])


@router.post("/api/options/{option_id}/comments", response_model=schemas.CreatedOut)
def comment_on_option(option_id: int, body: schemas.AddCommentIn,
                      user: str = Depends(_user)) -> schemas.CreatedOut:
    """Open or continue the concept's conversation; the thread is created on
    the first comment. Counts in the rollup like a screen's does."""
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        inst = accounts.instance_obj(project_id)
        thread = svc.ensure_option_thread(db, option, project_id, user)
        interaction.add_comment(
            db, thread, user, body.body_md,
            url=f"/p/{project_id}",
            context_label=f"{inst['label']} · {option.title}")
        svc.record(db, project_id, user, "option_comment",
                   {"option_id": option.id, "thread_id": thread.id})
        db.commit()
        return schemas.CreatedOut(id=thread.id)


# ------------------------------------------------------------- thumbnails

@router.post("/api/options/{option_id}/thumbnail", response_model=schemas.ThumbnailOut)
def upload_thumbnail(option_id: int, file: UploadFile = File(...),
                     user: str = Depends(_user)) -> schemas.ThumbnailOut:
    """A picture of the concept for the board (manager). The attachments
    rules apply — allow-list, magic bytes, our filename, caps — and it must
    be an image: a PDF is not a picture of anything."""
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        _require_manage(user, project_id)
        data = file.file.read(att_svc.MAX_BYTES + 1)
        upload_dir = get_settings().data_dir / "uploads"
        subject = f"option-{option.id}"
        try:
            saved = att_svc.save(upload_dir, subject,
                                 f"/api/options/{option.id}/thumbnail",
                                 file.filename or "", data)
        except att_svc.AttachmentError as exc:
            raise HTTPException(
                status_code=413 if exc.code == "TOO_LARGE" else 400,
                detail={"error_code": exc.code, "summary": str(exc)}) from exc
        if not saved["is_image"]:
            (upload_dir / subject / saved["name"]).unlink(missing_ok=True)
            raise HTTPException(status_code=400, detail={
                "error_code": "NOT_AN_IMAGE",
                "summary": "A thumbnail has to be an image (png, jpg, gif, webp)."})
        if option.thumbnail_name:
            (upload_dir / subject / option.thumbnail_name).unlink(missing_ok=True)
        option.thumbnail_name = saved["name"]
        db.commit()
        return schemas.ThumbnailOut(
            thumbnail_url=f"/api/options/{option_id}/thumbnail?v={saved['name'][:8]}")


@router.get("/api/options/{option_id}/thumbnail")
def get_thumbnail(option_id: int, user: str = Depends(_user)) -> FileResponse:
    sf = get_session_factory()
    with sf() as db:
        option, _project_id = _option_ctx(db, option_id, user)
        name = option.thumbnail_name
    if not name:
        raise _not_found()
    try:
        path, ctype = att_svc.resolve(get_settings().data_dir / "uploads",
                                      f"option-{option_id}", name)
    except att_svc.AttachmentError as exc:
        raise _not_found() from exc
    return FileResponse(path, media_type=ctype, headers={
        "X-Content-Type-Options": "nosniff",
        "Cache-Control": "private, max-age=3600",
    })


# ------------------------------------------------------------- discussion

def _subject_out(thread, project_id: str) -> schemas.SubjectThreadOut:
    if thread is None:
        return schemas.SubjectThreadOut()
    return schemas.SubjectThreadOut(
        thread_id=thread.id, resolved=thread.resolved,
        comments=[_comment_out(c, project_id) for c in thread.comments])


@router.get("/api/options/{option_id}/discussion", response_model=schemas.DiscussionOut)
def get_discussion(option_id: int, user: str = Depends(_user)) -> schemas.DiscussionOut:
    """Everything there is to talk about on this design, in one read: the
    notes dropped on it, the page as a whole, this version as a whole, and
    every walkthrough point. One mode in the viewer ("Discussion") lists them
    together; the writes stay on their own subject routes."""
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        rows = db.execute(
            select(Pin, Thread).join(Thread, Pin.thread_id == Thread.id)
            .where(Pin.option_id == option_id).order_by(Pin.id)).all()
        steps = list(db.scalars(
            select(WalkthroughStep).where(WalkthroughStep.option_id == option_id)
            .order_by(WalkthroughStep.step_order, WalkthroughStep.id)))
        threads = {int(t.subject_id): t for t in db.scalars(select(Thread).where(
            Thread.subject_type == "step",
            Thread.subject_id.in_([str(s.id) for s in steps])))} if steps else {}
        return schemas.DiscussionOut(
            pins=[_pin_out(db, pin, thread, i + 1, project_id)
                  for i, (pin, thread) in enumerate(rows)],
            page=_subject_out(svc.screen_thread(db, option.screen), project_id),
            version=_subject_out(svc.option_thread(db, option), project_id),
            points=[schemas.PointThreadOut(
                step_id=st.id, step_order=st.step_order, title=st.title,
                body_md=st.body_md or "",
                thread=_subject_out(threads.get(st.id), project_id))
                for st in steps])


# ------------------------------------------------- the introduction's words

@router.patch("/api/projects/{project_id}/tour/{key}", response_model=schemas.TourOverridesOut)
def update_tour_point(project_id: str, key: str, body: schemas.TourPointIn,
                      user: str = Depends(_user)) -> schemas.TourOverridesOut:
    """A manager rewrites one point of the tool introduction for THIS project
    (the shipped words are the default; "{name}" still becomes the reader's
    first name). Empty strings clear a field; both empty drops the override."""
    _require_manage(user, project_id)
    if not key.replace("-", "").replace("_", "").isalnum() or len(key) > 40:
        raise _not_found()
    sf = get_session_factory()
    with sf() as db:
        details = svc.ensure_details(db, project_id)
        overrides = dict(details.tour_overrides or {})
        current = dict(overrides.get(key, {}))
        if body.title is not None:
            current["title"] = body.title.strip()
        if body.body is not None:
            current["body"] = body.body.strip()
        current = {k: v for k, v in current.items() if v}
        if current:
            overrides[key] = current
        else:
            overrides.pop(key, None)
        details.tour_overrides = overrides or None
        svc.record(db, project_id, user, "tour_point_edited", {"key": key})
        db.commit()
        return schemas.TourOverridesOut(tour_overrides=overrides)
