export interface PreviousScanItem {
  id: string;
  displayName: string;
  quantity: number;
  unit: string;
  storageLocationId: string;
  bestBeforeDate?: string | null;
  useByDate?: string | null;
}

export interface NewScanObservation {
  displayName: string;
  quantity: number;
  unit: string;
  storageLocationId: string;
  bestBeforeDate?: string | null;
  useByDate?: string | null;
  /** Konfidens att denna observation verkligen representerar en fysisk vara. */
  observationConfidence: number;
}

export type ScanDiffRowKind =
  | "new_item"
  | "moved"
  | "quantity_changed"
  | "vanished"
  | "unchanged";

export interface ScanDiffRow {
  kind: ScanDiffRowKind;
  previousItemId?: string;
  displayName: string;
  unit: string;
  previousQuantity?: number;
  newQuantity?: number;
  previousLocationId?: string;
  newLocationId?: string;
  /** Konfidens för slutsatsen (0–1). */
  confidence: number;
  /** Mänsklig läsbar motivering. */
  reason: string;
}

export interface ScanDiffResult {
  rows: ScanDiffRow[];
  /** Identifierare för varor som är försvunna men ännu inte bekräftade borta. */
  pendingVanishedIds: string[];
}

/**
 * Jämför tidigare skannade varor med nya observationer.
 *
 * Regler (§5.5):
 * - "new_item": ny observation som inte matchar någon tidigare vara.
 * - "moved": samma vara, annan plats.
 * - "quantity_changed": samma vara, annan mängd.
 * - "vanished": tidigare vara saknas bland observationerna.
 * - "unchanged": inget avvikande.
 *
 * ALDRIG automatiska negativa transaktioner för "vanished" — det är endast
 * ett förslag som kräver användarbekräftelse.
 */
export function diffScans(
  previousItems: PreviousScanItem[],
  newObservations: NewScanObservation[],
  now: Date = new Date(),
): ScanDiffResult {
  const rows: ScanDiffRow[] = [];
  const matchedPreviousIds = new Set<string>();
  const matchedObservationIndexes = new Set<number>();

  // 1. Matcha observationer mot tidigare varor (namn + plats + datummer nära)
  for (let i = 0; i < newObservations.length; i++) {
    const obs = newObservations[i]!;
    let bestMatch: { item: PreviousScanItem; score: number } | null = null;

    for (const item of previousItems) {
      if (matchedPreviousIds.has(item.id)) continue;
      const nameScore = nameSimilarity(item.displayName, obs.displayName);
      if (nameScore < 0.6) continue;

      let score = nameScore;
      if (item.storageLocationId === obs.storageLocationId) score += 0.2;
      if (datesEqual(item.bestBeforeDate, obs.bestBeforeDate)) score += 0.1;
      if (datesEqual(item.useByDate, obs.useByDate)) score += 0.1;

      if (!bestMatch || score > bestMatch.score) {
        bestMatch = { item, score };
      }
    }

    if (bestMatch && bestMatch.score >= 0.7) {
      matchedPreviousIds.add(bestMatch.item.id);
      matchedObservationIndexes.add(i);
      const item = bestMatch.item;

      const locationChanged = item.storageLocationId !== obs.storageLocationId;
      const quantityChanged = Math.abs(item.quantity - obs.quantity) > 1e-6;

      if (locationChanged && quantityChanged) {
        rows.push({
          kind: "moved",
          previousItemId: item.id,
          displayName: obs.displayName,
          unit: obs.unit,
          previousQuantity: item.quantity,
          newQuantity: obs.quantity,
          previousLocationId: item.storageLocationId,
          newLocationId: obs.storageLocationId,
          confidence: round2(bestMatch.score * obs.observationConfidence),
          reason: "Flyttad och ändrad mängd",
        });
      } else if (locationChanged) {
        rows.push({
          kind: "moved",
          previousItemId: item.id,
          displayName: obs.displayName,
          unit: obs.unit,
          previousQuantity: item.quantity,
          newQuantity: obs.quantity,
          previousLocationId: item.storageLocationId,
          newLocationId: obs.storageLocationId,
          confidence: round2(bestMatch.score * obs.observationConfidence),
          reason: "Flyttad till annan plats",
        });
      } else if (quantityChanged) {
        rows.push({
          kind: "quantity_changed",
          previousItemId: item.id,
          displayName: obs.displayName,
          unit: obs.unit,
          previousQuantity: item.quantity,
          newQuantity: obs.quantity,
          previousLocationId: item.storageLocationId,
          newLocationId: obs.storageLocationId,
          confidence: round2(bestMatch.score * obs.observationConfidence),
          reason: "Ändrad mängd",
        });
      } else {
        rows.push({
          kind: "unchanged",
          previousItemId: item.id,
          displayName: obs.displayName,
          unit: obs.unit,
          previousQuantity: item.quantity,
          newQuantity: obs.quantity,
          previousLocationId: item.storageLocationId,
          newLocationId: obs.storageLocationId,
          confidence: round2(bestMatch.score * obs.observationConfidence),
          reason: "Oförändrad",
        });
      }
    }
  }

  // 2. Nya varor
  for (let i = 0; i < newObservations.length; i++) {
    if (matchedObservationIndexes.has(i)) continue;
    const obs = newObservations[i]!;
    rows.push({
      kind: "new_item",
      displayName: obs.displayName,
      unit: obs.unit,
      newQuantity: obs.quantity,
      newLocationId: obs.storageLocationId,
      confidence: round2(obs.observationConfidence),
      reason: "Ny vara på bilden",
    });
  }

  // 3. Försvunna varor — bara förslag, aldrig automatisk nollning
  const pendingVanishedIds: string[] = [];
  for (const item of previousItems) {
    if (matchedPreviousIds.has(item.id)) continue;
    rows.push({
      kind: "vanished",
      previousItemId: item.id,
      displayName: item.displayName,
      unit: item.unit,
      previousQuantity: item.quantity,
      previousLocationId: item.storageLocationId,
      confidence: 0.6,
      reason: "Saknas på nya bilden — bekräfta att den är slut",
    });
    pendingVanishedIds.push(item.id);
  }

  // Sortering: förändringar först, sedan nya, oförändrade sist
  const kindOrder: Record<ScanDiffRowKind, number> = {
    quantity_changed: 0,
    moved: 1,
    vanished: 2,
    new_item: 3,
    unchanged: 4,
  };
  rows.sort((a, b) => {
    if (kindOrder[a.kind] !== kindOrder[b.kind]) return kindOrder[a.kind] - kindOrder[b.kind];
    return a.displayName.localeCompare(b.displayName, "sv");
  });

  return { rows, pendingVanishedIds };
}

function nameSimilarity(a: string, b: string): number {
  const aa = a.toLowerCase().trim();
  const bb = b.toLowerCase().trim();
  if (aa === bb) return 1;
  if (aa.includes(bb) || bb.includes(aa)) return 0.85;
  const dist = levenshtein(aa, bb);
  const maxLen = Math.max(aa.length, bb.length);
  if (maxLen === 0) return 1;
  return Math.max(0, 1 - dist / maxLen);
}

function levenshtein(a: string, b: string): number {
  const m = a.length;
  const n = b.length;
  if (m === 0) return n;
  if (n === 0) return m;
  const prev: number[] = new Array(n + 1).fill(0).map((_, i) => i);
  for (let i = 1; i <= m; i++) {
    let curr0 = i;
    for (let j = 1; j <= n; j++) {
      const cost = a[i - 1] === b[j - 1] ? 0 : 1;
      curr0 = Math.min(
        prev[j]! + 1, // deletion
        curr0 + 1, // insertion
        prev[j - 1]! + cost, // substitution
      );
      prev[j - 1] = curr0;
    }
    prev[n] = i;
  }
  return prev[n]!;
}

function datesEqual(a: string | null | undefined, b: string | null | undefined): boolean {
  if (!a && !b) return true;
  if (!a || !b) return false;
  return a === b;
}

function round2(n: number): number {
  return Math.round(Math.max(0, Math.min(1, n)) * 100) / 100;
}
