"""Ticket routes. Thin: HTTP in/out and validation only.

There is deliberately no DELETE route. Ticket numbers are permanent and
auditable; unwanted tickets are moved to status `void`.
"""

from __future__ import annotations

import json

from fastapi import (
    APIRouter,
    Depends,
    File,
    Form,
    HTTPException,
    Query,
    UploadFile,
    status,
)
from fastapi.responses import Response
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.db import get_db
from app.models import schemas
from app.models.tables import Ticket, TicketMedia, TicketStatus
from app.services import media as media_service
from app.services import tickets as service
from app.services.identity import CurrentActor, resolve_actor

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


def _get_or_404(db: Session, ticket_id: int) -> Ticket:
    ticket = db.get(Ticket, ticket_id)
    if ticket is None:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail={
                "error_code": "TICKET_NOT_FOUND",
                "summary": "No ticket with that id.",
                "details": None,
            },
        )
    return ticket


@router.get("", response_model=list[schemas.TicketSummary])
def list_tickets(
    status_filter: TicketStatus | None = Query(default=None, alias="status"),
    open_only: bool = Query(default=False),
    limit: int = Query(default=100, ge=1, le=500),
    db: Session = Depends(get_db),
) -> list[schemas.TicketSummary]:
    stmt = select(Ticket)
    if status_filter is not None:
        stmt = stmt.where(Ticket.status == status_filter)
    elif open_only:
        stmt = stmt.where(
            Ticket.status.notin_([TicketStatus.closed, TicketStatus.void])
        )
    stmt = stmt.order_by(Ticket.number.desc()).limit(limit)

    rows = list(db.scalars(stmt).unique())
    counts = service.media_counts(db, [t.id for t in rows])
    return [service.to_summary(t, counts.get(t.id, 0)) for t in rows]


@router.post("", response_model=schemas.TicketDetail, status_code=201)
async def create_ticket(
    payload: str = Form(..., description="JSON-encoded TicketCreate"),
    attachment: UploadFile | None = File(default=None),
    audio: UploadFile | None = File(default=None),
    db: Session = Depends(get_db),
    actor: CurrentActor = Depends(resolve_actor),
) -> schemas.TicketDetail:
    """Multipart so a photo rides along with the first request.

    One round trip matters here: the whole product claim is that capture takes
    seconds on a plant floor with poor wifi.
    """
    try:
        parsed = schemas.TicketCreate.model_validate(json.loads(payload))
    except (json.JSONDecodeError, ValueError) as exc:
        raise HTTPException(
            status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
            detail={
                "error_code": "TICKET_PAYLOAD_INVALID",
                "summary": "The ticket details could not be read.",
                "details": str(exc)[:500],
            },
        ) from None

    try:
        ticket = service.create_ticket(db, parsed, actor)
    except ValueError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": str(exc),
                "summary": "The parent ticket does not exist.",
                "details": None,
            },
        ) from None

    # The photo and the voice memo are both optional and independent. Either
    # one failing must never cost the technician the report itself, which is
    # already saved by this point.
    for upload in (attachment, audio):
        if upload is None:
            continue
        content = await upload.read()
        try:
            stored = media_service.store(content, upload.content_type or "")
        except media_service.MediaError as exc:
            # The ticket itself is already saved - losing the photo must never
            # lose the report. Record the failure in the audit trail instead.
            service.record_event(db, ticket, "attachment_rejected", actor, exc.summary)
            db.commit()
        else:
            db.add(
                TicketMedia(
                    ticket_id=ticket.id,
                    rel_path=stored.rel_path,
                    mime=stored.mime,
                    size_bytes=stored.size_bytes,
                    uploaded_by_id=actor.id,
                )
            )
            service.record_event(db, ticket, "attachment_added", actor, stored.mime)
            db.commit()
        db.refresh(ticket)

    return service.to_detail(db, ticket)


@router.get("/{ticket_id}", response_model=schemas.TicketDetail)
def get_ticket(ticket_id: int, db: Session = Depends(get_db)) -> schemas.TicketDetail:
    return service.to_detail(db, _get_or_404(db, ticket_id))


@router.patch("/{ticket_id}", response_model=schemas.TicketDetail)
def patch_ticket(
    ticket_id: int,
    payload: schemas.TicketUpdate,
    db: Session = Depends(get_db),
    actor: CurrentActor = Depends(resolve_actor),
) -> schemas.TicketDetail:
    ticket = service.update_ticket(db, _get_or_404(db, ticket_id), payload, actor)
    return service.to_detail(db, ticket)


@router.post("/{ticket_id}/parts", response_model=schemas.TicketDetail, status_code=201)
def add_part(
    ticket_id: int,
    payload: schemas.PartUsageCreate,
    db: Session = Depends(get_db),
    actor: CurrentActor = Depends(resolve_actor),
) -> schemas.TicketDetail:
    ticket = _get_or_404(db, ticket_id)
    try:
        service.add_part_usage(db, ticket, payload, actor)
    except ValueError:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": "PART_NOT_FOUND",
                "summary": "No part with that id.",
                "details": None,
            },
        ) from None
    db.refresh(ticket)
    return service.to_detail(db, ticket)


@router.post("/{ticket_id}/notes", response_model=schemas.TicketDetail, status_code=201)
def add_note(
    ticket_id: int,
    payload: schemas.NoteCreate,
    db: Session = Depends(get_db),
    actor: CurrentActor = Depends(resolve_actor),
) -> schemas.TicketDetail:
    ticket = _get_or_404(db, ticket_id)
    service.record_event(db, ticket, "note", actor, payload.detail.strip())
    db.commit()
    db.refresh(ticket)
    return service.to_detail(db, ticket)


@router.post(
    "/{ticket_id}/attachments", response_model=schemas.TicketDetail, status_code=201
)
async def add_attachment(
    ticket_id: int,
    attachment: UploadFile = File(...),
    db: Session = Depends(get_db),
    actor: CurrentActor = Depends(resolve_actor),
) -> schemas.TicketDetail:
    ticket = _get_or_404(db, ticket_id)
    content = await attachment.read()
    try:
        stored = media_service.store(content, attachment.content_type or "")
    except media_service.MediaError as exc:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail={
                "error_code": exc.error_code,
                "summary": exc.summary,
                "details": exc.details,
            },
        ) from None

    db.add(
        TicketMedia(
            ticket_id=ticket.id,
            rel_path=stored.rel_path,
            mime=stored.mime,
            size_bytes=stored.size_bytes,
            uploaded_by_id=actor.id,
        )
    )
    service.record_event(db, ticket, "attachment_added", actor, stored.mime)
    db.commit()
    db.refresh(ticket)
    return service.to_detail(db, ticket)


@router.delete("/{ticket_id}", include_in_schema=False)
def delete_ticket_is_forbidden(ticket_id: int) -> Response:
    """Explicit refusal, so the guarantee is visible rather than merely absent."""
    raise HTTPException(
        status_code=status.HTTP_405_METHOD_NOT_ALLOWED,
        detail={
            "error_code": "TICKET_DELETE_FORBIDDEN",
            "summary": "Tickets are permanent and cannot be deleted.",
            "details": "Set the status to 'void' instead; the number is retained.",
        },
    )
