import { ProcessedReceipt } from "../schema/receipt";

/** Art des gefundenen Duplikats. */
export type DuplicateMatchKind = "exact" | "similar";

export interface DuplicateMatch {
  /** `"exact"` = identischer Bild-Hash, `"similar"` = Händler/Datum/Betrag passen. */
  kind: DuplicateMatchKind;
  /** Der bereits gespeicherte Beleg. */
  existing: ProcessedReceipt;
  /** Menschenlesbare Begründung (deutsch) für die Anzeige. */
  reason: string;
  /** Übereinstimmungsgrad 0…1 (1 = identischer Hash). */
  score: number;
}

const DUPLICATE_AMOUNT_TOLERANCE = 0.05;
const DUPLICATE_DATE_TOLERANCE_DAYS = 1;

function normalizeMerchant(name: string | null | undefined): string {
  if (!name) return "";
  return name
    .toLowerCase()
    .replace(/\b(gmbh|ag|kg|ohg|ug|mbh|co\.?|e\.?k\.?|se|ltd|inc)\b/g, "")
    .replace(/[^a-z0-9äöüß]/g, "")
    .trim();
}

function merchantsMatch(a: string, b: string): boolean {
  if (!a || !b) return false;
  if (a === b) return true;
  const shorter = a.length <= b.length ? a : b;
  const longer = a.length <= b.length ? b : a;
  if (shorter.length >= 4 && longer.startsWith(shorter)) return true;
  if (shorter.length >= 5 && longer.includes(shorter)) return true;
  return false;
}

function dayDistance(a: string | null | undefined, b: string | null | undefined): number | null {
  if (!a || !b) return null;
  const timeA = Date.parse(`${a}T00:00:00Z`);
  const timeB = Date.parse(`${b}T00:00:00Z`);
  if (Number.isNaN(timeA) || Number.isNaN(timeB)) return null;
  return Math.abs(timeA - timeB) / 86_400_000;
}

function formatEuro(value: number, currency: string = "EUR"): string {
  const symbol = currency === "EUR" ? "€" : currency;
  return `${value.toFixed(2)} ${symbol}`;
}

export type DuplicateCandidate = Pick<
  ProcessedReceipt,
  "imageHash" | "merchant" | "date" | "totalAmount"
> &
  Partial<Pick<ProcessedReceipt, "id" | "receiptNumber" | "currency">>;

/**
 * Sucht Duplikate gegen eine Belegliste (Postgres). Nichts wird automatisch
 * verworfen — der Aufrufer zeigt die Treffer und lässt den Nutzer entscheiden.
 */
export function findDuplicates(
  candidate: DuplicateCandidate,
  existing: ProcessedReceipt[],
  options: { excludeId?: string } = {}
): DuplicateMatch[] {
  const excludeId = options.excludeId ?? candidate.id;
  const matches: DuplicateMatch[] = [];

  const candidateMerchant = normalizeMerchant(candidate.merchant?.name);
  const candidateDate = candidate.date?.isoDate;
  const candidateGross = candidate.totalAmount?.value ?? null;
  const currency = candidate.currency ?? "EUR";

  for (const existingReceipt of existing) {
    if (excludeId && existingReceipt.id === excludeId) continue;

    if (
      candidate.imageHash &&
      existingReceipt.imageHash &&
      existingReceipt.imageHash === candidate.imageHash
    ) {
      matches.push({
        kind: "exact",
        existing: existingReceipt,
        reason: `Identisches Bild (gleicher SHA-256 Hash) wie „${existingReceipt.merchant?.name ?? "Beleg"}" vom ${existingReceipt.date?.isoDate ?? "?"}.`,
        score: 1,
      });
      continue;
    }

    if (candidateGross === null) continue;
    const existingGross = existingReceipt.totalAmount?.value ?? null;
    if (existingGross === null) continue;

    const amountMatches =
      Math.abs(existingGross - candidateGross) <= DUPLICATE_AMOUNT_TOLERANCE;
    if (!amountMatches) continue;

    const distance = dayDistance(candidateDate, existingReceipt.date?.isoDate);
    if (distance === null || distance > DUPLICATE_DATE_TOLERANCE_DAYS) continue;

    const merchantMatches = merchantsMatch(
      candidateMerchant,
      normalizeMerchant(existingReceipt.merchant?.name)
    );
    const receiptNumberMatches =
      !!candidate.receiptNumber &&
      !!existingReceipt.receiptNumber &&
      candidate.receiptNumber.trim() === existingReceipt.receiptNumber.trim();

    if (!merchantMatches && !receiptNumberMatches) continue;

    const score =
      0.4 * (merchantMatches ? 1 : 0) +
      0.3 * (distance === 0 ? 1 : 0.5) +
      0.3 +
      (receiptNumberMatches ? 0.1 : 0);

    matches.push({
      kind: "similar",
      existing: existingReceipt,
      reason: receiptNumberMatches
        ? `Gleiche Belegnummer (${existingReceipt.receiptNumber}) und Betrag ${formatEuro(existingGross, currency)} am ${existingReceipt.date?.isoDate ?? "?"}.`
        : `Gleicher Händler „${existingReceipt.merchant?.name ?? "?"}", Betrag ${formatEuro(existingGross, currency)}${distance === 0 ? " und Datum" : ` (${existingReceipt.date?.isoDate ?? "?"})`}.`,
      score: Math.min(1, score),
    });
  }

  return matches.sort((a, b) => b.score - a.score);
}
