"""
Deployment-history calendar service — builds the data structure for the
fleet-wide deployment-history grid (`/tools/deployment-history`).

For each calendar day in a 12-month window, computes which projects had
at least one unit assigned to a location on that day.  Renders as
multi-month grid (job-planner style) with project-colored bars per day.

Distinct from `services/fleet_calendar_service.py` which renders
forward-looking RESERVATIONS for the planner.  This one is purely
historical / current — it walks `unit_assignments` instead of
`job_reservations`.
"""

from __future__ import annotations

import hashlib
from datetime import date, datetime, timedelta
from calendar import monthrange
from typing import Optional

from sqlalchemy.orm import Session
from sqlalchemy import and_, or_

from backend.models import Project, UnitAssignment


# Color palette for projects without an explicit color attribute.  Chosen
# to have decent contrast on both light and dark backgrounds; cycles
# deterministically by SHA1(project_id).
_PROJECT_COLOR_PALETTE = [
    "#f48b1c", "#142a66", "#7d234d", "#0e7490", "#15803d",
    "#a16207", "#9333ea", "#dc2626", "#0d9488", "#1d4ed8",
    "#be185d", "#65a30d", "#0891b2", "#7c3aed", "#b91c1c",
]


def _color_for_project(project_id: str) -> str:
    """Deterministic color assignment from a fixed palette."""
    h = hashlib.sha1(project_id.encode("utf-8")).digest()[0]
    return _PROJECT_COLOR_PALETTE[h % len(_PROJECT_COLOR_PALETTE)]


def _month_short(m: int) -> str:
    return ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
            "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"][m - 1]


def _month_full(m: int) -> str:
    return ["January", "February", "March", "April", "May", "June",
            "July", "August", "September", "October", "November", "December"][m - 1]


def get_deployment_history_data(
    db: Session,
    start_year: int,
    start_month: int,
) -> dict:
    """
    Build the calendar data structure for a 12-month window starting at
    (start_year, start_month).

    Returns:
        {
          "months": [
            {
              "year": int,
              "month": int,            # 1-12
              "name": "January",
              "short_name": "Jan",
              "year_short": "26",
              "num_days": int,
              "first_weekday": int,    # 0=Mon..6=Sun (datetime.weekday())
              "active_days": {
                  day_num: [project_id, project_id, ...]  # projects with
                                                          # ≥1 active assignment
                                                          # on that day
              },
            },
            ...                                          # 12 entries
          ],
          "projects": [
            {
              "id": str,
              "name": str,
              "color": str,
              "status": str,
              "client_name": str | None,
              "assignment_count": int,    # total assignments contributing to
                                          # this 12-month window
              "first_active":   "YYYY-MM-DD" | None,
              "last_active":    "YYYY-MM-DD" | None,
            },
            ...                                          # only projects with
                                                         # ≥1 assignment in the
                                                         # window, sorted by
                                                         # first_active ASC
          ],
          "total_assignments": int,
          "total_active_units": int,    # distinct unit_ids across the window
          "window": {
              "start_year": int,
              "start_month": int,
              "end_year": int,
              "end_month": int,
              "first_date": "YYYY-MM-DD",
              "last_date":  "YYYY-MM-DD",
          },
        }
    """
    # Compute window edges.
    first_date = date(start_year, start_month, 1)
    # 12 months → end on day-1 of (start + 12)
    end_year  = start_year + ((start_month + 10) // 12)
    end_month = ((start_month + 10) % 12) + 1
    last_date = date(end_year, end_month, monthrange(end_year, end_month)[1])

    now = datetime.utcnow()

    # Fetch every assignment that overlaps the window.  An assignment
    # overlaps if assigned_at <= last_date AND (assigned_until is NULL
    # OR assigned_until >= first_date).
    assignments = (
        db.query(UnitAssignment)
        .filter(UnitAssignment.assigned_at <= datetime.combine(last_date, datetime.max.time()))
        .filter(
            or_(
                UnitAssignment.assigned_until == None,   # noqa: E711 — active
                UnitAssignment.assigned_until >= datetime.combine(first_date, datetime.min.time()),
            )
        )
        .all()
    )

    # Resolve referenced projects in one query.
    proj_ids = {a.project_id for a in assignments}
    proj_map = {
        p.id: p for p in db.query(Project).filter(Project.id.in_(proj_ids)).all()
    } if proj_ids else {}

    # Resolve location names in one batch query (used by the Gantt view
    # for per-bar tooltips).
    from backend.models import MonitoringLocation
    loc_ids = {a.location_id for a in assignments}
    loc_name_map = {
        l.id: l.name for l in db.query(MonitoringLocation).filter(
            MonitoringLocation.id.in_(loc_ids)
        ).all()
    } if loc_ids else {}

    # Compute "active days per project" by walking each assignment and
    # adding every day in its [start, end] ∩ [first_date, last_date].
    # O(N_assignments × avg_window_days); for a typical fleet this is
    # bounded (hundreds of assignments × hundreds of days = manageable).
    # Also collect raw per-assignment bar data for the Gantt view.
    project_active_days: dict[str, set[date]] = {}
    project_first_active: dict[str, date] = {}
    project_last_active:  dict[str, date] = {}
    project_assignment_count: dict[str, int] = {}
    project_bars: dict[str, list[dict]] = {}
    distinct_units: set[str] = set()

    for a in assignments:
        start = max(a.assigned_at.date() if a.assigned_at else first_date, first_date)
        end_dt = a.assigned_until or now
        end   = min(end_dt.date(), last_date)
        if end < start:
            continue
        days = project_active_days.setdefault(a.project_id, set())
        d = start
        while d <= end:
            days.add(d)
            d += timedelta(days=1)
        project_assignment_count[a.project_id] = project_assignment_count.get(a.project_id, 0) + 1
        distinct_units.add(a.unit_id)
        # Track first/last active dates in the window.
        prev_first = project_first_active.get(a.project_id)
        if prev_first is None or start < prev_first:
            project_first_active[a.project_id] = start
        prev_last = project_last_active.get(a.project_id)
        if prev_last is None or end > prev_last:
            project_last_active[a.project_id] = end

        # Per-assignment bar data — used by the Gantt view's renderer.
        # `is_active` reflects whether the assignment_until was still NULL
        # at fetch time (open-ended deployment); the clipped `end` here
        # is just for visual bar drawing.
        project_bars.setdefault(a.project_id, []).append({
            "unit_id":       a.unit_id,
            "location_id":   a.location_id,
            "location_name": loc_name_map.get(a.location_id, "(unknown location)"),
            "start":         start.isoformat(),
            "end":           end.isoformat(),
            "is_active":     a.assigned_until is None,
            "source":        a.source,
        })

    # Build the projects array (sorted by first_active ascending so the
    # legend reads in deployment-order).
    projects_data = []
    for pid, days in project_active_days.items():
        p = proj_map.get(pid)
        if not p:
            # Assignment references a deleted project — surface it anyway
            # with a placeholder name, since the bars still need a label.
            projects_data.append({
                "id": pid,
                "name": "(deleted project)",
                "color": _color_for_project(pid),
                "status": "deleted",
                "client_name": None,
                "assignment_count": project_assignment_count.get(pid, 0),
                "first_active": project_first_active[pid].isoformat() if pid in project_first_active else None,
                "last_active":  project_last_active[pid].isoformat()  if pid in project_last_active  else None,
                "bars":         project_bars.get(pid, []),
            })
            continue
        projects_data.append({
            "id":     pid,
            "name":   p.name,
            "color":  _color_for_project(pid),
            "status": p.status or "active",
            "client_name":      p.client_name,
            "assignment_count": project_assignment_count.get(pid, 0),
            "first_active":     project_first_active[pid].isoformat() if pid in project_first_active else None,
            "last_active":      project_last_active[pid].isoformat()  if pid in project_last_active  else None,
            "bars":             project_bars.get(pid, []),
        })

    projects_data.sort(key=lambda p: (p["first_active"] or "9999", p["name"]))

    # ── Per-unit view data (Gantt-by-Unit tab) ────────────────────────
    # Same source assignments, re-grouped by unit_id.  Each bar carries
    # the project's color + name so the renderer can paint by job
    # without doing a second lookup.
    unit_bars: dict[str, list[dict]] = {}
    project_lookup = {p["id"]: p for p in projects_data}
    for a in assignments:
        start = max(a.assigned_at.date() if a.assigned_at else first_date, first_date)
        end_dt = a.assigned_until or now
        end   = min(end_dt.date(), last_date)
        if end < start:
            continue
        p_info = project_lookup.get(a.project_id, {})
        unit_bars.setdefault(a.unit_id, []).append({
            "project_id":    a.project_id,
            "project_name":  p_info.get("name", "(deleted project)"),
            "project_color": p_info.get("color", _color_for_project(a.project_id)),
            "location_id":   a.location_id,
            "location_name": loc_name_map.get(a.location_id, "(unknown location)"),
            "start":         start.isoformat(),
            "end":           end.isoformat(),
            "is_active":     a.assigned_until is None,
            "source":        a.source,
        })

    # Sort units by first-active date so the most-recently-deployed
    # units sit at the top.  Reverse if we want oldest-first.
    units_data = []
    for uid, bars in unit_bars.items():
        bars.sort(key=lambda b: b["start"])
        first_start = bars[0]["start"]
        # "active now" flag = any bar is still active
        any_active = any(b["is_active"] for b in bars)
        units_data.append({
            "id":              uid,
            "bars":            bars,
            "first_active":    first_start,
            "assignment_count": len(bars),
            "any_active":      any_active,
        })
    units_data.sort(key=lambda u: (not u["any_active"], u["first_active"], u["id"]))

    # Now build the months array.
    months_data = []
    cur_year, cur_month = start_year, start_month
    for _ in range(12):
        num_days = monthrange(cur_year, cur_month)[1]
        first_weekday = date(cur_year, cur_month, 1).weekday()   # 0=Mon..6=Sun
        active_days: dict[int, list[str]] = {}
        for day_num in range(1, num_days + 1):
            d = date(cur_year, cur_month, day_num)
            day_projects = [
                pid for pid, days in project_active_days.items()
                if d in days
            ]
            if day_projects:
                # Sort by the project's color-stable order so bars don't
                # jitter between days.
                day_projects.sort()
                active_days[day_num] = day_projects
        months_data.append({
            "year":         cur_year,
            "month":        cur_month,
            "name":         _month_full(cur_month),
            "short_name":   _month_short(cur_month),
            "year_short":   f"{cur_year % 100:02d}",
            "num_days":     num_days,
            "first_weekday": first_weekday,
            "active_days":  active_days,
        })
        # Advance one month.
        if cur_month == 12:
            cur_year += 1
            cur_month = 1
        else:
            cur_month += 1

    return {
        "months":   months_data,
        "projects": projects_data,
        "units":    units_data,
        "total_assignments":    len(assignments),
        "total_active_units":   len(distinct_units),
        "window": {
            "start_year":  start_year,
            "start_month": start_month,
            "end_year":    end_year,
            "end_month":   end_month,
            "first_date":  first_date.isoformat(),
            "last_date":   last_date.isoformat(),
        },
    }


def get_deployments_on_day(
    db: Session,
    target_date: date,
) -> list[dict]:
    """
    Return the list of (unit, location, project) tuples that were
    actively assigned on a specific calendar date.  Used for the
    day-detail side panel when an operator clicks a day cell.
    """
    from backend.models import MonitoringLocation, RosterUnit

    day_start = datetime.combine(target_date, datetime.min.time())
    day_end   = datetime.combine(target_date, datetime.max.time())

    rows = (
        db.query(UnitAssignment)
        .filter(UnitAssignment.assigned_at <= day_end)
        .filter(
            or_(
                UnitAssignment.assigned_until == None,   # noqa: E711
                UnitAssignment.assigned_until >= day_start,
            )
        )
        .order_by(UnitAssignment.project_id, UnitAssignment.unit_id)
        .all()
    )

    if not rows:
        return []

    loc_ids = {a.location_id for a in rows}
    proj_ids = {a.project_id for a in rows}
    loc_map = {
        l.id: l for l in db.query(MonitoringLocation).filter(
            MonitoringLocation.id.in_(loc_ids)
        ).all()
    }
    proj_map = {
        p.id: p for p in db.query(Project).filter(
            Project.id.in_(proj_ids)
        ).all()
    }

    results = []
    for a in rows:
        loc  = loc_map.get(a.location_id)
        proj = proj_map.get(a.project_id)
        results.append({
            "assignment_id":   a.id,
            "unit_id":         a.unit_id,
            "location_id":     a.location_id,
            "location_name":   loc.name  if loc  else "(unknown location)",
            "project_id":      a.project_id,
            "project_name":    proj.name if proj else "(deleted project)",
            "project_color":   _color_for_project(a.project_id),
            "assigned_at":     a.assigned_at.isoformat()    if a.assigned_at    else None,
            "assigned_until":  a.assigned_until.isoformat() if a.assigned_until else None,
            "is_active":       a.assigned_until is None,
            "source":          a.source,
        })

    return results
