"""Ticket business logic. Routers stay thin; this is where the rules live."""

from __future__ import annotations

from datetime import UTC, datetime

from sqlalchemy import Sequence, func, select
from sqlalchemy.orm import Session

from app.config import get_settings
from app.models import schemas
from app.models.tables import (
    Part,
    PartUsage,
    Ticket,
    TicketEvent,
    TicketMedia,
    TicketStatus,
)
from app.services.identity import CurrentActor

# Never reset, never cycled, never reused. A gap in the series must always mean
# something happened - never that a row vanished. Tickets are therefore never
# deleted; unwanted ones are voided.
TICKET_NUMBER_SEQ = Sequence("ticket_number_seq")

_CLOSED_STATES = (TicketStatus.closed, TicketStatus.void)


def display_number(number: int) -> str:
    return f"{get_settings().ticket_prefix}-{number:06d}"


def next_ticket_number(db: Session) -> int:
    return db.execute(TICKET_NUMBER_SEQ.next_value()).scalar_one()


def record_event(
    db: Session,
    ticket: Ticket,
    kind: str,
    actor: CurrentActor | None,
    detail: str | None = None,
) -> TicketEvent:
    event = TicketEvent(
        ticket_id=ticket.id,
        kind=kind,
        detail=detail,
        actor_id=actor.id if actor else None,
    )
    db.add(event)
    return event


def create_ticket(
    db: Session, payload: schemas.TicketCreate, actor: CurrentActor
) -> Ticket:
    parent = None
    if payload.parent_ticket_id is not None:
        parent = db.get(Ticket, payload.parent_ticket_id)
        if parent is None:
            raise ValueError("PARENT_NOT_FOUND")

    ticket = Ticket(
        number=next_ticket_number(db),
        asset_id=payload.asset_id,
        description=payload.description.strip(),
        priority=payload.priority,
        parent_ticket_id=payload.parent_ticket_id,
        reported_by_id=actor.id,
    )
    db.add(ticket)
    db.flush()

    detail = (
        f"Spawned from {display_number(parent.number)}" if parent is not None else None
    )
    record_event(db, ticket, "reported", actor, detail)
    if parent is not None:
        # Record the link on BOTH sides. Reading the parent's history must show
        # what the job uncovered, without having to search for children.
        record_event(
            db,
            parent,
            "follow_up_raised",
            actor,
            f"{display_number(ticket.number)}: {ticket.description[:120]}",
        )
    db.commit()
    db.refresh(ticket)
    return ticket


def update_ticket(
    db: Session, ticket: Ticket, payload: schemas.TicketUpdate, actor: CurrentActor
) -> Ticket:
    if payload.priority is not None and payload.priority != ticket.priority:
        record_event(
            db,
            ticket,
            "priority_changed",
            actor,
            f"{ticket.priority.value} -> {payload.priority.value}",
        )
        ticket.priority = payload.priority

    if (
        payload.assigned_to_id is not None
        and payload.assigned_to_id != ticket.assigned_to_id
    ):
        ticket.assigned_to_id = payload.assigned_to_id
        db.flush()
        assignee = ticket.assigned_to.name if ticket.assigned_to else "unassigned"
        record_event(db, ticket, "assigned", actor, assignee)

    if (
        payload.scheduled_for is not None
        and payload.scheduled_for != ticket.scheduled_for
    ):
        ticket.scheduled_for = payload.scheduled_for
        record_event(
            db,
            ticket,
            "scheduled",
            actor,
            payload.scheduled_for.strftime("%Y-%m-%d %H:%M"),
        )

    if payload.status is not None and payload.status != ticket.status:
        record_event(
            db,
            ticket,
            "status_changed",
            actor,
            f"{ticket.status.value} -> {payload.status.value}",
        )
        ticket.status = payload.status
        if payload.status in _CLOSED_STATES:
            ticket.closed_at = datetime.now(UTC)
        else:
            ticket.closed_at = None

    db.commit()
    db.refresh(ticket)
    return ticket


def add_part_usage(
    db: Session, ticket: Ticket, payload: schemas.PartUsageCreate, actor: CurrentActor
) -> PartUsage:
    part = db.get(Part, payload.part_id)
    if part is None:
        raise ValueError("PART_NOT_FOUND")

    usage = PartUsage(
        ticket_id=ticket.id,
        part_id=part.id,
        quantity=payload.quantity,
        direction=payload.direction,
        actor_id=actor.id,
    )
    db.add(usage)
    record_event(
        db,
        ticket,
        "part_" + payload.direction.value,
        actor,
        f"{payload.quantity:g} x {part.part_number} {part.part_name}",
    )
    db.commit()
    db.refresh(usage)
    return usage


def list_events(db: Session, ticket_id: int) -> list[TicketEvent]:
    return list(
        db.scalars(
            select(TicketEvent)
            .where(TicketEvent.ticket_id == ticket_id)
            .order_by(TicketEvent.at, TicketEvent.id)
        )
    )


def list_usage(db: Session, ticket_id: int) -> list[PartUsage]:
    return list(
        db.scalars(
            select(PartUsage)
            .where(PartUsage.ticket_id == ticket_id)
            .order_by(PartUsage.at, PartUsage.id)
        )
    )


def media_counts(db: Session, ticket_ids: list[int]) -> dict[int, int]:
    if not ticket_ids:
        return {}
    rows = db.execute(
        select(TicketMedia.ticket_id, func.count(TicketMedia.id))
        .where(TicketMedia.ticket_id.in_(ticket_ids))
        .group_by(TicketMedia.ticket_id)
    ).all()
    return {ticket_id: count for ticket_id, count in rows}


def to_summary(ticket: Ticket, media_count: int) -> schemas.TicketSummary:
    return schemas.TicketSummary(
        id=ticket.id,
        number=ticket.number,
        display_number=display_number(ticket.number),
        description=ticket.description,
        status=ticket.status,
        priority=ticket.priority,
        asset_equipment_no=ticket.asset.equipment_no if ticket.asset else None,
        asset_description=ticket.asset.description if ticket.asset else None,
        reported_by_name=ticket.reported_by.name if ticket.reported_by else None,
        assigned_to_name=ticket.assigned_to.name if ticket.assigned_to else None,
        reported_at=ticket.reported_at,
        scheduled_for=ticket.scheduled_for,
        media_count=media_count,
    )


def to_detail(
    db: Session, ticket: Ticket, media_count: int | None = None
) -> schemas.TicketDetail:
    if media_count is None:
        media_count = len(ticket.media)

    parent_display = None
    if ticket.parent_ticket_id is not None:
        parent = db.get(Ticket, ticket.parent_ticket_id)
        if parent is not None:
            parent_display = display_number(parent.number)

    summary = to_summary(ticket, media_count)
    return schemas.TicketDetail(
        **summary.model_dump(),
        parent_ticket_id=ticket.parent_ticket_id,
        parent_display_number=parent_display,
        closed_at=ticket.closed_at,
        media=[schemas.MediaOut.model_validate(m) for m in ticket.media],
        events=[
            schemas.EventOut(
                id=e.id,
                kind=e.kind,
                detail=e.detail,
                actor_name=e.actor.name if e.actor else None,
                at=e.at,
            )
            for e in list_events(db, ticket.id)
        ],
        parts_used=[
            schemas.PartUsageOut(
                id=u.id,
                part_id=u.part_id,
                part_number=u.part.part_number,
                part_name=u.part.part_name,
                quantity=float(u.quantity),
                direction=u.direction,
                actor_name=u.actor.name if u.actor else None,
                at=u.at,
            )
            for u in list_usage(db, ticket.id)
        ],
    )
