import {
  computeEvenSpread,
  distributeHoursToWeeks,
  summarizeEstimateDemandLines,
} from "@capakraken/engine";
import { fmtEur } from "../lib/format-utils.js";
import type { ProjectGraphSnapshot } from "./computation-graph-project-snapshot.js";
import { type GraphLink, type GraphNode, fmtNum, l, n } from "./computation-graph-shared.js";

type ProjectEstimateGraphInput = {
  project: ProjectGraphSnapshot["project"];
  latestVersion: ProjectGraphSnapshot["latestVersion"];
  effortRuleCount: number;
  experienceRuleCount: number;
};

export function buildProjectEstimateGraph(input: ProjectEstimateGraphInput): {
  estimatedCostCents: number | null;
  links: GraphLink[];
  nodes: GraphNode[];
} {
  const { project, latestVersion, effortRuleCount, experienceRuleCount } = input;
  const nodes: GraphNode[] = [];
  const links: GraphLink[] = [];

  if (!latestVersion || latestVersion.demandLines.length === 0) {
    return {
      estimatedCostCents: null,
      nodes,
      links,
    };
  }

  const lines = latestVersion.demandLines;
  const summary = summarizeEstimateDemandLines(lines);
  const { totalHours, totalCostCents, totalPriceCents, marginCents, marginPercent: marginPct } = summary;

  const avgCostRate = totalHours > 0 ? Math.round(totalCostCents / totalHours) : 0;
  const avgBillRate = totalHours > 0 ? Math.round(totalPriceCents / totalHours) : 0;

  const chapterMap = new Map<string, number>();
  for (const demandLine of lines) {
    const chapter = demandLine.chapter ?? "(none)";
    chapterMap.set(chapter, (chapterMap.get(chapter) ?? 0) + demandLine.hours);
  }
  const chapterCount = chapterMap.size;
  const snapshotCount = latestVersion.resourceSnapshots?.length ?? 0;

  nodes.push(
    n("input.estLines", "Demand Lines", `${lines.length}`, "count", "INPUT", "Estimate demand line count", 0),
    n("input.avgCostRate", "Avg Cost Rate", fmtEur(avgCostRate), "cents/h", "INPUT", "Average cost rate across demand lines", 0),
    n("input.avgBillRate", "Avg Bill Rate", fmtEur(avgBillRate), "cents/h", "INPUT", "Average bill rate across demand lines", 0),
    ...(snapshotCount > 0 ? [
      n("input.resourceSnapshots", "Res. Snapshots", `${snapshotCount}`, "count", "INPUT", "Resource rate snapshots frozen in estimate version", 0),
    ] : []),
    n("est.totalHours", "Est. Hours", fmtNum(totalHours), "hours", "ESTIMATE", "Total estimated hours", 2, "Σ(line.hours)"),
    n("est.totalCostCents", "Est. Cost", fmtEur(totalCostCents), "EUR", "ESTIMATE", "Total estimated cost", 2, "Σ(hours × costRate)"),
    n("est.totalPriceCents", "Est. Price", fmtEur(totalPriceCents), "EUR", "ESTIMATE", "Total estimated price", 2, "Σ(hours × billRate)"),
    n("est.marginCents", "Margin", fmtEur(marginCents), "EUR", "ESTIMATE", "Price minus cost", 3, "price - cost"),
    n("est.marginPercent", "Margin %", `${marginPct.toFixed(1)}%`, "%", "ESTIMATE", "Margin as percentage of price", 3, "margin / price × 100"),
    ...(chapterCount > 1 ? [
      n("est.chapters", "Chapters", `${chapterCount}`, "count", "ESTIMATE", `Demand lines grouped by ${chapterCount} chapters`, 1),
    ] : []),
  );

  links.push(
    l("input.estLines", "est.totalHours", "Σ hours", 1),
    l("input.avgCostRate", "est.totalCostCents", "× hours", 2),
    l("est.totalHours", "est.totalCostCents", "× costRate", 2),
    l("input.avgBillRate", "est.totalPriceCents", "× hours", 2),
    l("est.totalHours", "est.totalPriceCents", "× billRate", 2),
    l("est.totalPriceCents", "est.marginCents", "−", 2),
    l("est.totalCostCents", "est.marginCents", "−", 2),
    l("est.marginCents", "est.marginPercent", "÷ price × 100", 2),
    l("est.totalPriceCents", "est.marginPercent", "÷", 1),
    ...(snapshotCount > 0 ? [
      l("input.resourceSnapshots", "input.avgCostRate", "LCR snapshot", 1),
      l("input.resourceSnapshots", "input.avgBillRate", "UCR snapshot", 1),
    ] : []),
    ...(chapterCount > 1 ? [
      l("input.estLines", "est.chapters", "group by", 1),
      l("est.chapters", "est.totalHours", "Σ per chapter", 1),
    ] : []),
  );

  const scopeItems = latestVersion.scopeItems ?? [];
  if (scopeItems.length > 0) {
    const totalFrameCount = scopeItems.reduce((sum, scopeItem) => sum + (scopeItem.frameCount ?? 0), 0);
    const totalItemCount = scopeItems.reduce((sum, scopeItem) => sum + (scopeItem.itemCount ?? 0), 0);
    const scopeTypes = new Set(scopeItems.map((scopeItem) => scopeItem.scopeType));

    nodes.push(
      n("effort.scopeItems", "Scope Items", `${scopeItems.length}`, "count", "EFFORT", `${scopeItems.length} scope items across ${scopeTypes.size} type(s)`, 0),
      ...(totalFrameCount > 0 ? [
        n("effort.totalFrames", "Total Frames", `${totalFrameCount}`, "frames", "EFFORT", "Sum of frame counts across scope items", 1),
      ] : []),
      ...(totalItemCount > 0 ? [
        n("effort.totalItems", "Total Items", fmtNum(totalItemCount), "items", "EFFORT", "Sum of item counts across scope items", 1),
      ] : []),
      n("effort.effortRules", "Effort Rules", `${effortRuleCount}`, "count", "EFFORT", "Configured effort expansion rules (scopeType → discipline)", 0),
      n("effort.expandedHours", "Expanded Hours", fmtNum(totalHours), "hours", "EFFORT", "Total hours from scope-to-effort expansion (unitCount × hoursPerUnit)", 2, "Σ(unitCount × hoursPerUnit)"),
    );

    links.push(
      l("effort.scopeItems", "effort.expandedHours", "expand", 2),
      l("effort.effortRules", "effort.expandedHours", "× hoursPerUnit", 2),
      ...(totalFrameCount > 0 ? [
        l("effort.scopeItems", "effort.totalFrames", "Σ frames", 1),
        l("effort.totalFrames", "effort.expandedHours", "per_frame", 1),
      ] : []),
      ...(totalItemCount > 0 ? [
        l("effort.scopeItems", "effort.totalItems", "Σ items", 1),
        l("effort.totalItems", "effort.expandedHours", "per_item", 1),
      ] : []),
      l("effort.expandedHours", "est.totalHours", "→ demand lines", 2),
    );
  }

  if (experienceRuleCount > 0) {
    nodes.push(
      n("exp.ruleCount", "Exp. Rules", `${experienceRuleCount}`, "count", "EXPERIENCE", "Experience multiplier rules (chapter/location/level → rate adjustments)", 0),
      n("exp.costMultiplier", "Cost Multiplier", "per rule", "×", "EXPERIENCE", "Multiplier applied to cost rate (costRateCents × multiplier)", 1, "costRate × costMultiplier"),
      n("exp.billMultiplier", "Bill Multiplier", "per rule", "×", "EXPERIENCE", "Multiplier applied to bill rate (billRateCents × multiplier)", 1, "billRate × billMultiplier"),
      n("exp.shoringRatio", "Shoring Ratio", "per rule", "ratio", "EXPERIENCE", "Offshore/nearshore effort factor (onsiteHours + offshoreHours × (1 + additionalEffort))", 2, "onsite + offshore × (1 + addlEffort)"),
      n("exp.adjustedRates", "Adjusted Rates", "applied", "—", "EXPERIENCE", "Final cost and bill rates after experience multipliers", 2, "rate × multiplier"),
    );

    links.push(
      l("exp.ruleCount", "exp.costMultiplier", "match rule", 1),
      l("exp.ruleCount", "exp.billMultiplier", "match rule", 1),
      l("exp.ruleCount", "exp.shoringRatio", "match rule", 1),
      l("exp.costMultiplier", "exp.adjustedRates", "×", 2),
      l("exp.billMultiplier", "exp.adjustedRates", "×", 2),
      l("exp.shoringRatio", "exp.adjustedRates", "adjust hours", 1),
      l("exp.adjustedRates", "est.totalCostCents", "→ costRate", 1),
      l("exp.adjustedRates", "est.totalPriceCents", "→ billRate", 1),
    );
  }

  const terms = latestVersion.commercialTerms as {
    contingencyPercent?: number;
    discountPercent?: number;
    pricingModel?: string;
    paymentTermDays?: number;
    warrantyMonths?: number;
    paymentMilestones?: Array<{ label: string; percent: number; dueDate?: string | null }>;
  } | null;

  const hasCommercialAdjustments = terms && (terms.contingencyPercent || terms.discountPercent);
  const hasCommercialMeta = terms && (terms.pricingModel || terms.paymentTermDays || terms.warrantyMonths);

  if (hasCommercialAdjustments) {
    const contingencyPct = terms!.contingencyPercent ?? 0;
    const discountPct = terms!.discountPercent ?? 0;
    const contingencyCents = Math.round((totalCostCents * contingencyPct) / 100);
    const discountCents = Math.round((totalPriceCents * discountPct) / 100);
    const adjustedCost = totalCostCents + contingencyCents;
    const adjustedPrice = totalPriceCents - discountCents;
    const adjustedMargin = adjustedPrice - adjustedCost;
    const adjustedMarginPct = adjustedPrice > 0 ? (adjustedMargin / adjustedPrice) * 100 : 0;

    nodes.push(
      n("input.contingencyPct", "Contingency %", `${contingencyPct}%`, "%", "INPUT", "Contingency percentage (risk buffer on cost)", 0),
      n("input.discountPct", "Discount %", `${discountPct}%`, "%", "INPUT", "Discount percentage (reduction on sell side)", 0),
      n("comm.contingencyCents", "Contingency", fmtEur(contingencyCents), "EUR", "COMMERCIAL", "Contingency surcharge", 2, "baseCost × contingency%"),
      n("comm.discountCents", "Discount", fmtEur(discountCents), "EUR", "COMMERCIAL", "Discount deduction", 2, "basePrice × discount%"),
      n("comm.adjustedCost", "Adj. Cost", fmtEur(adjustedCost), "EUR", "COMMERCIAL", "Cost plus contingency", 3, "baseCost + contingency"),
      n("comm.adjustedPrice", "Adj. Price", fmtEur(adjustedPrice), "EUR", "COMMERCIAL", "Price minus discount", 3, "basePrice - discount"),
      n("comm.adjustedMargin", "Adj. Margin", fmtEur(adjustedMargin), "EUR", "COMMERCIAL", "Adjusted margin", 3, "adjPrice - adjCost"),
      n("comm.adjustedMarginPct", "Adj. Margin %", `${adjustedMarginPct.toFixed(1)}%`, "%", "COMMERCIAL", "Adjusted margin percentage", 3, "adjMargin / adjPrice × 100"),
    );

    links.push(
      l("est.totalCostCents", "comm.contingencyCents", "×", 1),
      l("input.contingencyPct", "comm.contingencyCents", "× %", 1),
      l("est.totalPriceCents", "comm.discountCents", "×", 1),
      l("input.discountPct", "comm.discountCents", "× %", 1),
      l("est.totalCostCents", "comm.adjustedCost", "+", 2),
      l("comm.contingencyCents", "comm.adjustedCost", "+", 2),
      l("est.totalPriceCents", "comm.adjustedPrice", "−", 2),
      l("comm.discountCents", "comm.adjustedPrice", "−", 2),
      l("comm.adjustedPrice", "comm.adjustedMargin", "−", 2),
      l("comm.adjustedCost", "comm.adjustedMargin", "−", 2),
      l("comm.adjustedMargin", "comm.adjustedMarginPct", "÷ price × 100", 2),
      l("comm.adjustedPrice", "comm.adjustedMarginPct", "÷", 1),
    );
  }

  if (hasCommercialMeta || (terms?.paymentMilestones && terms.paymentMilestones.length > 0)) {
    if (terms!.pricingModel) {
      nodes.push(
        n("comm.pricingModel", "Pricing Model", terms!.pricingModel.replace(/_/g, " "), "—", "COMMERCIAL", `Pricing model: ${terms!.pricingModel}`, 0),
      );
    }
    if (terms!.paymentTermDays) {
      nodes.push(
        n("comm.paymentTermDays", "Payment Terms", `${terms!.paymentTermDays} days`, "days", "COMMERCIAL", `Net payment terms: ${terms!.paymentTermDays} days`, 0),
      );
    }
    if (terms!.warrantyMonths) {
      nodes.push(
        n("comm.warrantyMonths", "Warranty", `${terms!.warrantyMonths} mo`, "months", "COMMERCIAL", `Warranty period: ${terms!.warrantyMonths} months`, 0),
      );
    }
    const milestones = terms!.paymentMilestones ?? [];
    if (milestones.length > 0) {
      nodes.push(
        n("comm.milestones", "Milestones", `${milestones.length}`, "count", "COMMERCIAL", `${milestones.length} payment milestones (${milestones.map((milestone) => `${milestone.label}: ${milestone.percent}%`).join(", ")})`, 2),
        n("comm.milestoneTotalPct", "Milestone Sum", `${milestones.reduce((sum, milestone) => sum + milestone.percent, 0).toFixed(0)}%`, "%", "COMMERCIAL", "Sum of milestone percentages (should be 100%)", 2, "Σ(milestone.percent)"),
      );
      links.push(
        l(hasCommercialAdjustments ? "comm.adjustedPrice" : "est.totalPriceCents", "comm.milestones", "× %", 1),
      );
    }
  }

  if (project.startDate && project.endDate) {
    const spreadResult = computeEvenSpread({
      totalHours,
      startDate: project.startDate,
      endDate: project.endDate,
    });
    const weeklyResult = distributeHoursToWeeks({
      totalHours,
      startDate: project.startDate.toISOString().slice(0, 10),
      endDate: project.endDate.toISOString().slice(0, 10),
      pattern: "even",
    });
    const hasManualSpreads = lines.some((demandLine) => {
      const spread = demandLine.monthlySpread as Record<string, number> | null;
      return spread && Object.keys(spread).length > 0;
    });

    nodes.push(
      n("spread.monthCount", "Months", `${spreadResult.months.length}`, "count", "SPREAD", `${spreadResult.months.length} months in project date range`, 1),
      n("spread.weekCount", "Weeks", `${weeklyResult.weeks.length}`, "count", "SPREAD", `${weeklyResult.weeks.length} ISO weeks in project date range`, 1),
      n("spread.monthlySpread", "Monthly Spread", hasManualSpreads ? "manual + even" : "even", "—", "SPREAD", "Hours distributed across months weighted by working days", 2, "hours × (monthWorkDays / totalWorkDays)"),
      n("spread.weeklyPhasing", "Weekly Phasing", "even", "—", "SPREAD", "Hours distributed across ISO weeks (even/front/back-loaded)", 2, "totalHours / weekCount"),
      n("spread.totalDistributed", "Distributed Hours", fmtNum(weeklyResult.totalDistributedHours), "hours", "SPREAD", "Total hours after weekly distribution (should match estimate)", 3, "Σ(weeklyHours)"),
    );

    links.push(
      l("input.projectStart", "spread.monthCount", "→ range", 1),
      l("input.projectEnd", "spread.monthCount", "→ range", 1),
      l("input.projectStart", "spread.weekCount", "→ range", 1),
      l("input.projectEnd", "spread.weekCount", "→ range", 1),
      l("est.totalHours", "spread.monthlySpread", "distribute", 2),
      l("spread.monthCount", "spread.monthlySpread", "÷ by workdays", 1),
      l("est.totalHours", "spread.weeklyPhasing", "distribute", 2),
      l("spread.weekCount", "spread.weeklyPhasing", "÷ by weeks", 1),
      l("spread.weeklyPhasing", "spread.totalDistributed", "Σ", 2),
    );
  }

  return {
    estimatedCostCents: totalCostCents,
    nodes,
    links,
  };
}
