"""Module 3 — connection complaints: intake, assignment, workflow."""
from datetime import date

from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy import func, or_, select
from sqlalchemy.orm import Session, selectinload

from app.api.deps import client_ip, get_current_user, require_admin, require_write
from app.core.time import range_bounds_utc, utcnow
from app.db.session import get_db
from app.models.complaint import Complaint
from app.models.billing import BillingCustomer
from app.models.customer import Connection, Customer
from app.models.enums import ComplaintPriority, ComplaintStatus, ComplaintType, UserRole
from app.models.reference import Area
from app.models.user import User
from app.schemas.common import Message, Page
from app.schemas.complaint import (
    ComplaintAssign,
    ComplaintCreate,
    ComplaintDetail,
    ComplaintListItem,
    ComplaintStatusChange,
    ComplaintStatusCount,
    ComplaintSummary,
    ComplaintUpdate,
)
from app.services import audit
from app.services import complaints as workflow
from app.services.numbering import next_number

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

#: The detail view adds the status trail; a list row does not need it.
_DETAIL_LOADS = (selectinload(Complaint.status_history),)


def _get_or_404(db: Session, complaint_id: int, *, detail: bool = False) -> Complaint:
    stmt = select(Complaint).where(
        Complaint.id == complaint_id, Complaint.is_deleted.is_(False)
    )
    if detail:
        stmt = stmt.options(*_DETAIL_LOADS)
    complaint = db.scalar(stmt)
    if complaint is None:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Complaint not found")
    return complaint


def _as_detail(db: Session, complaint: Complaint) -> ComplaintDetail:
    out = ComplaintDetail.model_validate(complaint)
    out.allowed_transitions = workflow.allowed_transitions(complaint.status)
    if complaint.customer_ref:
        out.pop_name = db.scalar(
            select(BillingCustomer.pop).where(
                or_(
                    BillingCustomer.cust_id == complaint.customer_ref,
                    BillingCustomer.username == complaint.customer_ref,
                )
            )
        )
    return out


def _attach_pop_names(db: Session, items: list[ComplaintListItem]) -> None:
    """Fill `pop_name` for list rows from the billing mirror, in one query.

    Tickets store only the free-text `customer_ref` (billing cust_id/username);
    the panels own the canonical record, so the POP is resolved here at read
    time rather than copied onto the ticket.
    """
    refs = {i.customer_ref for i in items if i.customer_ref}
    if not refs:
        return
    rows = db.execute(
        select(BillingCustomer.cust_id, BillingCustomer.username, BillingCustomer.pop).where(
            or_(BillingCustomer.cust_id.in_(refs), BillingCustomer.username.in_(refs))
        )
    ).all()
    by_ref: dict[str, str] = {}
    for cust_id, username, pop in rows:
        # A cust_id match identifies the record; a username match is the fallback.
        if cust_id in refs and cust_id not in by_ref:
            by_ref[cust_id] = pop
        if username in refs and username not in by_ref:
            by_ref[username] = pop
    for item in items:
        if item.customer_ref in by_ref:
            item.pop_name = by_ref[item.customer_ref]


def _reload_detail(db: Session, complaint_id: int) -> ComplaintDetail:
    """Re-read after a write.

    The session runs with expire_on_commit=False, so a plain re-query hands back the same
    cached instance with its joined technician and history as they were first loaded.
    Expiring first forces the reload — same reason as the applications router.
    """
    db.expire_all()
    return _as_detail(db, _get_or_404(db, complaint_id, detail=True))


def _validate_refs(
    db: Session, *, connection_id: int | None, technician_id: int | None
) -> None:
    if connection_id is not None and not db.scalar(
        select(Connection.id).where(
            Connection.id == connection_id, Connection.is_deleted.is_(False)
        )
    ):
        raise HTTPException(status_code=422, detail="Unknown connection")
    if technician_id is not None and not db.scalar(
        select(User.id).where(
            User.id == technician_id,
            User.is_deleted.is_(False),
            User.is_active.is_(True),
            User.role.in_([UserRole.FIELD_STAFF, UserRole.MANAGER]),
        )
    ):
        raise HTTPException(status_code=422, detail="Unknown or inactive technician")


def _filtered(
    *,
    search: str | None,
    status_in: list[ComplaintStatus] | None,
    priority_in: list[ComplaintPriority] | None,
    type_in: list[ComplaintType] | None,
    technician_id: int | None,
    unassigned: bool,
    customer_id: int | None,
    connection_id: int | None,
    area_id: int | None,
    zone_id: int | None,
    date_from: date | None,
    date_to: date | None,
):
    stmt = select(Complaint).where(Complaint.is_deleted.is_(False))

    if search:
        pattern = f"%{search.strip()}%"
        # Ticket, symptom, address, or whoever is on the phone — one box for all four.
        customer_match = select(Connection.id).join(Customer).where(
            or_(
                Customer.full_name.ilike(pattern),
                Customer.phone.ilike(pattern),
                Customer.customer_code.ilike(pattern),
            )
        )
        stmt = stmt.where(
            or_(
                Complaint.ticket_no.ilike(pattern),
                Complaint.description.ilike(pattern),
                Complaint.address.ilike(pattern),
                Complaint.contact_phone.ilike(pattern),
                Complaint.customer_name.ilike(pattern),
                Complaint.customer_ref.ilike(pattern),
                Complaint.connection_id.in_(customer_match),
            )
        )
    if status_in:
        stmt = stmt.where(Complaint.status.in_(status_in))
    if priority_in:
        stmt = stmt.where(Complaint.priority.in_(priority_in))
    if type_in:
        stmt = stmt.where(Complaint.complaint_type.in_(type_in))
    if technician_id is not None:
        stmt = stmt.where(Complaint.assigned_technician_id == technician_id)
    if unassigned:
        stmt = stmt.where(Complaint.assigned_technician_id.is_(None))
    if connection_id is not None:
        stmt = stmt.where(Complaint.connection_id == connection_id)
    if customer_id is not None:
        stmt = stmt.where(
            Complaint.connection_id.in_(
                select(Connection.id).where(Connection.customer_id == customer_id)
            )
        )
    if area_id is not None:
        stmt = stmt.where(
            Complaint.connection_id.in_(select(Connection.id).where(Connection.area_id == area_id))
        )
    if zone_id is not None:
        stmt = stmt.where(
            Complaint.connection_id.in_(
                select(Connection.id).where(
                    Connection.area_id.in_(select(Area.id).where(Area.zone_id == zone_id))
                )
            )
        )
    start, end = range_bounds_utc(date_from, date_to)
    if start is not None:
        stmt = stmt.where(Complaint.reported_at >= start)
    if end is not None:
        stmt = stmt.where(Complaint.reported_at < end)
    return stmt


# --- list / summary ---


@router.get("", response_model=Page[ComplaintListItem])
def list_complaints(
    db: Session = Depends(get_db),
    _: User = Depends(get_current_user),
    search: str | None = Query(None, description="Ticket no, description, or customer"),
    status_in: list[ComplaintStatus] | None = Query(None, alias="status"),
    priority_in: list[ComplaintPriority] | None = Query(None, alias="priority"),
    type_in: list[ComplaintType] | None = Query(None, alias="complaint_type"),
    technician_id: int | None = None,
    unassigned: bool = Query(False, description="Only tickets with no technician"),
    customer_id: int | None = Query(None, description="Full complaint history for one customer"),
    connection_id: int | None = None,
    area_id: int | None = None,
    zone_id: int | None = None,
    date_from: date | None = Query(None, description="reported_at >= (Asia/Dhaka)"),
    date_to: date | None = Query(None, description="reported_at <= (Asia/Dhaka)"),
    sort: str = Query(
        "-reported_at",
        pattern="^-?(ticket_no|customer|complaint_type|technician|reported_at|created_at|priority|status|resolved_at)$",
    ),
    page: int = Query(1, ge=1),
    page_size: int = Query(25, ge=1, le=200),
) -> Page[ComplaintListItem]:
    stmt = _filtered(
        search=search,
        status_in=status_in,
        priority_in=priority_in,
        type_in=type_in,
        technician_id=technician_id,
        unassigned=unassigned,
        customer_id=customer_id,
        connection_id=connection_id,
        area_id=area_id,
        zone_id=zone_id,
        date_from=date_from,
        date_to=date_to,
    )
    total = db.scalar(select(func.count()).select_from(stmt.subquery())) or 0

    descending = sort.startswith("-")
    field = sort.lstrip("-")
    # Related-name sorts use a correlated subquery rather than a join: `connection`
    # and `technician` are already joined eagerly, and re-joining those tables would
    # collide with the eager load's aliasing.
    if field == "customer":
        order = func.coalesce(
            Complaint.customer_name,
            select(Customer.full_name)
            .join(Connection, Connection.customer_id == Customer.id)
            .where(Connection.id == Complaint.connection_id)
            .scalar_subquery(),
        )
    elif field == "technician":
        order = (
            select(User.full_name)
            .where(User.id == Complaint.assigned_technician_id)
            .scalar_subquery()
        )
    else:
        order = getattr(Complaint, field)
    stmt = stmt.order_by(order.desc() if descending else order.asc(), Complaint.id.desc())

    rows = db.scalars(stmt.offset((page - 1) * page_size).limit(page_size)).unique().all()
    items = [ComplaintListItem.model_validate(row) for row in rows]
    _attach_pop_names(db, items)
    return Page.build(items, total, page, page_size)


@router.get("/summary", response_model=ComplaintSummary)
def complaint_summary(
    db: Session = Depends(get_db),
    _: User = Depends(get_current_user),
) -> ComplaintSummary:
    return ComplaintSummary(**workflow.summary(db))


@router.get("/status-summary", response_model=list[ComplaintStatusCount])
def status_summary(
    db: Session = Depends(get_db),
    _: User = Depends(get_current_user),
    date_from: date | None = None,
    date_to: date | None = None,
) -> list[ComplaintStatusCount]:
    """Counts per status for the filter chips and the dashboard."""
    stmt = (
        select(Complaint.status, func.count())
        .where(Complaint.is_deleted.is_(False))
        .group_by(Complaint.status)
    )
    start, end = range_bounds_utc(date_from, date_to)
    if start is not None:
        stmt = stmt.where(Complaint.reported_at >= start)
    if end is not None:
        stmt = stmt.where(Complaint.reported_at < end)

    counts = {value: count for value, count in db.execute(stmt).all()}
    # Every status, including zeroes, so the chips keep a stable width.
    return [ComplaintStatusCount(status=s, count=counts.get(s, 0)) for s in ComplaintStatus]


# --- create / read / update / delete ---


@router.post("", response_model=ComplaintDetail, status_code=status.HTTP_201_CREATED)
def create_complaint(
    payload: ComplaintCreate,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_write),
) -> ComplaintDetail:
    _validate_refs(
        db,
        connection_id=payload.connection_id,
        technician_id=payload.assigned_technician_id,
    )

    now = utcnow()
    reported_at = payload.reported_at or now
    if reported_at.tzinfo is None:
        raise HTTPException(status_code=422, detail="reported_at must include a timezone offset")
    if reported_at > now:
        raise HTTPException(status_code=422, detail="reported_at cannot be in the future")

    data = payload.model_dump(exclude={"reported_at"})
    complaint = Complaint(
        ticket_no=next_number(db, "TKT"),
        **data,
        status=ComplaintStatus.OPEN,
        reported_at=reported_at,
        created_by_id=current_user.id,
    )
    db.add(complaint)
    db.flush()

    workflow.record_history(
        db,
        complaint,
        from_status=None,
        to_status=ComplaintStatus.OPEN,
        user_id=current_user.id,
        remarks="Complaint logged",
    )
    audit.record(
        db,
        user_id=current_user.id,
        action="create",
        entity_type="complaint",
        entity_id=complaint.id,
        payload={"ticket_no": complaint.ticket_no, "priority": complaint.priority.value},
        ip_address=client_ip(request),
    )
    db.commit()
    return _reload_detail(db, complaint.id)


@router.get("/{complaint_id}", response_model=ComplaintDetail)
def get_complaint(
    complaint_id: int,
    db: Session = Depends(get_db),
    _: User = Depends(get_current_user),
) -> ComplaintDetail:
    return _reload_detail(db, complaint_id)


@router.patch("/{complaint_id}", response_model=ComplaintDetail)
def update_complaint(
    complaint_id: int,
    payload: ComplaintUpdate,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_write),
) -> ComplaintDetail:
    complaint = _get_or_404(db, complaint_id)
    if complaint.status == ComplaintStatus.CLOSED:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="A closed complaint can no longer be edited — reopen it first",
        )

    changes = payload.model_dump(exclude_unset=True)
    if not changes:
        return _reload_detail(db, complaint_id)

    _validate_refs(
        db, connection_id=None, technician_id=changes.get("assigned_technician_id")
    )
    for field, value in changes.items():
        setattr(complaint, field, value)

    audit.record(
        db,
        user_id=current_user.id,
        action="update",
        entity_type="complaint",
        entity_id=complaint.id,
        payload={k: str(v) for k, v in changes.items()},
        ip_address=client_ip(request),
    )
    db.commit()
    return _reload_detail(db, complaint_id)


@router.post("/{complaint_id}/status", response_model=ComplaintDetail)
def change_complaint_status(
    complaint_id: int,
    payload: ComplaintStatusChange,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_write),
) -> ComplaintDetail:
    """Advance the workflow. Closing a ticket is manager-or-admin only."""
    complaint = _get_or_404(db, complaint_id, detail=True)

    if payload.status == ComplaintStatus.CLOSED and current_user.role not in (
        UserRole.ADMIN,
        UserRole.MANAGER,
        UserRole.SYSTEM_DEVELOPER,
    ):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="Only a manager or admin can close a complaint",
        )

    workflow.change_status(db, complaint, payload, current_user)
    audit.record(
        db,
        user_id=current_user.id,
        action="status_change",
        entity_type="complaint",
        entity_id=complaint.id,
        payload={"to": payload.status.value},
        ip_address=client_ip(request),
    )
    db.commit()
    return _reload_detail(db, complaint_id)


@router.post("/{complaint_id}/assign", response_model=ComplaintDetail)
def assign_complaint(
    complaint_id: int,
    payload: ComplaintAssign,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_write),
) -> ComplaintDetail:
    """Assign or clear the technician without touching the status."""
    complaint = _get_or_404(db, complaint_id)
    if complaint.status == ComplaintStatus.CLOSED:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="A closed complaint cannot be reassigned",
        )
    _validate_refs(db, connection_id=None, technician_id=payload.assigned_technician_id)

    complaint.assigned_technician_id = payload.assigned_technician_id
    audit.record(
        db,
        user_id=current_user.id,
        action="assign",
        entity_type="complaint",
        entity_id=complaint.id,
        payload={"technician_id": payload.assigned_technician_id},
        ip_address=client_ip(request),
    )
    db.commit()
    return _reload_detail(db, complaint_id)


@router.delete("/{complaint_id}", response_model=Message)
def delete_complaint(
    complaint_id: int,
    request: Request,
    db: Session = Depends(get_db),
    current_user: User = Depends(require_admin),
) -> Message:
    """Soft delete — for tickets logged in error. Resolved history is kept."""
    complaint = _get_or_404(db, complaint_id)
    complaint.is_deleted = True
    complaint.deleted_at = utcnow()
    audit.record(
        db,
        user_id=current_user.id,
        action="delete",
        entity_type="complaint",
        entity_id=complaint.id,
        payload={"ticket_no": complaint.ticket_no},
        ip_address=client_ip(request),
    )
    db.commit()
    return Message(detail=f"Complaint {complaint.ticket_no} deleted")
