import { ChevronRight } from "lucide-react";
import Link from "next/link";
import { DraftsCard } from "@/components/dashboard/DraftsCard";
import { Card, CARD_CLASS, CardTitle } from "@/components/ui/Card";
import { actionBadgeStyle } from "@/lib/colors";
import { addDaysIso, fmtDate, todayIso } from "@/lib/format";
import { divisionOf, loadOrgMaps } from "@/lib/org";
import { loadPlacements } from "@/lib/placement";
import { loadOpenPositions } from "@/lib/positions";
import { deriveStatusAsOf } from "@/lib/reports";
import { currentUserId } from "@/lib/auth/session";
import { withUser } from "@/lib/db";
import type { HistoryEventType } from "@/lib/supabase/types";

// Each KPI carries a colour already; the accent bar repeats it in a second
// channel so the tiles are scannable as a row rather than six identical
// boxes, and so the meaning does not rest on hue alone.
const TONE: Record<string, { text: string; bar: string }> = {
  default: { text: "text-ink", bar: "bg-ink-muted" },
  success: { text: "text-success-text", bar: "bg-success-text" },
  danger: { text: "text-danger-text", bar: "bg-danger-text" },
  warning: { text: "text-warning-text", bar: "bg-warning-text" },
  brand: { text: "text-brand-700", bar: "bg-brand-500" },
};

const DOT_STYLES: Record<string, string> = {
  Eintritt: "bg-success-text",
  Wiedereintritt: "bg-success-text",
  Rückkehr: "bg-success-text",
  Austritt: "bg-danger-text",
  Versetzung: "bg-info-text",
  Beförderung: "bg-purple-text",
  Reorganisation: "bg-purple-text",
  Karenz: "bg-warning-text",
  Vertragsänderung: "bg-warning-text",
  Stammdatenänderung: "bg-warning-text",
  Gehaltsanpassung: "bg-warning-text",
};

const KIND_LABEL = { hire: "Eintritt", exit: "Austritt", return: "Rückkehr aus Abwesenheit" } as const;

export default async function DashboardPage() {
  // Built as strings, not by round-tripping a local Date through
  // toISOString(): in any positive-offset zone new Date(year, 0, 1) is still
  // the previous year in UTC, which shifted the whole YTD window a day early
  // and dropped 31 December from it entirely.
  const today = todayIso();
  const year = today.slice(0, 4);
  const yearStart = `${year}-01-01`;
  const yearEnd = `${year}-12-31`;
  const in60Iso = addDaysIso(today, 60);

  const userId = await currentUserId();

  // Headcount, FTE, Karenz and the division bars all come from one full read
  // and the *derived* status, not from the `employees.status` column.
  //
  // That column only ever reflects what the last mutation or cron run wrote,
  // while every report derives status from entry/exit/karenz dates — so a
  // planned hire whose start date has passed, or a Karenz that ended without
  // anyone recording the return, made the dashboard and the Berichte page
  // disagree about the same headcount. Same derivation, same numbers.
  const {
    drafts,
    staffRows,
    hiresYtd,
    exitsYtd,
    openPositions,
    orgMaps,
    placements,
    upcomingHires,
    upcomingExits,
    upcomingReturns,
    history,
  } = await withUser(userId, async (tx) => {
    const countIn = (types: readonly HistoryEventType[]) =>
      tx
        .selectFrom("employee_history")
        .select(({ fn }) => fn.countAll<string>().as("anzahl"))
        .where("event_type", "in", [...types])
        .where("event_date", ">=", yearStart)
        .where("event_date", "<=", yearEnd)
        .executeTakeFirst();

    const [
      drafts,
      staffRows,
      hiresYtd,
      exitsYtd,
      openPositions,
      orgMaps,
      placements,
      upcomingHires,
      upcomingExits,
      upcomingReturns,
      history,
    ] = await Promise.all([
      userId
        ? tx
            .selectFrom("hire_drafts")
            .select(["id", "step", "payload", "updated_at"])
            .where("created_by", "=", userId)
            .orderBy("updated_at", "desc")
            .execute()
        : Promise.resolve([]),

      tx
        .selectFrom("employees")
        .select(["id", "weekly_hours", "entry_date", "exit_date", "karenz_start_date", "karenz_return_date"])
        .orderBy("id")
        .execute(),

      // Entries/exits count history events, which is what the linked report
      // counts too. `entry_date` would also sweep up rehires, whose event is
      // logged as 'Wiedereintritt' — the tile and its destination then showed
      // different numbers for the same year.
      countIn(["Eintritt", "Wiedereintritt"]),
      countIn(["Austritt"]),

      loadOpenPositions(tx),
      loadOrgMaps(tx),
      loadPlacements(tx, { asOf: today }),

      tx
        .selectFrom("employees")
        .select(["id", "first_name", "last_name", "entry_date"])
        .where("status", "=", "Geplant")
        .where("entry_date", ">=", today)
        .where("entry_date", "<=", in60Iso)
        .execute(),

      tx
        .selectFrom("employees")
        .select(["id", "first_name", "last_name", "exit_date"])
        .where("exit_date", "is not", null)
        .where("exit_date", ">=", today)
        .where("exit_date", "<=", in60Iso)
        .execute(),

      tx
        .selectFrom("employees")
        .select(["id", "first_name", "last_name", "karenz_return_date"])
        .where("status", "=", "Karenz")
        .where("karenz_return_date", "is not", null)
        .where("karenz_return_date", ">=", today)
        .where("karenz_return_date", "<=", in60Iso)
        .execute(),

      tx
        .selectFrom("employee_history as h")
        .leftJoin("employees as e", "e.id", "h.employee_id")
        .select(["h.id", "h.employee_id", "h.event_date", "h.event_type", "h.description", "e.first_name", "e.last_name"])
        .orderBy("h.event_date", "desc")
        .orderBy("h.created_at", "desc")
        .limit(10)
        .execute(),
    ]);

    return {
      drafts,
      staffRows,
      hiresYtd: Number(hiresYtd?.anzahl ?? 0),
      exitsYtd: Number(exitsYtd?.anzahl ?? 0),
      openPositions,
      orgMaps,
      placements,
      upcomingHires,
      upcomingExits,
      upcomingReturns,
      history,
    };
  });

  // "Aktiv" means status Aktiv — somebody on Karenz is employed but not
  // active, and is counted by its own tile instead. FTE follows the same
  // set: Karenz contributes no capacity, so including it would overstate
  // what the company can actually staff.
  //
  // Note this is narrower than DEFAULT_STATUSES in lib/reports (Aktiv +
  // Karenz), which still governs what the Berichte page shows when no
  // status filter is chosen.
  const statusOf = (row: (typeof staffRows)[number]) => deriveStatusAsOf(row, today);
  const activeStaff = staffRows.filter((row) => statusOf(row) === "Aktiv");
  const activeCount = activeStaff.length;
  const karenzCount = staffRows.filter((row) => statusOf(row) === "Karenz").length;
  const fte = activeStaff.reduce((sum, row) => sum + Number(row.weekly_hours), 0) / 38.5;

  // Der Bereich einer Person steht nicht mehr auf ihr; er ergibt sich aus der
  // Einheit ihrer Planstelle und deren Vorfahren. Die Bereichsleitung selbst
  // sitzt *am* Bereich, ihre Leute darunter — beide landen über die
  // Vorfahrenkette im selben Balken.
  const headcountByDivision = new Map<string, number>();
  for (const row of activeStaff) {
    const division = divisionOf(orgMaps, placements.get(row.id)?.orgUnitId);
    if (!division) continue;
    headcountByDivision.set(division.id, (headcountByDivision.get(division.id) ?? 0) + 1);
  }
  const divisionBars = orgMaps.unitList
    .filter((u) => u.unit_type === "Bereich")
    .map((d) => ({ name: d.name, count: headcountByDivision.get(d.id) ?? 0 }))
    .sort((a, b) => b.count - a.count);
  const maxDivisionCount = Math.max(1, ...divisionBars.map((d) => d.count));

  type UpcomingItem = { id: string; label: string; date: string; kind: keyof typeof KIND_LABEL };
  const upcoming: UpcomingItem[] = [
    ...(upcomingHires).map((e) => ({
      id: e.id,
      label: `${e.first_name} ${e.last_name}`,
      date: e.entry_date,
      kind: "hire" as const,
    })),
    ...(upcomingExits).map((e) => ({
      id: e.id,
      label: `${e.first_name} ${e.last_name}`,
      date: e.exit_date!,
      kind: "exit" as const,
    })),
    ...(upcomingReturns).map((e) => ({
      id: e.id,
      label: `${e.first_name} ${e.last_name}`,
      date: e.karenz_return_date!,
      kind: "return" as const,
    })),
  ]
    .sort((a, b) => a.date.localeCompare(b.date))
    .slice(0, 8);

  // Each tile links to the view that shows what it counts, with the filters
  // pre-applied.
  //
  // Two of them cannot match exactly, and it is worth knowing which: the
  // headcount tiles filter `employees` and their targets filter the same
  // table, so those agree. Eintritte/Austritte count `employees.entry_date`
  // / `exit_date`, while the events report counts `employee_history` rows —
  // and rehire_employee sets entry_date but logs the event as
  // 'Wiedereintritt'. A year with rehires therefore shows a slightly higher
  // number on the tile than in the linked report.
  const kpis = [
    {
      label: "Aktive Mitarbeiter:innen",
      value: activeCount,
      tone: "default",
      href: "/employees?status=Aktiv",
    },
    { label: "FTE", value: fte.toFixed(1), tone: "default", href: "/reports?mode=snapshot&measure=fte&status=Aktiv" },
    {
      label: "Eintritte (Jahr)",
      value: hiresYtd,
      tone: "success",
      href: `/reports?mode=events&eventType=Eintritt&from=${yearStart}&to=${yearEnd}`,
    },
    {
      label: "Austritte (Jahr)",
      value: exitsYtd,
      tone: "danger",
      href: `/reports?mode=events&eventType=Austritt&from=${yearStart}&to=${yearEnd}`,
    },
    { label: "Langzeitabwesend", value: karenzCount, tone: "warning", href: "/employees?status=Karenz" },
    { label: "Offene Positionen", value: openPositions.length, tone: "brand", href: "/positions" },
  ];

  return (
    <div className="flex flex-col gap-6">
      {drafts && drafts.length > 0 && <DraftsCard drafts={drafts} />}
      <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
        {kpis.map((kpi) => (
          <Link
            key={kpi.label}
            href={kpi.href}
            className={`${CARD_CLASS} group relative overflow-hidden p-4 pl-5 transition-shadow hover:shadow-[var(--shadow-card-hover)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand-500`}
          >
            <span className={`absolute inset-y-0 left-0 w-1 ${TONE[kpi.tone].bar}`} aria-hidden />
            {/* Number first in the visual order: it is what the eye is
                looking for, and the label only qualifies it. */}
            <div className={`text-3xl font-extrabold leading-none tabular-nums ${TONE[kpi.tone].text}`}>{kpi.value}</div>
            <div className="mt-1.5 flex items-center gap-1 text-xs font-semibold leading-tight text-ink-muted">
              {kpi.label}
              <ChevronRight className="h-3 w-3 shrink-0 opacity-0 transition-opacity group-hover:opacity-100" aria-hidden />
            </div>
          </Link>
        ))}
      </div>

      <div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
        <Card>
          {/* Aktive, not headcount: the bars count the same set as the tile
              above them, which excludes Karenz. */}
          <CardTitle className="mb-3">Aktive nach Bereich</CardTitle>
          <div className="flex flex-col gap-2.5">
            {divisionBars.map((d) => (
              <div key={d.name}>
                <div className="mb-1 flex justify-between text-xs">
                  <span className="text-ink-body">{d.name}</span>
                  <span className="font-semibold tabular-nums text-ink">{d.count}</span>
                </div>
                {/* Rounded ends and a minimum width so the smallest division
                    still reads as a bar rather than a stray pixel. */}
                <div className="h-1.5 overflow-hidden rounded-full bg-surface">
                  <div
                    className="h-full rounded-full bg-brand-500"
                    style={{ width: `${Math.max(2, (d.count / maxDivisionCount) * 100)}%` }}
                  />
                </div>
              </div>
            ))}
            {divisionBars.length === 0 && <p className="text-sm text-ink-muted">Keine Daten vorhanden.</p>}
          </div>
        </Card>

        <Card>
          <CardTitle className="mb-1">Anstehend (60 Tage)</CardTitle>
          <ul className="flex flex-col divide-y divide-border-subtle">
            {upcoming.map((item) => (
              <li key={`${item.kind}-${item.id}`}>
                <Link
                  href={`/employees/${item.id}`}
                  className="-mx-2 flex items-center justify-between gap-2 rounded px-2 py-2.5 text-sm hover:bg-surface"
                >
                  <span className="min-w-0">
                    <span className="block truncate font-semibold text-ink">{item.label}</span>
                    <span className="text-xs text-ink-muted">{KIND_LABEL[item.kind]}</span>
                  </span>
                  <span className="shrink-0 text-xs font-semibold tabular-nums text-ink-muted">{fmtDate(item.date)}</span>
                </Link>
              </li>
            ))}
            {upcoming.length === 0 && <p className="py-2 text-sm text-ink-muted">Keine anstehenden Ereignisse.</p>}
          </ul>
        </Card>

        <Card>
          <CardTitle className="mb-1">Letzte Aktivitäten</CardTitle>
          <ul className="flex flex-col divide-y divide-border-subtle">
            {(history).map((h) => (
              <li key={h.id} className="flex gap-2.5 py-2.5">
                {/* Dot aligned to the first line of text, not centred on the
                    whole row, so it stays put as descriptions wrap. */}
                <span className={`mt-1.5 h-2 w-2 shrink-0 rounded-full ${DOT_STYLES[h.event_type] ?? "bg-ink-muted"}`} aria-hidden />
                <div className="min-w-0 flex-1">
                  <div className="flex flex-wrap items-center gap-x-2 gap-y-1">
                    <span className="text-sm font-semibold text-ink">{h.first_name && h.last_name ? `${h.first_name} ${h.last_name}` : "Unbekannt"}</span>
                    <span className={`rounded-full px-2 py-0.5 text-[11px] font-semibold ${actionBadgeStyle(h.event_type)}`}>
                      {h.event_type}
                    </span>
                  </div>
                  <p className="mt-0.5 text-xs leading-relaxed text-ink-muted">{h.description}</p>
                </div>
              </li>
            ))}
            {(history).length === 0 && <p className="py-2 text-sm text-ink-muted">Keine Aktivitäten vorhanden.</p>}
          </ul>
        </Card>
      </div>
    </div>
  );
}
