"""The decision cards, and every comment thread on the site.

Sources of truth: this module, `app/services/discussion.py` (the one write path, the subject
resolver, the permission a subject needs), `app/models/discussion.py`, `docs/CLIENT-SURFACES.md`.

Authorship comes from the session, never the payload: each write stores the EFFECTIVE
account's id in the who-column and its display name as a snapshot (the raw record). The
`item:*` threads belong to the running list on /plan: the reads omit them, and a post, an
edit, a delete or a resolve on one answers 404, unless the caller holds `plan.view` (the same
non-disclosure as the /plan page). A comment is editable by its author under
`client.participate` and by anyone under `discussion.curate`; the same two may remove it
(soft) and resolve or reopen its thread. The legacy routes (`feature-comments`, the card's
`/comments`) still accept the old keys for one release; they write through the same service,
so every comment gets its thread.
"""

from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import RedirectResponse
from sqlalchemy import func, select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.orm import Session, selectinload

from app.db import get_db
from app.models import DiscussionComment, DiscussionItem, FeaturePriority
from app.models.discussion import Thread
from app.models.schemas import (
    CommentIn,
    DiscussionCommentIn,
    DiscussionCommentOut,
    DiscussionItemIn,
    DiscussionItemOut,
    FeatureCommentEdit,
    FeatureCommentIn,
    FeatureCommentOut,
    FeaturePriorityIn,
    FeaturePriorityOut,
    ResolveIn,
    ThreadCommentOut,
    ThreadOut,
)
from app.services import accounts, identity
from app.services import discussion as disc

router = APIRouter(prefix="/api/discussion", tags=["discussion"])
#: The three client pages folded into /discuss's tabs (T8): each old address answers a 301
#: whose Location carries no fragment, so the browser inherits the request's own and a deep
#: link `/todo#t-todo-9#c-88` lands on `/discuss?tab=todo#t-todo-9#c-88`. Included ahead of the
#: shell's catch-all; the policy classes each as a client.view page.
pages_router = APIRouter(include_in_schema=False)
MOVED_PAGES = {"/todo": "/discuss?tab=todo", "/structure": "/discuss?tab=structure", "/quote": "/discuss?tab=quote"}

ITEM_PREFIX = "item:"


def _moved(target: str):
    def page() -> RedirectResponse:
        return RedirectResponse(target, status_code=301, headers={"Cache-Control": "no-store"})
    return page


for _old, _new in MOVED_PAGES.items():
    pages_router.add_api_route(_old, _moved(_new), methods=["GET", "HEAD"])


def _sees_items(request: Request) -> bool:
    return accounts.can(identity.optional_user(request), accounts.PERM_PLAN_VIEW)


def _not_found() -> HTTPException:
    return HTTPException(status_code=404, detail="no such comment")


def _forbidden(summary: str) -> HTTPException:
    return HTTPException(status_code=403, detail={"error_code": "FORBIDDEN", "summary": summary})


def _may_manage(request: Request, author_id: int | None) -> bool:
    """The author under client.participate, anyone under discussion.curate."""
    who = identity.actor(request)
    return author_id == who.id or accounts.can(who.username, accounts.PERM_DISCUSSION_CURATE)


def _visible_comment(db: Session, request: Request, comment_id: int) -> DiscussionComment:
    comment = db.get(DiscussionComment, comment_id)
    if comment is None or comment.deleted_at is not None:
        raise _not_found()
    subject_type, _ = disc.subject_of_legacy(comment.item_id, comment.feature_key)
    if subject_type == "item" and not _sees_items(request):
        raise _not_found()
    return comment


# --- threads (the new shape) ------------------------------------------------------------------

@router.get("/threads", response_model=list[ThreadOut])
def list_threads(request: Request, db: Session = Depends(get_db)) -> list[dict]:
    """Every thread the caller may see, with its live comments, oldest first: the one read a
    page joins its own subject list onto (the rest are starters)."""
    who = identity.actor(request)
    return disc.threads_for(db, who.username, who.id)


@router.post("/comments", response_model=ThreadCommentOut, status_code=201)
def add_comment(payload: CommentIn, request: Request, db: Session = Depends(get_db)) -> dict:
    if not disc.valid_subject(payload.subject_type, payload.subject_id):
        raise HTTPException(status_code=422, detail={"error_code": "UNKNOWN_SUBJECT"})
    if payload.subject_type == "item" and not _sees_items(request):
        raise _not_found()
    who = identity.actor(request)
    comment = disc.add_comment(db, subject_type=payload.subject_type, subject_id=payload.subject_id,
                               actor=who, body=payload.body, label=payload.label)
    db.commit()
    db.refresh(comment)
    return {**disc.comment_out(comment, who.id), "thread_id": comment.thread_id}


@router.post("/comments/{comment_id}/edit", response_model=ThreadCommentOut)
def edit_comment(comment_id: int, payload: FeatureCommentEdit, request: Request, db: Session = Depends(get_db)) -> dict:
    """Rewrite the words of any thread's comment (a card's included): the author or a curator."""
    comment = _visible_comment(db, request, comment_id)
    if not _may_manage(request, comment.author_id):
        raise _forbidden("You can edit your own comments only.")
    disc.edit_comment(db, comment, body=payload.body)
    db.commit()
    db.refresh(comment)
    return {**disc.comment_out(comment, identity.actor(request).id), "thread_id": comment.thread_id}


@router.post("/comments/{comment_id}/delete", status_code=200)
def remove_comment(comment_id: int, request: Request, db: Session = Depends(get_db)) -> dict:
    """Soft: the row stays, the reader stops seeing it. The author or a curator."""
    comment = _visible_comment(db, request, comment_id)
    if not _may_manage(request, comment.author_id):
        raise _forbidden("You can remove your own comments only.")
    disc.delete_comment(db, comment)
    db.commit()
    return {"ok": True, "id": comment_id}


@router.post("/threads/{thread_id}/resolve", response_model=ThreadOut)
def resolve_thread(thread_id: int, payload: ResolveIn, request: Request, db: Session = Depends(get_db)) -> dict:
    """Resolve or reopen a conversation: its opener or a curator."""
    thread = db.get(Thread, thread_id)
    if thread is None or (thread.subject_type == "item" and not _sees_items(request)):
        raise HTTPException(status_code=404, detail="no such thread")
    comments = disc.live_comments(db, thread.id)
    opener = comments[0].author_id if comments else None
    if not _may_manage(request, opener):
        raise _forbidden("Only the person who opened this conversation can resolve it.")
    disc.resolve_thread(db, thread, actor=identity.actor(request), resolved=payload.resolved)
    db.commit()
    out = disc.thread_out(db, thread)
    assert out is not None
    return out


# --- the legacy comment routes (one release) ------------------------------------------------

@router.get("/feature-comments", response_model=list[FeatureCommentOut])
def list_feature_comments(request: Request, db: Session = Depends(get_db)) -> list[DiscussionComment]:
    """Every live comment left on a keyed thread, oldest first; the running list's `item:*`
    threads only for a caller who may see /plan."""
    query = select(DiscussionComment).where(
        DiscussionComment.feature_key.isnot(None), DiscussionComment.deleted_at.is_(None))
    if not _sees_items(request):
        query = query.where(DiscussionComment.feature_key.notlike(ITEM_PREFIX + "%"))
    return list(db.scalars(query.order_by(DiscussionComment.created_at, DiscussionComment.id)))


@router.post("/feature-comments", response_model=FeatureCommentOut, status_code=201)
def add_feature_comment(
    payload: FeatureCommentIn, request: Request, db: Session = Depends(get_db)
) -> DiscussionComment:
    if payload.feature_key.startswith(ITEM_PREFIX) and not _sees_items(request):
        raise _not_found()
    subject_type, subject_id = disc.subject_of_legacy(None, payload.feature_key)
    comment = disc.add_comment(db, subject_type=subject_type, subject_id=subject_id,
                               actor=identity.actor(request), body=payload.body)
    db.commit()
    db.refresh(comment)
    return comment


@router.post("/feature-comments/{comment_id}", response_model=FeatureCommentOut)
def edit_feature_comment(
    comment_id: int, payload: FeatureCommentEdit, request: Request, db: Session = Depends(get_db)
) -> DiscussionComment:
    """Rewrite the body of a comment: one's own under client.participate, anyone's under
    discussion.curate. The author is never editable and `edited_at` is stamped, so an edited
    comment says so."""
    comment = _visible_comment(db, request, comment_id)
    if comment.feature_key is None:
        raise _not_found()  # a card's comment is not a keyed thread's; the legacy route keeps its shape
    if not _may_manage(request, comment.author_id):
        raise _forbidden("You can edit your own comments only.")
    disc.edit_comment(db, comment, body=payload.body)
    db.commit()
    db.refresh(comment)
    return comment


# --- the feature ranking ----------------------------------------------------------------------

@router.get("/feature-priorities", response_model=list[FeaturePriorityOut])
def list_feature_priorities(db: Session = Depends(get_db)) -> list[FeaturePriority]:
    """The client's current demo-scope ranking, one row per feature."""
    return list(db.scalars(select(FeaturePriority).order_by(FeaturePriority.feature_key)))


@router.post("/feature-priorities", response_model=FeaturePriorityOut)
def set_feature_priority(
    payload: FeaturePriorityIn, request: Request, db: Session = Depends(get_db)
) -> FeaturePriority:
    """Upsert: the latest choice per feature wins; the client can change his mind.

    ON CONFLICT so two simultaneous first choices for one feature (a fast
    double-tap, or two viewers) cannot race a SELECT-then-INSERT into a 500.
    """
    who = identity.actor(request)
    stmt = (
        pg_insert(FeaturePriority)
        .values(
            feature_key=payload.feature_key,
            priority=payload.priority,
            author=who.display_name,
            author_id=who.id,
        )
        .on_conflict_do_update(
            index_elements=[FeaturePriority.feature_key],
            set_={"priority": payload.priority, "author": who.display_name, "author_id": who.id,
                  "updated_at": func.now()},
        )
    )
    db.execute(stmt)
    db.commit()
    row = db.scalar(
        select(FeaturePriority).where(FeaturePriority.feature_key == payload.feature_key)
    )
    assert row is not None
    return row


# --- the decision cards -----------------------------------------------------------------------

@router.get("", response_model=list[DiscussionItemOut])
def list_items(db: Session = Depends(get_db)) -> list[DiscussionItem]:
    items = list(
        db.scalars(
            select(DiscussionItem)
            .options(selectinload(DiscussionItem.comments))
            .order_by(
                DiscussionItem.resolved, DiscussionItem.sort_order, DiscussionItem.id
            )
        )
    )
    for item in items:
        item.comments = [c for c in item.comments if c.deleted_at is None]
    return items


@router.post("", response_model=DiscussionItemOut, status_code=201)
def add_item(payload: DiscussionItemIn, db: Session = Depends(get_db)) -> DiscussionItem:
    title = payload.title.strip()
    if not title:
        raise HTTPException(status_code=422, detail={"error_code": "TITLE_REQUIRED"})
    item = DiscussionItem(title=title, note=(payload.note or "").strip() or None)
    db.add(item)
    db.commit()
    return item


@router.post("/{item_id}", response_model=DiscussionItemOut)
def update_item(
    item_id: int, payload: DiscussionItemIn, db: Session = Depends(get_db)
) -> DiscussionItem:
    """Reword an item. The list must track reality, and reality moves."""
    item = db.get(DiscussionItem, item_id)
    if item is None:
        raise HTTPException(status_code=404, detail={"error_code": "ITEM_NOT_FOUND"})
    title = payload.title.strip()
    if not title:
        raise HTTPException(status_code=422, detail={"error_code": "TITLE_REQUIRED"})
    item.title = title
    item.note = (payload.note or "").strip() or None
    if payload.theme is not None:
        item.theme = payload.theme.strip() or None
    if payload.needs_decision is not None:
        item.needs_decision = payload.needs_decision
    if payload.sort_order is not None:
        item.sort_order = payload.sort_order
    db.commit()
    return item


@router.post("/{item_id}/resolved", response_model=DiscussionItemOut)
def set_resolved(item_id: int, resolved: bool, request: Request, db: Session = Depends(get_db)) -> DiscussionItem:
    """Resolving a card is a DECISION: the people in its thread hear it as news."""
    item = db.get(DiscussionItem, item_id)
    if item is None:
        raise HTTPException(status_code=404, detail={"error_code": "ITEM_NOT_FOUND"})
    was = item.resolved
    item.resolved = resolved
    if resolved and not was:
        disc.emit_decision(db, subject_type="decision", subject_id=str(item.id), actor=identity.actor(request),
                           body=f"Resolved: {item.title}", label=item.title)
    db.commit()
    return item


@router.post("/{item_id}/comments", response_model=DiscussionCommentOut, status_code=201)
def add_item_comment(
    item_id: int, payload: DiscussionCommentIn, request: Request, db: Session = Depends(get_db)
) -> DiscussionComment:
    """The client's side of the conversation on a card, under the signed-in account's name."""
    item = db.get(DiscussionItem, item_id)
    if item is None:
        raise HTTPException(status_code=404, detail={"error_code": "ITEM_NOT_FOUND"})
    body = payload.body.strip()[:2000]
    if not body:
        raise HTTPException(status_code=422, detail={"error_code": "BODY_REQUIRED"})
    comment = disc.add_comment(db, subject_type="decision", subject_id=str(item.id),
                               actor=identity.actor(request), body=body, label=item.title)
    db.commit()
    db.refresh(comment)
    return comment
