// Multi-entry building-part detection (ADR-0004).
//
// A BulkUpload row can carry several comma-separated entries in a physical-
// element column (e.g. Walls = "Cavity: AsBuilt (1976-1982), Cavity:
// FilledCavity"). Each entry is a Building part (Main building + Extensions).
// This module finds that pattern and captures one sample — the row with the
// MOST building parts — so the user can confirm the ordering downstream.
//
// Pure + I/O-free so it's unit-testable; the start-address-matching route runs
// it over the already-parsed upload rows and persists the result on the upload.

import { ADDRESS_FIELDS, classifierMapping } from "./columnFields";
import type {
  MultiEntryEntry,
  MultiEntryColumn,
  MultiEntrySample,
  MultiEntrySummary,
} from "@/app/db/schema/bulk_address_uploads";

// The jsonb shape lives with the column (schema/bulk_address_uploads.ts) so the
// migration is self-contained; re-export here for callers of this module.
export type {
  MultiEntryEntry,
  MultiEntryColumn,
  MultiEntrySample,
  MultiEntrySummary,
  MultiEntryOrdering,
} from "@/app/db/schema/bulk_address_uploads";

// --- Building-part ordering (ADR-0004) ---

// Label for building-part slot k: slot 0 is the Main building, the rest are
// numbered extensions.
export function partLabel(slot: number): string {
  return slot === 0 ? "Main building" : `Extension ${slot}`;
}

// True when `arr` is a permutation of [0, n-1] (each slot used exactly once).
export function isPermutation(arr: number[]): boolean {
  const seen = new Set(arr);
  return (
    seen.size === arr.length &&
    arr.every((n) => Number.isInteger(n) && n >= 0 && n < arr.length)
  );
}

// The UI collects, per file position, which building-part slot it holds
// (`assignment[pos] = slot`). Storage is keyed the other way —
// `permutation[slot] = pos` — so a consumer can ask "which file position holds
// the main building?". Both are permutations of [0, n-1]; this inverts one to
// the other.
export function assignmentToPermutation(assignment: number[]): number[] {
  const permutation = new Array<number>(assignment.length);
  assignment.forEach((slot, position) => {
    permutation[slot] = position;
  });
  return permutation;
}

export const EMPTY_MULTI_ENTRY_SUMMARY: MultiEntrySummary = {
  multiValuedFields: [],
  countDistribution: {},
  largestCount: 0,
  sample: null,
  samplesByCount: {},
};

// Split a cell into building-part entries. Mirrors the classifier's
// split(",") → trim → lower, dropping empty fragments so positions align
// across raw and normalized forms.
export function splitEntries(value: unknown): MultiEntryEntry[] {
  return String(value ?? "")
    .split(",")
    .map((s) => s.trim())
    .filter((s) => s.length > 0)
    .map((raw) => ({ raw, description: raw.toLowerCase() }));
}

// Compose a display address from the mapped address fields (reference excluded).
function buildAddress(
  row: Record<string, unknown>,
  columnMapping: Record<string, string>,
): string {
  const parts: string[] = [];
  for (const field of ADDRESS_FIELDS) {
    if (field.value === "internal_reference") continue;
    const header = columnMapping[field.value];
    if (!header) continue;
    const value = String(row[header] ?? "").trim();
    if (value) parts.push(value);
  }
  return parts.join(", ");
}

// Scan the mapped classifier columns and capture one sample address. Only
// classifier columns are considered — they're the physical-element descriptions
// we slice into building parts; address columns are single-valued by nature.
//
// The sample serves both review steps (ADR-0004): the largest-count multi-entry
// row when one exists (Step 2 — Confirm order), otherwise the first row carrying
// any classifier value so Step 1 — Verify classification still has something to
// show. `largestCount >= 2` is the multi-entry signal; `sample != null` means
// "there is something to verify". The sample carries every mapped classifier
// column with a value — Step 1 lists them all; Step 2 renders only the
// multi-valued ones.
export function detectMultiEntry(
  rows: Array<Record<string, unknown>>,
  columnMapping: Record<string, string>,
): MultiEntrySummary {
  const classifierCols = Object.entries(classifierMapping(columnMapping));
  if (classifierCols.length === 0) return EMPTY_MULTI_ENTRY_SUMMARY;

  const multiValued = new Set<string>();
  const countDistribution: Record<string, number> = {};
  let largestCount = 0;
  let multiEntryRowIndex = -1;
  // Fallback sample for Step 1 when no row is multi-entry: the first row that
  // carries any classifier value.
  let firstClassifiedRowIndex = -1;
  // First row index seen at each distinct count ≥ 2 — one ordering sample per
  // count (ADR-0004 amendment): each count needs its own confirmed permutation.
  const sampleRowIndexByCount: Record<string, number> = {};

  rows.forEach((row, index) => {
    let rowMax = 0;
    let hasValue = false;
    for (const [field, header] of classifierCols) {
      const n = splitEntries(row[header]).length;
      if (n > 0) hasValue = true;
      if (n > 1) multiValued.add(field);
      if (n > rowMax) rowMax = n;
    }
    if (hasValue && firstClassifiedRowIndex === -1) firstClassifiedRowIndex = index;
    if (rowMax >= 2) {
      const key = String(rowMax);
      countDistribution[key] = (countDistribution[key] ?? 0) + 1;
      if (sampleRowIndexByCount[key] === undefined) sampleRowIndexByCount[key] = index;
      // First row at a new maximum becomes the multi-entry (Step 1) sample.
      if (rowMax > largestCount) {
        largestCount = rowMax;
        multiEntryRowIndex = index;
      }
    }
  });

  const sampleRowIndex =
    multiEntryRowIndex !== -1 ? multiEntryRowIndex : firstClassifiedRowIndex;
  if (sampleRowIndex === -1) {
    return {
      multiValuedFields: [...multiValued],
      countDistribution,
      largestCount,
      sample: null,
      samplesByCount: {},
    };
  }

  // One ordering sample per distinct count, so the UI can render a panel per
  // count and the user confirms each independently.
  const samplesByCount: Record<string, MultiEntrySample> = {};
  for (const [count, rowIndex] of Object.entries(sampleRowIndexByCount)) {
    samplesByCount[count] = sampleFromRow(rows[rowIndex], columnMapping, classifierCols, Number(count));
  }

  return {
    multiValuedFields: [...multiValued],
    countDistribution,
    largestCount,
    sample: sampleFromRow(
      rows[sampleRowIndex],
      columnMapping,
      classifierCols,
      largestCount >= 2 ? largestCount : 1,
    ),
    samplesByCount,
  };
}

// Build the sample for one row: its display address plus every mapped classifier
// column carrying a value. Step 1 lists all columns; Step 2's order table filters
// to the multi-valued ones (single-value columns are whole-dwelling facts).
function sampleFromRow(
  row: Record<string, unknown>,
  columnMapping: Record<string, string>,
  classifierCols: Array<[string, string]>,
  count: number,
): MultiEntrySample {
  const columns: MultiEntryColumn[] = classifierCols
    .map(([field, header]) => ({ field, header, entries: splitEntries(row[header]) }))
    .filter((column) => column.entries.length > 0);
  return { address: buildAddress(row, columnMapping), count, columns };
}
