"""The unified daily log — one Asia/Dhaka day across all three modules.

Nothing here writes. Entry still goes through each module's own endpoint, which keeps a
single implementation of every business rule; this service only answers "what happened
on this day, and what is still waiting".

**The day is a Dhaka calendar day**, resolved to UTC bounds once and reused by every
query, so a ticket logged at 11pm Dhaka never leaks into the next day's log.

**Which timestamp counts as "the day"** differs per module, and deliberately matches
whatever that module already filters on: applications by `created_at`, complaints by
`reported_at` (which may be back-dated to when the call actually came in), equipment by
`issued_at` / `collected_at`.
"""
from datetime import date

from sqlalchemy import Select, func, or_, select
from sqlalchemy.orm import Session, selectinload

from app.core.time import day_bounds_utc, today_local
from app.models.application import ApplicationStatusHistory, ConnectionApplication
from app.models.complaint import Complaint, ComplaintStatusHistory
from app.models.enums import (
    ApplicationStatus,
    ComplaintStatus,
    EquipmentCondition,
    HolderType,
)
from app.models.equipment import EquipmentCollection, EquipmentDistribution
from app.schemas.daily_log import DailyLogEventKind
from app.services.equipment import overdue_filter

#: Per-module cap on the rows returned. The counts are exact regardless; these lists are
#: the day's entries as a person scans them, not a report.
LIST_LIMIT = 50

#: Cap on the merged feed. `timeline_truncated` tells the caller when it bit.
TIMELINE_LIMIT = 150


def _count(db: Session, stmt: Select) -> int:
    return db.scalar(select(func.count()).select_from(stmt.subquery())) or 0


# --- per-module day queries ---


def _applications_stmt(start, end, actor_id: int | None) -> Select:
    stmt = select(ConnectionApplication).where(
        ConnectionApplication.is_deleted.is_(False),
        ConnectionApplication.created_at >= start,
        ConnectionApplication.created_at < end,
    )
    if actor_id is not None:
        stmt = stmt.where(ConnectionApplication.created_by_id == actor_id)
    return stmt


def _complaints_stmt(start, end, actor_id: int | None) -> Select:
    stmt = select(Complaint).where(
        Complaint.is_deleted.is_(False),
        Complaint.reported_at >= start,
        Complaint.reported_at < end,
    )
    if actor_id is not None:
        stmt = stmt.where(Complaint.created_by_id == actor_id)
    return stmt


def _distributions_stmt(start, end, actor_id: int | None) -> Select:
    stmt = select(EquipmentDistribution).where(
        EquipmentDistribution.is_deleted.is_(False),
        EquipmentDistribution.issued_at >= start,
        EquipmentDistribution.issued_at < end,
    )
    if actor_id is not None:
        stmt = stmt.where(EquipmentDistribution.issued_by_id == actor_id)
    return stmt


def _collections_stmt(start, end, actor_id: int | None) -> Select:
    stmt = select(EquipmentCollection).where(
        EquipmentCollection.is_deleted.is_(False),
        EquipmentCollection.collected_at >= start,
        EquipmentCollection.collected_at < end,
    )
    if actor_id is not None:
        stmt = stmt.where(EquipmentCollection.collected_by_id == actor_id)
    return stmt


def _app_history_stmt(start, end, actor_id: int | None) -> Select:
    stmt = (
        select(ApplicationStatusHistory)
        .join(ApplicationStatusHistory.application)
        .where(
            ConnectionApplication.is_deleted.is_(False),
            ApplicationStatusHistory.changed_at >= start,
            ApplicationStatusHistory.changed_at < end,
        )
    )
    if actor_id is not None:
        stmt = stmt.where(ApplicationStatusHistory.changed_by_id == actor_id)
    return stmt


def _complaint_history_stmt(start, end, actor_id: int | None) -> Select:
    stmt = (
        select(ComplaintStatusHistory)
        .join(ComplaintStatusHistory.complaint)
        .where(
            Complaint.is_deleted.is_(False),
            ComplaintStatusHistory.changed_at >= start,
            ComplaintStatusHistory.changed_at < end,
        )
    )
    if actor_id is not None:
        stmt = stmt.where(ComplaintStatusHistory.changed_by_id == actor_id)
    return stmt


# --- counts ---


def _counts(db: Session, start, end, actor_id: int | None) -> dict:
    installed = _app_history_stmt(start, end, actor_id).where(
        ApplicationStatusHistory.to_status == ApplicationStatus.INSTALLED
    )
    # A ticket is "resolved today" when it reached `resolved`, or when it was closed
    # today having never been resolved. Closing an already-resolved ticket is not a
    # second fix.
    resolved = _complaint_history_stmt(start, end, actor_id).where(
        or_(
            ComplaintStatusHistory.to_status == ComplaintStatus.RESOLVED,
            (ComplaintStatusHistory.to_status == ComplaintStatus.CLOSED)
            & (ComplaintStatusHistory.from_status != ComplaintStatus.RESOLVED),
        )
    )
    return {
        "applications_created": _count(db, _applications_stmt(start, end, actor_id)),
        "applications_installed": _count(db, installed),
        "complaints_reported": _count(db, _complaints_stmt(start, end, actor_id)),
        "complaints_resolved": _count(db, resolved),
        "equipment_issued": _count(db, _distributions_stmt(start, end, actor_id)),
        "equipment_collected": _count(db, _collections_stmt(start, end, actor_id)),
    }


def _pending(db: Session) -> dict:
    """Live backlog, never scoped to the selected day — a queue is a queue."""
    apps = select(ConnectionApplication).where(ConnectionApplication.is_deleted.is_(False))
    tickets = select(Complaint).where(Complaint.is_deleted.is_(False))
    return {
        "applications_pending": _count(
            db, apps.where(ConnectionApplication.status == ApplicationStatus.PENDING)
        ),
        "applications_awaiting_install": _count(
            db, apps.where(ConnectionApplication.status == ApplicationStatus.APPROVED)
        ),
        "complaints_open": _count(
            db,
            tickets.where(
                Complaint.status.in_((ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS))
            ),
        ),
        "complaints_unassigned": _count(
            db,
            tickets.where(
                Complaint.status.in_((ComplaintStatus.OPEN, ComplaintStatus.IN_PROGRESS)),
                Complaint.assigned_technician_id.is_(None),
            ),
        ),
        "equipment_overdue_returns": _count(
            db,
            select(EquipmentDistribution).where(
                EquipmentDistribution.is_deleted.is_(False), overdue_filter()
            ),
        ),
    }


# --- timeline ---


def _item_label(item) -> str:
    name = item.equipment_type.name
    return f"{name} · {item.serial_number}" if item.serial_number else name


def _holder_name(distribution: EquipmentDistribution) -> str:
    if distribution.issued_to_type == HolderType.TECHNICIAN:
        return distribution.issued_to_user.full_name if distribution.issued_to_user else "a technician"
    return (
        distribution.issued_to_customer.full_name
        if distribution.issued_to_customer
        else "a customer"
    )


def _timeline(db: Session, start, end, actor_id: int | None) -> tuple[list[dict], bool]:
    """Merge every module's events into one newest-first feed.

    Four small queries merged in Python rather than a SQL UNION: the rows have nothing
    in common beyond a timestamp, and each one needs its own relationships loaded to
    render a label.
    """
    events: list[dict] = []

    app_history = db.scalars(
        _app_history_stmt(start, end, actor_id).options(
            selectinload(ApplicationStatusHistory.application)
        )
    ).unique().all()
    for entry in app_history:
        application = entry.application
        events.append(
            {
                "kind": DailyLogEventKind.APPLICATION,
                "at": entry.changed_at,
                "ref_id": application.id,
                "ref_no": application.application_no,
                "title": application.applicant_name,
                "detail": entry.remarks,
                "from_status": entry.from_status.value if entry.from_status else None,
                "to_status": entry.to_status.value,
                "actor": entry.changed_by,
            }
        )

    ticket_history = db.scalars(
        _complaint_history_stmt(start, end, actor_id).options(
            selectinload(ComplaintStatusHistory.complaint)
        )
    ).unique().all()
    for entry in ticket_history:
        complaint = entry.complaint
        events.append(
            {
                "kind": DailyLogEventKind.COMPLAINT,
                "at": entry.changed_at,
                "ref_id": complaint.id,
                "ref_no": complaint.ticket_no,
                "title": (
                    complaint.connection.customer.full_name
                    if complaint.connection
                    else complaint.customer_name or "Unknown customer"
                ),
                "detail": entry.remarks or complaint.complaint_type.value.replace("_", " "),
                "from_status": entry.from_status.value if entry.from_status else None,
                "to_status": entry.to_status.value,
                "actor": entry.changed_by,
            }
        )

    for issue in db.scalars(_distributions_stmt(start, end, actor_id)).unique().all():
        events.append(
            {
                "kind": DailyLogEventKind.EQUIPMENT_ISSUE,
                "at": issue.issued_at,
                "ref_id": issue.id,
                "ref_no": issue.item.serial_number,
                "title": _item_label(issue.item),
                "detail": (
                    f"{issue.quantity} {issue.item.equipment_type.unit} to {_holder_name(issue)}"
                ),
                "actor": issue.issued_by,
            }
        )

    for collection in db.scalars(_collections_stmt(start, end, actor_id)).unique().all():
        condition = collection.condition_on_return
        events.append(
            {
                "kind": DailyLogEventKind.EQUIPMENT_COLLECTION,
                "at": collection.collected_at,
                "ref_id": collection.id,
                "ref_no": collection.item.serial_number,
                "title": _item_label(collection.item),
                "detail": (
                    f"{collection.quantity} {collection.item.equipment_type.unit} back to "
                    f"{collection.returned_to_location.name}"
                    + (" — faulty" if condition == EquipmentCondition.FAULTY else "")
                ),
                "actor": collection.collected_by,
            }
        )

    # Newest first: on a live entry screen the thing you just saved belongs at the top.
    events.sort(key=lambda e: (e["at"], e["ref_id"]), reverse=True)
    return events[:TIMELINE_LIMIT], len(events) > TIMELINE_LIMIT


# --- entry point ---


def build(db: Session, day: date, *, actor_id: int | None = None) -> dict:
    """Everything the Daily Log screen renders for one Dhaka day.

    `actor_id` narrows every list, count, and event to entries that user logged; the
    `pending` backlog stays global, since it is about the business, not the person.
    """
    start, end = day_bounds_utc(day)
    timeline, truncated = _timeline(db, start, end, actor_id)

    applications = db.scalars(
        _applications_stmt(start, end, actor_id)
        .order_by(ConnectionApplication.created_at.desc())
        .limit(LIST_LIMIT)
    ).unique().all()
    complaints = db.scalars(
        _complaints_stmt(start, end, actor_id)
        .order_by(Complaint.reported_at.desc())
        .limit(LIST_LIMIT)
    ).unique().all()
    distributions = db.scalars(
        _distributions_stmt(start, end, actor_id)
        .order_by(EquipmentDistribution.issued_at.desc())
        .limit(LIST_LIMIT)
    ).unique().all()
    collections = db.scalars(
        _collections_stmt(start, end, actor_id)
        .order_by(EquipmentCollection.collected_at.desc())
        .limit(LIST_LIMIT)
    ).unique().all()

    return {
        "day": day,
        "is_today": day == today_local(),
        "mine_only": actor_id is not None,
        "counts": _counts(db, start, end, actor_id),
        "pending": _pending(db),
        "timeline": timeline,
        "timeline_truncated": truncated,
        "applications": applications,
        "complaints": complaints,
        "distributions": distributions,
        "collections": collections,
    }
