/**
 * Pure diff engine for comparing two estimate version snapshots.
 * No DB or IO dependencies — suitable for engine package.
 */

export interface VersionCompareInput {
  label?: string | null;
  versionNumber: number;
  demandLines: Array<{
    id: string;
    name: string;
    hours: number;
    costRateCents: number;
    billRateCents: number;
    costTotalCents: number;
    priceTotalCents: number;
    chapter?: string | null;
    lineType: string;
  }>;
  assumptions: Array<{
    key: string;
    label: string;
    value: unknown;
  }>;
  scopeItems: Array<{
    id?: string;
    name: string;
    sequenceNo: number;
    scopeType: string;
    packageCode?: string | null;
    description?: string | null;
    frameCount?: number | null;
    itemCount?: number | null;
  }>;
  resourceSnapshots?: Array<{
    id?: string;
    resourceId?: string | null;
    displayName: string;
    chapter?: string | null;
    currency: string;
    lcrCents: number;
    ucrCents: number;
    location?: string | null;
    level?: string | null;
  }>;
}

export type DemandLineDiffStatus = "added" | "removed" | "changed" | "unchanged";

export interface DemandLineDiff {
  name: string;
  status: DemandLineDiffStatus;
  hoursDelta?: number;
  costDelta?: number;
  priceDelta?: number;
  a?: { hours: number; costTotalCents: number; priceTotalCents: number };
  b?: { hours: number; costTotalCents: number; priceTotalCents: number };
}

export type AssumptionDiffStatus = "added" | "removed" | "changed" | "unchanged";

export interface AssumptionDiff {
  key: string;
  label: string;
  status: AssumptionDiffStatus;
  aValue?: unknown;
  bValue?: unknown;
}

export type ScopeItemDiffStatus = "added" | "removed" | "changed" | "unchanged";

export interface ScopeItemDiff {
  name: string;
  scopeType: string;
  status: ScopeItemDiffStatus;
  changedFields?: string[] | undefined;
  a?: { frameCount?: number | null | undefined; itemCount?: number | null | undefined; description?: string | null | undefined } | undefined;
  b?: { frameCount?: number | null | undefined; itemCount?: number | null | undefined; description?: string | null | undefined } | undefined;
}

export type ResourceSnapshotDiffStatus = "added" | "removed" | "changed" | "unchanged";

export interface ResourceSnapshotDiff {
  displayName: string;
  status: ResourceSnapshotDiffStatus;
  lcrDelta?: number | undefined;
  ucrDelta?: number | undefined;
  a?: { lcrCents: number; ucrCents: number; currency: string; location?: string | null | undefined; level?: string | null | undefined } | undefined;
  b?: { lcrCents: number; ucrCents: number; currency: string; location?: string | null | undefined; level?: string | null | undefined } | undefined;
}

export interface ChapterSubtotal {
  chapter: string;
  hoursA: number;
  hoursB: number;
  hoursDelta: number;
  costA: number;
  costB: number;
  costDelta: number;
}

export interface VersionDiffSummary {
  totalHoursDelta: number;
  totalCostDelta: number;
  totalPriceDelta: number;
  marginPercentA: number;
  marginPercentB: number;
  marginPercentDelta: number;
  linesAdded: number;
  linesRemoved: number;
  linesChanged: number;
  assumptionsChanged: number;
  scopeItemsAdded: number;
  scopeItemsRemoved: number;
  scopeItemsChanged: number;
  resourceSnapshotsChanged: number;
}

export interface VersionDiff {
  summary: VersionDiffSummary;
  demandLineDiffs: DemandLineDiff[];
  assumptionDiffs: AssumptionDiff[];
  scopeItemDiffs: ScopeItemDiff[];
  resourceSnapshotDiffs: ResourceSnapshotDiff[];
  chapterSubtotals: ChapterSubtotal[];
}

type DemandLine = VersionCompareInput["demandLines"][number];
type ScopeItem = VersionCompareInput["scopeItems"][number];
type ResourceSnapshot = NonNullable<VersionCompareInput["resourceSnapshots"]>[number];

function scopeDetail(s: ScopeItem): ScopeItemDiff["a"] {
  return {
    ...(s.frameCount !== undefined ? { frameCount: s.frameCount } : {}),
    ...(s.itemCount !== undefined ? { itemCount: s.itemCount } : {}),
    ...(s.description !== undefined ? { description: s.description } : {}),
  };
}

function resDetail(r: ResourceSnapshot): NonNullable<ResourceSnapshotDiff["a"]> {
  return {
    lcrCents: r.lcrCents,
    ucrCents: r.ucrCents,
    currency: r.currency,
    ...(r.location !== undefined ? { location: r.location } : {}),
    ...(r.level !== undefined ? { level: r.level } : {}),
  };
}

function demandLineFuzzyKey(line: DemandLine): string {
  return `${line.name}::${line.lineType}`;
}

/**
 * Match demand lines from version A to version B.
 * First pass: exact id match. Second pass: name+lineType fuzzy match for unmatched lines.
 */
function matchDemandLines(
  aLines: DemandLine[],
  bLines: DemandLine[],
): {
  matched: Array<{ a: DemandLine; b: DemandLine }>;
  addedInB: DemandLine[];
  removedFromA: DemandLine[];
} {
  const matched: Array<{ a: DemandLine; b: DemandLine }> = [];
  const unmatchedA = new Map<string, DemandLine>();
  const unmatchedB = new Map<string, DemandLine>();

  // Index B by id
  const bById = new Map<string, DemandLine>();
  for (const line of bLines) {
    bById.set(line.id, line);
  }

  // Pass 1: exact id match
  const matchedBIds = new Set<string>();
  for (const aLine of aLines) {
    const bLine = bById.get(aLine.id);
    if (bLine) {
      matched.push({ a: aLine, b: bLine });
      matchedBIds.add(bLine.id);
    } else {
      unmatchedA.set(aLine.id, aLine);
    }
  }

  for (const bLine of bLines) {
    if (!matchedBIds.has(bLine.id)) {
      unmatchedB.set(bLine.id, bLine);
    }
  }

  // Pass 2: fuzzy match by name+lineType
  const bByFuzzy = new Map<string, DemandLine>();
  for (const bLine of unmatchedB.values()) {
    const key = demandLineFuzzyKey(bLine);
    // Only use fuzzy if unique in unmatched B
    if (!bByFuzzy.has(key)) {
      bByFuzzy.set(key, bLine);
    } else {
      // Duplicate fuzzy key — skip fuzzy matching for this key
      bByFuzzy.delete(key);
    }
  }

  const fuzzyMatchedBIds = new Set<string>();
  const stillUnmatchedA: DemandLine[] = [];

  for (const aLine of unmatchedA.values()) {
    const key = demandLineFuzzyKey(aLine);
    const bLine = bByFuzzy.get(key);
    if (bLine && !fuzzyMatchedBIds.has(bLine.id)) {
      matched.push({ a: aLine, b: bLine });
      fuzzyMatchedBIds.add(bLine.id);
    } else {
      stillUnmatchedA.push(aLine);
    }
  }

  const addedInB: DemandLine[] = [];
  for (const bLine of unmatchedB.values()) {
    if (!fuzzyMatchedBIds.has(bLine.id)) {
      addedInB.push(bLine);
    }
  }

  return { matched, addedInB, removedFromA: stillUnmatchedA };
}

function valuesEqual(a: unknown, b: unknown): boolean {
  if (a === b) return true;
  if (a == null && b == null) return true;
  if (typeof a !== typeof b) return false;
  if (typeof a === "object" && a !== null && b !== null) {
    return JSON.stringify(a) === JSON.stringify(b);
  }
  return false;
}

/**
 * Compare two estimate version snapshots and return a structured diff.
 * Pure function — no side effects.
 */
export function compareEstimateVersions(
  a: VersionCompareInput,
  b: VersionCompareInput,
): VersionDiff {
  // --- Demand lines ---
  const { matched, addedInB, removedFromA } = matchDemandLines(
    a.demandLines,
    b.demandLines,
  );

  const demandLineDiffs: DemandLineDiff[] = [];

  for (const { a: aLine, b: bLine } of matched) {
    const hoursDelta = bLine.hours - aLine.hours;
    const costDelta = bLine.costTotalCents - aLine.costTotalCents;
    const priceDelta = bLine.priceTotalCents - aLine.priceTotalCents;
    const isChanged = hoursDelta !== 0 || costDelta !== 0 || priceDelta !== 0;

    demandLineDiffs.push({
      name: bLine.name,
      status: isChanged ? "changed" : "unchanged",
      ...(isChanged ? { hoursDelta, costDelta, priceDelta } : {}),
      a: { hours: aLine.hours, costTotalCents: aLine.costTotalCents, priceTotalCents: aLine.priceTotalCents },
      b: { hours: bLine.hours, costTotalCents: bLine.costTotalCents, priceTotalCents: bLine.priceTotalCents },
    });
  }

  for (const line of addedInB) {
    demandLineDiffs.push({
      name: line.name,
      status: "added",
      hoursDelta: line.hours,
      costDelta: line.costTotalCents,
      priceDelta: line.priceTotalCents,
      b: { hours: line.hours, costTotalCents: line.costTotalCents, priceTotalCents: line.priceTotalCents },
    });
  }

  for (const line of removedFromA) {
    demandLineDiffs.push({
      name: line.name,
      status: "removed",
      hoursDelta: -line.hours,
      costDelta: -line.costTotalCents,
      priceDelta: -line.priceTotalCents,
      a: { hours: line.hours, costTotalCents: line.costTotalCents, priceTotalCents: line.priceTotalCents },
    });
  }

  // --- Assumptions ---
  const aAssumptions = new Map(a.assumptions.map((x) => [x.key, x]));
  const bAssumptions = new Map(b.assumptions.map((x) => [x.key, x]));
  const allKeys = new Set([...aAssumptions.keys(), ...bAssumptions.keys()]);

  const assumptionDiffs: AssumptionDiff[] = [];
  let assumptionsChanged = 0;

  for (const key of allKeys) {
    const aItem = aAssumptions.get(key);
    const bItem = bAssumptions.get(key);

    if (aItem && bItem) {
      const changed = !valuesEqual(aItem.value, bItem.value);
      if (changed) assumptionsChanged++;
      assumptionDiffs.push({
        key,
        label: bItem.label,
        status: changed ? "changed" : "unchanged",
        aValue: aItem.value,
        bValue: bItem.value,
      });
    } else if (bItem) {
      assumptionsChanged++;
      assumptionDiffs.push({
        key,
        label: bItem.label,
        status: "added",
        bValue: bItem.value,
      });
    } else if (aItem) {
      assumptionsChanged++;
      assumptionDiffs.push({
        key,
        label: aItem.label,
        status: "removed",
        aValue: aItem.value,
      });
    }
  }

  // --- Scope items (detailed diff) ---
  const aScopeByKey = new Map(a.scopeItems.map((s) => [`${s.name}::${s.scopeType}`, s]));
  const bScopeByKey = new Map(b.scopeItems.map((s) => [`${s.name}::${s.scopeType}`, s]));
  const allScopeKeys = new Set([...aScopeByKey.keys(), ...bScopeByKey.keys()]);

  const scopeItemDiffs: ScopeItemDiff[] = [];
  let scopeItemsAdded = 0;
  let scopeItemsRemoved = 0;
  let scopeItemsChanged = 0;

  for (const key of allScopeKeys) {
    const aItem = aScopeByKey.get(key);
    const bItem = bScopeByKey.get(key);

    if (aItem && bItem) {
      const changedFields: string[] = [];
      if (aItem.frameCount !== bItem.frameCount) changedFields.push("frameCount");
      if (aItem.itemCount !== bItem.itemCount) changedFields.push("itemCount");
      if ((aItem.description ?? null) !== (bItem.description ?? null)) changedFields.push("description");
      if ((aItem.packageCode ?? null) !== (bItem.packageCode ?? null)) changedFields.push("packageCode");

      const status: ScopeItemDiffStatus = changedFields.length > 0 ? "changed" : "unchanged";
      if (status === "changed") scopeItemsChanged++;

      scopeItemDiffs.push({
        name: bItem.name,
        scopeType: bItem.scopeType,
        status,
        ...(changedFields.length > 0 ? { changedFields } : {}),
        a: scopeDetail(aItem),
        b: scopeDetail(bItem),
      });
    } else if (bItem) {
      scopeItemsAdded++;
      scopeItemDiffs.push({
        name: bItem.name,
        scopeType: bItem.scopeType,
        status: "added",
        b: scopeDetail(bItem),
      });
    } else if (aItem) {
      scopeItemsRemoved++;
      scopeItemDiffs.push({
        name: aItem.name,
        scopeType: aItem.scopeType,
        status: "removed",
        a: scopeDetail(aItem),
      });
    }
  }

  // --- Resource snapshots ---
  const aResources = a.resourceSnapshots ?? [];
  const bResources = b.resourceSnapshots ?? [];

  const aResByKey = new Map(aResources.map((r) => [r.resourceId ?? r.displayName, r]));
  const bResByKey = new Map(bResources.map((r) => [r.resourceId ?? r.displayName, r]));
  const allResKeys = new Set([...aResByKey.keys(), ...bResByKey.keys()]);

  const resourceSnapshotDiffs: ResourceSnapshotDiff[] = [];
  let resourceSnapshotsChanged = 0;

  for (const key of allResKeys) {
    const aRes = aResByKey.get(key);
    const bRes = bResByKey.get(key);

    if (aRes && bRes) {
      const lcrDelta = bRes.lcrCents - aRes.lcrCents;
      const ucrDelta = bRes.ucrCents - aRes.ucrCents;
      const locationChanged = (aRes.location ?? null) !== (bRes.location ?? null);
      const levelChanged = (aRes.level ?? null) !== (bRes.level ?? null);
      const isChanged = lcrDelta !== 0 || ucrDelta !== 0 || locationChanged || levelChanged;

      if (isChanged) resourceSnapshotsChanged++;

      resourceSnapshotDiffs.push({
        displayName: bRes.displayName,
        status: isChanged ? "changed" : "unchanged",
        ...(isChanged ? { lcrDelta, ucrDelta } : {}),
        a: resDetail(aRes),
        b: resDetail(bRes),
      });
    } else if (bRes) {
      resourceSnapshotsChanged++;
      resourceSnapshotDiffs.push({
        displayName: bRes.displayName,
        status: "added",
        b: resDetail(bRes),
      });
    } else if (aRes) {
      resourceSnapshotsChanged++;
      resourceSnapshotDiffs.push({
        displayName: aRes.displayName,
        status: "removed",
        a: resDetail(aRes),
      });
    }
  }

  // --- Chapter subtotals ---
  const chapterMap = new Map<string, { hoursA: number; hoursB: number; costA: number; costB: number }>();

  for (const line of a.demandLines) {
    const ch = line.chapter ?? "(no chapter)";
    const entry = chapterMap.get(ch) ?? { hoursA: 0, hoursB: 0, costA: 0, costB: 0 };
    entry.hoursA += line.hours;
    entry.costA += line.costTotalCents;
    chapterMap.set(ch, entry);
  }
  for (const line of b.demandLines) {
    const ch = line.chapter ?? "(no chapter)";
    const entry = chapterMap.get(ch) ?? { hoursA: 0, hoursB: 0, costA: 0, costB: 0 };
    entry.hoursB += line.hours;
    entry.costB += line.costTotalCents;
    chapterMap.set(ch, entry);
  }

  const chapterSubtotals: ChapterSubtotal[] = [...chapterMap.entries()]
    .map(([chapter, v]) => ({
      chapter,
      hoursA: v.hoursA,
      hoursB: v.hoursB,
      hoursDelta: v.hoursB - v.hoursA,
      costA: v.costA,
      costB: v.costB,
      costDelta: v.costB - v.costA,
    }))
    .sort((x, y) => Math.abs(y.costDelta) - Math.abs(x.costDelta));

  // --- Summary ---
  const aTotalHours = a.demandLines.reduce((s, l) => s + l.hours, 0);
  const bTotalHours = b.demandLines.reduce((s, l) => s + l.hours, 0);
  const aTotalCost = a.demandLines.reduce((s, l) => s + l.costTotalCents, 0);
  const bTotalCost = b.demandLines.reduce((s, l) => s + l.costTotalCents, 0);
  const aTotalPrice = a.demandLines.reduce((s, l) => s + l.priceTotalCents, 0);
  const bTotalPrice = b.demandLines.reduce((s, l) => s + l.priceTotalCents, 0);

  const marginPercentA = aTotalPrice > 0 ? ((aTotalPrice - aTotalCost) / aTotalPrice) * 100 : 0;
  const marginPercentB = bTotalPrice > 0 ? ((bTotalPrice - bTotalCost) / bTotalPrice) * 100 : 0;

  return {
    summary: {
      totalHoursDelta: bTotalHours - aTotalHours,
      totalCostDelta: bTotalCost - aTotalCost,
      totalPriceDelta: bTotalPrice - aTotalPrice,
      marginPercentA,
      marginPercentB,
      marginPercentDelta: marginPercentB - marginPercentA,
      linesAdded: addedInB.length,
      linesRemoved: removedFromA.length,
      linesChanged: demandLineDiffs.filter((d) => d.status === "changed").length,
      assumptionsChanged,
      scopeItemsAdded,
      scopeItemsRemoved,
      scopeItemsChanged,
      resourceSnapshotsChanged,
    },
    demandLineDiffs,
    assumptionDiffs,
    scopeItemDiffs,
    resourceSnapshotDiffs,
    chapterSubtotals,
  };
}
