"""Options: admin management, the research-package import, and screenshot serving.

Screenshots are served here — through membership checks — never via /assets or
any public path (D13). A member can fetch a screenshot only for a PUBLISHED
option of a project they are on; anonymized draft work is admin-only until the
admin deliberately publishes (D14).
"""

from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status
from fastapi.responses import FileResponse
from sqlalchemy import func, select
from sqlalchemy.orm import Session

from app.constants import OptionStatus
from app.db import get_db
from app.models.account import Account
from app.models.option import Option, Review
from app.models.schemas import (
    ImportResultOut,
    OptionAdminOut,
    OptionReorder,
    OptionUpdate,
)
from app.services import options as options_service
from app.services import research_import, storage
from app.services import levels
from app.services.authz import (
    current_account,
    project_with_permission,
    readable_project,
)

router = APIRouter(prefix="/api/projects/{project_id}", tags=["options"])

# The whole research package (manifest + a dozen screenshots) in one upload.
MAX_IMPORT_BYTES = 200 * 1024 * 1024


def _review_counts(db: Session, project_id: int) -> dict[int, int]:
    rows = db.execute(
        select(Review.option_id, func.count())
        .where(Review.project_id == project_id)
        .group_by(Review.option_id)
    ).all()
    return dict(rows)


def _to_admin_out(option: Option, review_count: int = 0) -> OptionAdminOut:
    out = OptionAdminOut.model_validate(option)
    out.review_count = review_count
    out.has_desktop = bool(option.screenshot_desktop)
    out.has_mobile = bool(option.screenshot_mobile)
    return out


@router.get("/options", response_model=list[OptionAdminOut])
def list_options(
    project_id: int,
    admin: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> list[OptionAdminOut]:
    project = project_with_permission(db, project_id, admin, levels.PROJECT_MANAGE)
    counts = _review_counts(db, project.id)
    return [
        _to_admin_out(option, counts.get(option.id, 0))
        for option in options_service.list_for_admin(db, project.id)
    ]


@router.patch("/options/{option_id}", response_model=OptionAdminOut)
def update_option(
    project_id: int,
    option_id: int,
    payload: OptionUpdate,
    admin: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> OptionAdminOut:
    project = project_with_permission(db, project_id, admin, levels.PROJECT_MANAGE)
    option = options_service.get_option(db, project.id, option_id)
    if option is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error_code": "OPTION_NOT_FOUND", "summary": "Option not found."},
        )
    options_service.update(db, option, **payload.model_dump(exclude_unset=True))
    counts = _review_counts(db, project.id)
    return _to_admin_out(option, counts.get(option.id, 0))


@router.delete("/options/{option_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_option(
    project_id: int,
    option_id: int,
    admin: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> None:
    """Refuses when reviews exist — collected reactions are the product and are
    never a cascade casualty. Unpublish instead; deletion is for mistakes."""
    project = project_with_permission(db, project_id, admin, levels.PROJECT_MANAGE)
    option = options_service.get_option(db, project.id, option_id)
    if option is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={"error_code": "OPTION_NOT_FOUND", "summary": "Option not found."},
        )
    review_count = db.scalar(
        select(func.count()).select_from(Review).where(Review.option_id == option.id)
    )
    if review_count:
        raise HTTPException(
            status_code=status.HTTP_409_CONFLICT,
            detail={
                "error_code": "OPTION_HAS_REVIEWS",
                "summary": f"{review_count} review(s) exist for this option.",
                "details": "Unpublish it instead of deleting, so collected reactions are kept.",
            },
        )
    options_service.delete(db, option)


@router.post("/options/reorder", response_model=list[OptionAdminOut])
def reorder_options(
    project_id: int,
    payload: OptionReorder,
    admin: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> list[OptionAdminOut]:
    project = project_with_permission(db, project_id, admin, levels.PROJECT_MANAGE)
    reordered = options_service.reorder(db, project.id, payload.ordered_ids)
    counts = _review_counts(db, project.id)
    return [_to_admin_out(option, counts.get(option.id, 0)) for option in reordered]


@router.post("/import", response_model=ImportResultOut)
def import_research_package(
    project_id: int,
    package: UploadFile = File(...),
    admin: Account = Depends(current_account),
    db: Session = Depends(get_db),
) -> ImportResultOut:
    """Upload the research package produced by the briefing's Claude session.
    Validated as a whole before anything is written; idempotent by option slug."""
    project = project_with_permission(db, project_id, admin, levels.PROJECT_MANAGE)
    payload = package.file.read(MAX_IMPORT_BYTES + 1)
    if len(payload) > MAX_IMPORT_BYTES:
        raise HTTPException(
            status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
            detail={
                "error_code": "IMPORT_TOO_LARGE",
                "summary": f"The package exceeds {MAX_IMPORT_BYTES // (1024 * 1024)} MB.",
            },
        )
    try:
        result = research_import.import_package(db, project, payload)
    except research_import.ImportError_ as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail={"error_code": exc.code, "summary": exc.summary, "details": exc.details},
        ) from exc
    except storage.StorageError as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail={"error_code": exc.code, "summary": exc.summary, "details": exc.details},
        ) from exc
    return ImportResultOut(created=result.created, updated=result.updated)


@router.get("/options/{option_id}/screenshot/{kind}", response_class=FileResponse)
def option_screenshot(
    project_id: int,
    option_id: int,
    kind: str,
    account: Account = Depends(current_account),
    db: Session = Depends(get_db),
):
    """The authed screenshot route. Access to the project lets you see published
    captures; only someone who manages the project may see draft ones."""
    project = readable_project(db, project_id, account)
    can_manage = levels.project_can(account.username, project.id, levels.PROJECT_MANAGE)
    option = options_service.get_option(db, project.id, option_id)
    not_found = HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail={"error_code": "SCREENSHOT_NOT_FOUND", "summary": "Screenshot not found."},
    )
    if option is None or kind not in ("desktop", "mobile"):
        raise not_found
    if not can_manage and option.status != OptionStatus.PUBLISHED:
        raise not_found  # 404, not 403: drafts must be invisible, not teased
    relative = option.screenshot_desktop if kind == "desktop" else option.screenshot_mobile
    if not relative:
        raise not_found
    try:
        absolute = storage.resolve(relative)
    except storage.StorageError as exc:
        raise not_found from exc
    if not absolute.is_file():
        raise not_found
    return FileResponse(
        absolute,
        media_type="image/png",
        headers={"Cache-Control": "private, max-age=300"},
    )
