import { db } from "@/lib/db";
import { POST_APPROVAL_STATUSES } from "@/lib/utils";

/**
 * Spend reporting (Reports → Purchasing). Aggregates approved purchase-order
 * spend across two dimensions:
 *   • Cost centres  — the PO's vessel (`PurchaseOrder.vesselId`).
 *   • Accounting codes — the self-referential `Account` tree (Heading →
 *     Sub-heading → Leaf); each PO's `accountId` is a leaf, rolled up to parents.
 *
 * "Spend" = a PO that has reached manager approval (`POST_APPROVAL_STATUSES`),
 * dated by `approvedAt` and valued at the full `totalAmount` — the same
 * definition the dashboard's spend tiles use. Financial year is the Indian
 * Apr–Mar year. The heavy lifting is a single query in `getReportDataset()`;
 * everything below is pure functions over that dataset so they're unit-testable.
 */

export const FY_MONTHS = ["Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan", "Feb", "Mar"] as const;

/** Indian FY start year for a date (Apr–Mar): Jan–Mar belong to the prior year. */
export function fyStartYear(d: Date): number {
  return d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
}
/** "FY 2025–26" for start year 2025. */
export function fyLabel(start: number): string {
  return `FY ${start}–${String((start + 1) % 100).padStart(2, "0")}`;
}
/** Month index within the FY: Apr=0 … Mar=11. */
export function fyMonthIndex(d: Date): number {
  return (d.getMonth() - 3 + 12) % 12;
}
/** Week-of-month bucket: 0–4 (W1–W5) from the day of the month. */
export function weekOfMonth(d: Date): number {
  return Math.min(4, Math.floor((d.getDate() - 1) / 7));
}
export const WEEK_LABELS = ["W1", "W2", "W3", "W4", "W5"] as const;

export type Tier = "Heading" | "Sub-heading" | "Leaf";

export interface CostCentre {
  id: string;
  code: string;
  name: string;
}
export interface AccountNode {
  id: string;
  code: string;
  name: string;
  parentId: string | null;
  tier: Tier;
}
/** One row per (PO, accounting code). Multi-account POs yield several rows. */
export interface SpendRow {
  poId: string;
  vesselId: string;
  accountId: string;
  amount: number;
  fy: number;
  month: number; // 0–11 within the FY (Apr=0)
  week: number; // 0–4 within the calendar month
}

/**
 * Split a PO's spend across the accounting codes its line items carry, so the
 * accounting-code report attributes multi-account POs correctly. The PO's
 * `totalAmount` is allocated **proportionally** to each line's account share
 * (line `accountId`, falling back to the PO-level account), so the per-PO rows
 * always sum back to `totalAmount` exactly. With no line items (or zero line
 * value) the whole amount lands on the PO-level account.
 */
export function allocatePoSpend(
  po: { id: string; vesselId: string; accountId: string; amount: number; fy: number; month: number; week: number },
  lines: { accountId: string | null; amount: number }[]
): SpendRow[] {
  const base = { poId: po.id, vesselId: po.vesselId, fy: po.fy, month: po.month, week: po.week };
  const byAccount = new Map<string, number>();
  let lineTotal = 0;
  for (const l of lines) {
    const key = l.accountId ?? po.accountId;
    byAccount.set(key, (byAccount.get(key) ?? 0) + l.amount);
    lineTotal += l.amount;
  }
  if (byAccount.size === 0 || lineTotal <= 0) {
    return [{ ...base, accountId: po.accountId, amount: po.amount }];
  }
  return [...byAccount.entries()].map(([accountId, share]) => ({ ...base, accountId, amount: po.amount * (share / lineTotal) }));
}

export interface ReportDataset {
  rows: SpendRow[];
  vessels: CostCentre[];
  accounts: AccountNode[];
  fys: number[]; // ascending FYs that have spend (falls back to the current FY)
}

/** Pull every approved PO and the cost-centre / accounting-code reference data. */
export async function getReportDataset(): Promise<ReportDataset> {
  const [pos, vessels, accounts] = await Promise.all([
    db.purchaseOrder.findMany({
      where: { status: { in: [...POST_APPROVAL_STATUSES] }, approvedAt: { not: null } },
      select: {
        id: true,
        vesselId: true,
        accountId: true,
        totalAmount: true,
        approvedAt: true,
        lineItems: { select: { accountId: true, totalPrice: true } },
      },
    }),
    db.vessel.findMany({ select: { id: true, code: true, name: true }, orderBy: { name: "asc" } }),
    db.account.findMany({ select: { id: true, code: true, name: true, parentId: true } }),
  ]);

  const childCount = new Map<string, number>();
  for (const a of accounts) if (a.parentId) childCount.set(a.parentId, (childCount.get(a.parentId) ?? 0) + 1);
  const accountNodes: AccountNode[] = accounts.map((a) => ({
    id: a.id,
    code: a.code,
    name: a.name,
    parentId: a.parentId,
    tier: a.parentId === null ? "Heading" : (childCount.get(a.id) ?? 0) > 0 ? "Sub-heading" : "Leaf",
  }));

  const rows: SpendRow[] = [];
  for (const po of pos) {
    if (!po.approvedAt) continue;
    const meta = {
      id: po.id,
      vesselId: po.vesselId,
      accountId: po.accountId,
      amount: Number(po.totalAmount),
      fy: fyStartYear(po.approvedAt),
      month: fyMonthIndex(po.approvedAt),
      week: weekOfMonth(po.approvedAt),
    };
    rows.push(...allocatePoSpend(meta, po.lineItems.map((l) => ({ accountId: l.accountId, amount: Number(l.totalPrice) }))));
  }

  const fySet = new Set(rows.map((r) => r.fy));
  const fys = fySet.size ? [...fySet].sort((a, b) => a - b) : [fyStartYear(new Date())];

  return { rows, vessels, accounts: accountNodes, fys };
}

// ── Account tree helpers ───────────────────────────────────────────────────

export interface AccountIndex {
  byId: Map<string, AccountNode>;
  childrenOf: (parentId: string | null) => AccountNode[];
  leavesUnder: (id: string) => Set<string>;
  isLeaf: (id: string) => boolean;
  pathTo: (id: string) => AccountNode[];
}

export function buildAccountIndex(accounts: AccountNode[]): AccountIndex {
  const byId = new Map(accounts.map((a) => [a.id, a]));
  const kids = new Map<string | null, AccountNode[]>();
  for (const a of accounts) {
    const k = a.parentId;
    if (!kids.has(k)) kids.set(k, []);
    kids.get(k)!.push(a);
  }
  const childrenOf = (parentId: string | null) => kids.get(parentId) ?? [];
  const isLeaf = (id: string) => childrenOf(id).length === 0;

  const leafCache = new Map<string, Set<string>>();
  function leavesUnder(id: string): Set<string> {
    const cached = leafCache.get(id);
    if (cached) return cached;
    const out = new Set<string>();
    const children = childrenOf(id);
    if (children.length === 0) out.add(id);
    else for (const c of children) for (const lf of leavesUnder(c.id)) out.add(lf);
    leafCache.set(id, out);
    return out;
  }
  function pathTo(id: string): AccountNode[] {
    const node = byId.get(id);
    if (!node) return [];
    return node.parentId ? [...pathTo(node.parentId), node] : [node];
  }
  return { byId, childrenOf, leavesUnder, isLeaf, pathTo };
}

// ── Aggregations ───────────────────────────────────────────────────────────

export interface CostCentreSpend {
  id: string;
  code: string;
  name: string;
  total: number; // selected FY
  months: number[]; // 12 (Apr–Mar) of the selected FY
  poCount: number; // selected FY
  fyTotals: number[]; // aligned to ds.fys
}

export function costCentreRows(ds: ReportDataset, fy: number): CostCentreSpend[] {
  const idx = new Map<string, CostCentreSpend>();
  const poSets = new Map<string, Set<string>>(); // distinct POs per vessel in the selected FY
  for (const v of ds.vessels) {
    idx.set(v.id, { id: v.id, code: v.code, name: v.name, total: 0, months: Array(12).fill(0), poCount: 0, fyTotals: Array(ds.fys.length).fill(0) });
    poSets.set(v.id, new Set());
  }
  for (const r of ds.rows) {
    const row = idx.get(r.vesselId);
    if (!row) continue;
    const fi = ds.fys.indexOf(r.fy);
    if (fi >= 0) row.fyTotals[fi] += r.amount;
    if (r.fy === fy) {
      row.months[r.month] += r.amount;
      row.total += r.amount;
      poSets.get(r.vesselId)!.add(r.poId);
    }
  }
  for (const [id, set] of poSets) idx.get(id)!.poCount = set.size;
  return [...idx.values()];
}

/** Weekly buckets (W1–W5) of one FY month for a cost centre. */
export function costCentreWeekly(ds: ReportDataset, vesselId: string, fy: number, month: number): number[] {
  const weeks = Array(5).fill(0);
  for (const r of ds.rows) if (r.vesselId === vesselId && r.fy === fy && r.month === month) weeks[r.week] += r.amount;
  return weeks;
}

/** Spend for an account node (rolls leaf descendants up) in a FY: total + 12 months + per-FY totals. */
export function accountNodeSpend(ds: ReportDataset, idx: AccountIndex, nodeId: string, fy: number) {
  const leaves = idx.leavesUnder(nodeId);
  const months = Array(12).fill(0);
  const fyTotals = Array(ds.fys.length).fill(0);
  const poSet = new Set<string>();
  let total = 0;
  for (const r of ds.rows) {
    if (!leaves.has(r.accountId)) continue;
    const fi = ds.fys.indexOf(r.fy);
    if (fi >= 0) fyTotals[fi] += r.amount;
    if (r.fy === fy) {
      months[r.month] += r.amount;
      total += r.amount;
      poSet.add(r.poId);
    }
  }
  return { total, months, fyTotals, poCount: poSet.size };
}

/** Weekly buckets (W1–W5) of one FY month for an account node (rolls leaves up). */
export function accountNodeWeekly(ds: ReportDataset, idx: AccountIndex, nodeId: string, fy: number, month: number): number[] {
  const leaves = idx.leavesUnder(nodeId);
  const weeks = Array(5).fill(0);
  for (const r of ds.rows) if (leaves.has(r.accountId) && r.fy === fy && r.month === month) weeks[r.week] += r.amount;
  return weeks;
}

export interface NodeSpend {
  node: AccountNode;
  total: number;
  months: number[];
  fyTotals: number[];
  poCount: number;
}

/** The accounting-code nodes to compare at a drill level (children of `parentId`; null = top headings). */
export function accountLevelRows(ds: ReportDataset, idx: AccountIndex, parentId: string | null, fy: number): NodeSpend[] {
  return idx.childrenOf(parentId).map((node) => ({ node, ...accountNodeSpend(ds, idx, node.id, fy) }));
}

export interface Breakdown {
  id: string;
  label: string;
  value: number;
}

/** For a cost centre detail: spend on each accounting code of `tier`, this FY. */
export function topAccountsForCostCentre(ds: ReportDataset, idx: AccountIndex, vesselId: string, fy: number, tier: Tier): Breakdown[] {
  return ds.accounts
    .filter((a) => a.tier === tier)
    .map((a) => {
      const leaves = idx.leavesUnder(a.id);
      let value = 0;
      for (const r of ds.rows) if (r.fy === fy && r.vesselId === vesselId && leaves.has(r.accountId)) value += r.amount;
      return { id: a.id, label: `${a.code} · ${a.name}`, value };
    })
    .filter((b) => b.value > 0)
    .sort((a, b) => b.value - a.value);
}

/** For an account-node detail: which cost centres drive its spend, this FY. */
export function costCentresForAccount(ds: ReportDataset, idx: AccountIndex, nodeId: string, fy: number): Breakdown[] {
  const leaves = idx.leavesUnder(nodeId);
  const byVessel = new Map<string, number>();
  for (const r of ds.rows) if (r.fy === fy && leaves.has(r.accountId)) byVessel.set(r.vesselId, (byVessel.get(r.vesselId) ?? 0) + r.amount);
  return ds.vessels
    .map((v) => ({ id: v.id, label: v.name, value: byVessel.get(v.id) ?? 0 }))
    .filter((b) => b.value > 0)
    .sort((a, b) => b.value - a.value);
}

/** For a non-leaf account-node detail: spend split across its direct children, this FY. */
export function childBreakdown(ds: ReportDataset, idx: AccountIndex, nodeId: string, fy: number): Breakdown[] {
  return idx
    .childrenOf(nodeId)
    .map((c) => ({ id: c.id, label: `${c.code} · ${c.name}`, value: accountNodeSpend(ds, idx, c.id, fy).total }))
    .filter((b) => b.value > 0)
    .sort((a, b) => b.value - a.value);
}

// ── Scope (Top N / Bottom N) ───────────────────────────────────────────────

export type ScopeMode = "top5" | "top10" | "bottom5" | "all";
export const SCOPE_LABELS: Record<ScopeMode, string> = { top5: "Top 5", top10: "Top 10", bottom5: "Bottom 5", all: "All" };

/** Apply a Top/Bottom-N scope to rows already sorted by spend descending. */
export function applyScope<T>(sortedDesc: T[], scope: ScopeMode): T[] {
  if (scope === "top5") return sortedDesc.slice(0, 5);
  if (scope === "top10") return sortedDesc.slice(0, 10);
  if (scope === "bottom5") return sortedDesc.slice(-5).reverse();
  return sortedDesc;
}

export function parseScope(v: string | undefined): ScopeMode {
  return v === "top10" || v === "bottom5" || v === "all" ? v : "top5";
}
export type Granularity = "yearly" | "monthly" | "weekly";
export function parseGranularity(v: string | undefined): Granularity {
  return v === "yearly" || v === "weekly" ? v : "monthly";
}
/** Resolve the selected FY from a query param against the available FYs (default: latest). */
export function resolveFy(ds: ReportDataset, v: string | undefined): number {
  const n = v ? Number(v) : NaN;
  if (Number.isFinite(n) && ds.fys.includes(n)) return n;
  return ds.fys[ds.fys.length - 1];
}
/** Resolve the FY-month index (0–11) for weekly mode (default: latest month with spend, else 0). */
export function resolveMonth(ds: ReportDataset, fy: number, v: string | undefined): number {
  const n = v ? Number(v) : NaN;
  if (Number.isFinite(n) && n >= 0 && n <= 11) return n;
  let last = 0;
  for (const r of ds.rows) if (r.fy === fy && r.month > last) last = r.month;
  return last;
}

/** Parse the `?sel=id1,id2` custom-comparison selection into an ordered, de-duped id list. */
export function parseSel(v: string | undefined): string[] {
  if (!v) return [];
  const seen = new Set<string>();
  for (const id of v.split(",")) if (id.trim()) seen.add(id.trim());
  return [...seen];
}
/** Toggle an id within a selection list (for the checkbox links). */
export function toggleSel(sel: string[], id: string): string[] {
  return sel.includes(id) ? sel.filter((x) => x !== id) : [...sel, id];
}
