"""Guided walkthrough: presentation beats an option's author scripts, replayed
to the client with per-step approvals. Approvals from the client side are what
the status rollup counts (a manager approving their own presentation is noise)."""

import logging

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import func, select

from app import accounts, schemas
from app.db import get_session_factory
from app.identity import optional_user
from app.models import StepApproval, Thread, WalkthroughStep
from app.services import interaction, walkthrough_import
from app.services import projects as svc

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


def _user(user: str | None = Depends(optional_user)) -> str:
    assert user is not None
    return user


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
    # Not merely "can you see the project" — an option with no mockup is
    # invisible to the client in the project view and must 404 by id too.
    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 _step_ctx(db, step_id: int, user: str):
    step = db.get(WalkthroughStep, step_id)
    if step is None:
        raise HTTPException(status_code=404, detail={
            "error_code": "NO_SUCH_STEP", "summary": "No such step."})
    option, project_id = _option_ctx(db, step.option_id, user)
    return step, option, project_id


@router.get("/api/options/{option_id}/walkthrough",
            response_model=schemas.WalkthroughOut)
def get_walkthrough(option_id: int, user: str = Depends(_user)) -> schemas.WalkthroughOut:
    sf = get_session_factory()
    with sf() as db:
        _option, project_id = _option_ctx(db, option_id, user)
        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 {}
        out = []
        for s in steps:
            approvals = db.scalars(select(StepApproval).where(
                StepApproval.step_id == s.id,
                StepApproval.status == "approved")).all()
            thread = threads.get(s.id)
            live = svc.comment_count(thread)
            out.append(schemas.StepOut(
                id=s.id, step_order=s.step_order, title=s.title,
                body_md=s.body_md, target_selector=s.target_selector,
                rect=s.rect,
                requires_approval=s.requires_approval,
                click_selector=s.click_selector,
                click_delay_seconds=s.click_delay_seconds,
                click_dismiss_seconds=s.click_dismiss_seconds,
                modal=s.modal,
                approved_by_me=any(a.username == user for a in approvals),
                client_approved=any(
                    svc.side_of(a.username, project_id) != "manager"
                    for a in approvals),
                comment_count=live,
                thread_open=thread is not None and live > 0 and not thread.resolved))
        return schemas.WalkthroughOut(steps=out)


@router.post("/api/options/{option_id}/walkthrough",
             response_model=schemas.CreatedOut)
def create_step(option_id: int, body: schemas.CreateStepIn,
                user: str = Depends(_user)) -> schemas.CreatedOut:
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        if svc.side_of(user, project_id) != "manager":
            raise HTTPException(status_code=403, detail={
                "error_code": "FORBIDDEN",
                "summary": "Authoring the walkthrough needs the manager side."})
        if body.step_order is None:
            from sqlalchemy import func
            body.step_order = (db.scalar(
                select(func.max(WalkthroughStep.step_order)).where(
                    WalkthroughStep.option_id == option_id)) or 0) + 1
        step = WalkthroughStep(
            option_id=option.id, step_order=body.step_order,
            title=body.title.strip(), body_md=body.body_md,
            target_selector=body.target_selector.strip(),
            rect=body.rect, requires_approval=body.requires_approval,
            click_selector=body.click_selector.strip(),
            click_delay_seconds=body.click_delay_seconds,
            click_dismiss_seconds=body.click_dismiss_seconds,
            modal=body.modal,
            created_by=user)
        db.add(step)
        db.flush()
        svc.record(db, project_id, user, "step_added",
                   {"option_id": option.id, "step_id": step.id})
        db.commit()
        return schemas.CreatedOut(id=step.id)


@router.post("/api/options/{option_id}/walkthrough/reorder",
             response_model=schemas.OkOut)
def reorder_steps(option_id: int, body: schemas.ReorderStepsIn,
                  user: str = Depends(_user)) -> schemas.OkOut:
    """Renumber the walkthrough in one transaction.

    Points were append-only: a new one always landed last, and nothing could
    move it. Inserting "and here is the menu opening" as the second beat meant
    it read as the last, after the closing remarks.
    """
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        if svc.side_of(user, project_id) != "manager":
            raise HTTPException(status_code=403, detail={
                "error_code": "FORBIDDEN",
                "summary": "Authoring the walkthrough needs the manager side."})
        steps = list(db.scalars(select(WalkthroughStep).where(
            WalkthroughStep.option_id == option.id)))
        by_id = {s.id: s for s in steps}
        unknown = [i for i in body.step_ids if i not in by_id]
        if unknown:
            raise HTTPException(status_code=400, detail={
                "error_code": "NO_SUCH_STEP",
                "summary": "That order names a point which is not in this "
                           "walkthrough.",
                "details": f"Unknown ids: {unknown}. Reload and try again; "
                           "someone may have deleted a point."})
        order = 0
        for step_id in body.step_ids:
            order += 1
            by_id[step_id].step_order = order
        # Anything the caller did not mention keeps its relative place, after
        # the points that were named. Silently dropping it would lose a beat.
        for step in sorted(steps, key=lambda s: (s.step_order, s.id)):
            if step.id not in set(body.step_ids):
                order += 1
                step.step_order = order
        svc.record(db, project_id, user, "walkthrough_reordered",
                   {"option_id": option.id, "count": len(body.step_ids)})
        db.commit()
    return schemas.OkOut()


@router.post("/api/options/{option_id}/walkthrough/import",
             response_model=schemas.ImportStepsOut)
def import_steps(option_id: int, body: schemas.ImportStepsIn,
                 user: str = Depends(_user)) -> schemas.ImportStepsOut:
    """Paste written notes, get beats.

    The walkthrough is written as prose before easel ever sees it; typing it back
    in one drag at a time is retyping a document that already exists. Format and
    reasoning live in services/walkthrough_import.py.
    """
    sf = get_session_factory()
    with sf() as db:
        option, project_id = _option_ctx(db, option_id, user)
        if svc.side_of(user, project_id) != "manager":
            raise HTTPException(status_code=403, detail={
                "error_code": "FORBIDDEN",
                "summary": "Authoring the walkthrough needs the manager side."})
        try:
            beats = walkthrough_import.parse(body.text)
        except walkthrough_import.ImportError_ as exc:
            raise HTTPException(status_code=400, detail={
                "error_code": "BAD_IMPORT", "summary": str(exc)}) from exc

        replaced = 0
        if body.replace:
            existing = list(db.scalars(select(WalkthroughStep).where(
                WalkthroughStep.option_id == option.id)))
            if existing:
                # Approvals belong to the steps they were given on; a replaced
                # beat is not the beat the client approved.
                for approval in db.scalars(select(StepApproval).where(
                        StepApproval.step_id.in_([e.id for e in existing]))):
                    db.delete(approval)
                for step in existing:
                    db.delete(step)
                replaced = len(existing)
            db.flush()

        start = 0 if body.replace else int(db.scalar(
            select(func.max(WalkthroughStep.step_order))
            .where(WalkthroughStep.option_id == option.id)) or 0)
        for i, beat in enumerate(beats, start=1):
            db.add(WalkthroughStep(option_id=option.id, step_order=start + i,
                                   created_by=user, **beat))
        svc.record(db, project_id, user, "walkthrough_imported",
                   {"option_id": option.id, "created": len(beats),
                    "replaced": replaced})
        db.commit()
        return schemas.ImportStepsOut(created=len(beats), replaced=replaced)


@router.patch("/api/walkthrough/{step_id}", response_model=schemas.OkOut)
def update_step(step_id: int, body: schemas.UpdateStepIn,
                user: str = Depends(_user)) -> schemas.OkOut:
    sf = get_session_factory()
    with sf() as db:
        step, _option, project_id = _step_ctx(db, step_id, user)
        if svc.side_of(user, project_id) != "manager":
            raise HTTPException(status_code=403, detail={
                "error_code": "FORBIDDEN", "summary": "Manager side only."})
        for field in ("title", "body_md", "target_selector", "rect",
                      "step_order", "requires_approval", "click_selector",
                      "click_delay_seconds", "click_dismiss_seconds", "modal"):
            value = getattr(body, field)
            if value is not None:
                setattr(step, field,
                        value.strip()
                        if field in ("title", "target_selector", "click_selector")
                        else value)
        db.commit()
    return schemas.OkOut()


@router.delete("/api/walkthrough/{step_id}", response_model=schemas.OkOut)
def delete_step(step_id: int, user: str = Depends(_user)) -> schemas.OkOut:
    sf = get_session_factory()
    with sf() as db:
        step, _option, project_id = _step_ctx(db, step_id, user)
        if svc.side_of(user, project_id) != "manager":
            raise HTTPException(status_code=403, detail={
                "error_code": "FORBIDDEN", "summary": "Manager side only."})
        db.delete(step)
        db.commit()
    return schemas.OkOut()


@router.post("/api/walkthrough/{step_id}/approve", response_model=schemas.OkOut)
def approve_step(step_id: int, body: schemas.ApproveStepIn,
                 user: str = Depends(_user)) -> schemas.OkOut:
    sf = get_session_factory()
    with sf() as db:
        step, option, project_id = _step_ctx(db, step_id, user)
        existing = db.scalar(select(StepApproval).where(
            StepApproval.step_id == step.id, StepApproval.username == user))
        if existing:
            existing.status = "approved"
            existing.comment = body.comment
        else:
            db.add(StepApproval(step_id=step.id, username=user,
                                status="approved", comment=body.comment))
        db.flush()
        svc.record(db, project_id, user, "step_approved",
                   {"step_id": step.id, "option_id": option.id})
        if svc.side_of(user, project_id) == "client":
            inst = accounts.instance_obj(project_id)
            interaction.notify_project_side(
                db, project_id=project_id, actor=user, side="manager",
                kind="status", category="walkthrough",
                context_label=f"{inst['label']} · {option.title}",
                body=f"Approved: {step.title}",
                url=f"/p/{project_id}/o/{option.id}",
                source_type="step", source_id=str(step.id),
                dedupe_prefix=f"approve:{step.id}:{user}")
        db.commit()
    return schemas.OkOut()


# ------------------------------------------------- conversation on a beat

@router.get("/api/steps/{step_id}/thread", response_model=schemas.StepThreadOut)
def get_step_thread(step_id: int, user: str = Depends(_user)) -> schemas.StepThreadOut:
    """The conversation about one point of the walkthrough.

    A beat is a subject the way a screen is (one thread per subject), so the
    question about a point is asked inside the tour where the point is shown.
    Before this, asking meant pausing the tour and dropping a pin near the
    spot — a spatial note for a question that was about the words.
    """
    sf = get_session_factory()
    with sf() as db:
        step, _option, project_id = _step_ctx(db, step_id, user)
        thread = svc.step_thread(db, step)
        if thread is None:
            return schemas.StepThreadOut()
        return schemas.StepThreadOut(
            thread_id=thread.id, resolved=thread.resolved,
            comments=[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))
                for c in thread.comments])


@router.post("/api/steps/{step_id}/comments", response_model=schemas.CreatedOut)
def comment_on_step(step_id: int, body: schemas.AddCommentIn,
                    user: str = Depends(_user)) -> schemas.CreatedOut:
    """Open or continue the beat's conversation. The thread is created on the
    first comment, so a point nobody has spoken about carries no empty thread.
    The notification links straight back into the tour at this point."""
    sf = get_session_factory()
    with sf() as db:
        step, option, project_id = _step_ctx(db, step_id, user)
        inst = accounts.instance_obj(project_id)
        order = list(db.scalars(
            select(WalkthroughStep.id)
            .where(WalkthroughStep.option_id == option.id)
            .order_by(WalkthroughStep.step_order, WalkthroughStep.id)))
        position = order.index(step.id) + 1
        thread = svc.ensure_step_thread(db, step, project_id, user)
        interaction.add_comment(
            db, thread, user, body.body_md,
            url=f"/p/{project_id}/o/{option.id}?mode=play&step={position}",
            context_label=f"{inst['label']} · {option.title} · {step.title}")
        svc.record(db, project_id, user, "step_comment",
                   {"step_id": step.id, "thread_id": thread.id})
        db.commit()
        return schemas.CreatedOut(id=thread.id)
