import {
  ReceiptData,
  ReviewField,
  ValidationIssue,
  ValidationIssueField,
} from "../schema/receipt";

export interface MathValidationResult {
  isMathValid: boolean;
  needsUserReview: boolean;
  /** Legacy-Feld: Feld des Befunds mit der höchsten Priorität. Siehe `issues`. */
  reviewField: ReviewField;
  /** Legacy-Feld: Meldung des Befunds mit der höchsten Priorität. Siehe `issues`. */
  reviewReason: string | null;
  /** Alle Befunde, nach Schweregrad sortiert (errors zuerst). */
  issues: ValidationIssue[];
  calculatedGross: number;
  calculatedTaxSum: number;
  calculatedNetSum: number;
  calculatedItemsSum: number;
}

/**
 * Confidence-Schwellen für die gesamte App.
 * UI-Komponenten (z.B. LiveTable-Highlighting) sollen diese Konstanten
 * importieren, damit Validator und Oberfläche identische Grenzen verwenden.
 */
export const CONFIDENCE_THRESHOLDS = {
  /** Unterhalb dieser Confidence wird der Händlername zur Prüfung markiert. */
  merchant: 0.80,
  /** Unterhalb dieser Confidence wird das Belegdatum zur Prüfung markiert. */
  date: 0.80,
  /** Unterhalb dieser Confidence wird der Bruttobetrag zur Prüfung markiert. */
  totalAmount: 0.85,
  /** Confidence, die für ein vom Benutzer eingegebenes Feld gesetzt wird. */
  userEdited: 1.0,
} as const;

/**
 * Platzhalter, die ein Vision-Modell einsetzt, wenn es den Händlernamen nicht
 * entziffern kann (z.B. "Nicht lesbar", "Taxiunternehmen (Name unleserlich)").
 * Solche Werte sind kein Händlername, sondern ein Prüfauftrag.
 */
const MERCHANT_PLACEHOLDER_PATTERN =
  /(nicht\s+lesbar|unleserlich|nicht\s+erkennbar|nicht\s+identifizierbar|unbekannt|unknown|illegible|unreadable|not\s+readable|keine?\s+angabe)/i;

/** true, wenn der Händlername ein Platzhalter statt eines echten Namens ist. */
export function isPlaceholderMerchantName(name: string | null | undefined): boolean {
  const trimmed = (name ?? "").trim();
  if (!trimmed) return true;
  // Reine Füllzeichen ("-", "—", "n/a", "?") gelten ebenfalls als Platzhalter.
  if (/^[-—–?.\s]*$/.test(trimmed)) return true;
  if (/^n\/?a$/i.test(trimmed)) return true;
  return MERCHANT_PLACEHOLDER_PATTERN.test(trimmed);
}

/**
 * Trinkgeld-Positionen. Sie werden auf Bewirtungsbelegen zusätzlich zum
 * Rechnungsbetrag gezahlt und tragen keine ausgewiesene MwSt.
 */
const TIP_LINE_ITEM_PATTERN = /^\s*(trinkgeld|tip|gratuity)\b/i;

/** true, wenn die Position ein Trinkgeld ist (nicht Teil des Rechnungsbetrags). */
export function isTipLineItem(description: string | null | undefined): boolean {
  return TIP_LINE_ITEM_PATTERN.test((description ?? "").trim());
}

/** Absolute Mindesttoleranz in Euro (reine Cent-Rundung gem. Spezifikation: 0.03€). */
export const MIN_TOLERANCE = 0.03;
/** Relativer Toleranzanteil am Bruttobetrag (nur für extrem große Beträge > 10.000€). */
export const RELATIVE_TOLERANCE = 0.001;
/** Zusätzliche Toleranz pro Einzelposition (Rundungsfehler akkumulieren). */
export const PER_ITEM_TOLERANCE = 0.01;
/** Printed line totals are already rounded; long receipts must not gain euro-sized slack. */
export const MAX_ITEMS_TOLERANCE = 0.05;
/** Belege vor diesem Jahr gelten als unplausibel. */
export const MIN_RECEIPT_YEAR = 1990;
/** Plausibler Bereich für Steuersätze in Prozent (DE: 0 / 7 / 19). */
export const MAX_PLAUSIBLE_TAX_RATE = 25;

/**
 * Deterministiche Rundungstoleranz: 0.03€ Basis gemäß Anforderung.
 */
export function getTolerance(amount: number, itemCount: number = 0): number {
  if (itemCount > 0) {
    return Math.min(
      MAX_ITEMS_TOLERANCE,
      Math.max(MIN_TOLERANCE, itemCount * PER_ITEM_TOLERANCE)
    );
  }
  return MIN_TOLERANCE;
}

/** Parst ein striktes YYYY-MM-DD Datum. Gibt null zurück, wenn unparsebar. */
function parseIsoDate(value: string | null | undefined): Date | null {
  if (!value || typeof value !== "string") return null;
  const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.trim());
  if (!match) return null;
  const year = Number(match[1]);
  const month = Number(match[2]);
  const day = Number(match[3]);
  const date = new Date(Date.UTC(year, month - 1, day));
  if (
    date.getUTCFullYear() !== year ||
    date.getUTCMonth() !== month - 1 ||
    date.getUTCDate() !== day
  ) {
    return null;
  }
  return date;
}

const EUR = (value: number) => `${value.toFixed(2)} €`;

/** "lineItems" existiert im Legacy-ReviewField-Enum nicht -> auf totalAmount abbilden. */
function toReviewField(field: ValidationIssueField): ReviewField {
  return field === "lineItems" ? "totalAmount" : field;
}

/**
 * Prüft die extrahierten Belegdaten deterministisch auf Konsistenz:
 * 1. Netto + MwSt = Brutto
 * 2. Steuerbetrag je Steuersatz ≈ Netto × Satz / 100 (Plausibilitätsprüfung)
 * 3. Impliziertes Netto (Brutto − Steuersumme) konsistent zur Aufschlüsselung
 * 4. Summe der Einzelpositionen = Brutto (mengengewichtet nur bei `unitPrice`)
 * 5. Datums-Plausibilität (Zukunft, vor 1990, unparsebar)
 * 6. Confidence-Schwellen für Brutto, Datum und Händler
 *
 * Alle Befunde landen in `issues`. `reviewField` / `reviewReason` bleiben aus
 * Kompatibilitätsgründen erhalten und spiegeln den schwerwiegendsten Befund.
 */
export function validateReceiptMath(
  receipt: Partial<ReceiptData>
): MathValidationResult {
  const gross = receipt.totalAmount?.value ?? 0;
  const net = receipt.netAmount ?? null;
  const taxes = receipt.taxBreakdown ?? [];
  const items = receipt.lineItems ?? [];
  const confidence = receipt.totalAmount?.confidence ?? 1.0;

  const grossTolerance = getTolerance(gross);

  // Summe aller Steuerbeträge
  const calculatedTaxSum = taxes.reduce(
    (acc, curr) => acc + (curr.taxAmount || 0),
    0
  );

  // Trinkgeld steht auf Bewirtungsbelegen typischerweise handschriftlich unter
  // dem Rechnungsbetrag und trägt keine MwSt. Es gehört damit NICHT in die
  // Gegenprobe gegen den Bruttobetrag — sonst meldet Check 5 bei jedem
  // Bewirtungsbeleg mit Tip eine Abweichung in Höhe des Trinkgelds.
  const billedItems = items.filter((curr) => !isTipLineItem(curr.description));

  // Summe der Einzelpositionen.
  // `price` ist laut Schema der GESAMTPREIS der Position (Menge bereits drin).
  // Nur wenn `unitPrice` vorhanden ist, darf mit der Menge multipliziert werden.
  const lineTotalsSum = billedItems.reduce((acc, curr) => acc + (curr.price || 0), 0);
  const itemsTolerance = getTolerance(gross, billedItems.length);
  // `price` is the printed total of a row. Recomputing the complete receipt
  // from unitPrice silently breaks promotions, weight rounding and discounts.
  const calculatedItemsSum = lineTotalsSum;

  // Netto aus der Steueraufschlüsselung
  const calculatedNetFromTax = taxes.reduce(
    (acc, curr) => acc + (curr.netAmount || 0),
    0
  );

  const issues: ValidationIssue[] = [];
  const addIssue = (
    field: ValidationIssueField,
    severity: ValidationIssue["severity"],
    message: string
  ) => {
    issues.push({ field, severity, message });
  };

  let isMathValid = true;

  // --- Check 1: Confidence Bruttobetrag ---------------------------------
  if (confidence < CONFIDENCE_THRESHOLDS.totalAmount) {
    addIssue(
      "totalAmount",
      "warning",
      `Bruttobetrag (${EUR(gross)}) mit geringer Sicherheit (${Math.round(confidence * 100)}%) erkannt.`
    );
  }

  // --- Check 2: Netto + MwSt = Brutto -----------------------------------
  if (net !== null && taxes.length > 0) {
    const expectedGross = net + calculatedTaxSum;
    if (Math.abs(expectedGross - gross) > grossTolerance) {
      isMathValid = false;
      addIssue(
        "taxBreakdown",
        "error",
        `Netto (${EUR(net)}) + MwSt (${EUR(calculatedTaxSum)}) = ${EUR(expectedGross)} weicht von Brutto (${EUR(gross)}) ab.`
      );
    }
  }

  // --- Check 3: Plausibilität der Steuersätze --------------------------
  taxes.forEach((tax) => {
    const rate = tax.ratePercent || 0;
    const taxAmount = tax.taxAmount || 0;

    if (rate < 0 || rate > MAX_PLAUSIBLE_TAX_RATE) {
      addIssue(
        "taxBreakdown",
        "warning",
        `Unplausibler Steuersatz (${rate}%) in der Aufschlüsselung.`
      );
      return;
    }

    if (tax.netAmount !== null && tax.netAmount !== undefined && rate > 0) {
      const expectedTax = (tax.netAmount * rate) / 100;
      const tolerance = getTolerance(tax.netAmount);
      if (Math.abs(expectedTax - taxAmount) > Math.max(tolerance, 0.5)) {
        addIssue(
          "taxBreakdown",
          "warning",
          `MwSt ${rate}%: errechnet ${EUR(expectedTax)} aus Netto ${EUR(tax.netAmount)}, ausgewiesen ist ${EUR(taxAmount)}.`
        );
      }
    } else if (taxes.length === 1 && rate > 0 && gross > 0) {
      // Einziger Steuersatz ohne Nettoangabe: Steuer lässt sich direkt
      // aus dem Brutto herausrechnen.
      const expectedTax = (gross * rate) / (100 + rate);
      if (Math.abs(expectedTax - taxAmount) > grossTolerance) {
        isMathValid = false;
        addIssue(
          "taxBreakdown",
          "error",
          `MwSt ${rate}%: aus Brutto (${EUR(gross)}) errechnet ${EUR(expectedTax)}, ausgewiesen ist ${EUR(taxAmount)}.`
        );
      }
    }
  });

  // --- Check 4: Impliziertes Netto (Brutto − Steuersumme) ---------------
  if (taxes.length > 0) {
    const impliedNet = gross - calculatedTaxSum;

    if (impliedNet < -grossTolerance) {
      isMathValid = false;
      addIssue(
        "taxBreakdown",
        "error",
        `Steuersumme (${EUR(calculatedTaxSum)}) übersteigt den Bruttobetrag (${EUR(gross)}).`
      );
    }

    const allNetsPresent = taxes.every(
      (tax) => tax.netAmount !== null && tax.netAmount !== undefined
    );
    if (allNetsPresent && Math.abs(calculatedNetFromTax - impliedNet) > Math.max(grossTolerance, 0.50)) {
      isMathValid = false;
      addIssue(
        "netAmount",
        "error",
        `Nettosumme der Steueraufschlüsselung (${EUR(calculatedNetFromTax)}) weicht vom implizierten Netto (${EUR(impliedNet)}) ab.`
      );
    }

    if (net === null && !allNetsPresent && impliedNet >= 0) {
      // Kein Netto vorhanden – nur Hinweis, kein Rechenfehler.
      addIssue(
        "netAmount",
        "warning",
        `Kein Nettobetrag erkannt. Impliziert: ${EUR(impliedNet)}.`
      );
    }
  }

  // --- Check 5: Summe der Einzelpositionen ------------------------------
  if (gross !== 0 && billedItems.length === 0) {
    addIssue(
      "lineItems",
      "warning",
      "Keine Einzelartikel erkannt, obwohl der Beleg einen Gesamtbetrag enthält."
    );
  } else if (billedItems.length > 0) {
    if (Math.abs(calculatedItemsSum - gross) > itemsTolerance) {
      addIssue(
        "lineItems",
        "warning",
        `Summe der Einzelartikel (${EUR(calculatedItemsSum)}) weicht von Bruttosumme (${EUR(gross)}) ab.`
      );
    }
  }

  // --- Check 6: Datums-Plausibilität ------------------------------------
  if (receipt.date) {
    const parsed = parseIsoDate(receipt.date.isoDate);
    if (parsed === null) {
      addIssue(
        "date",
        "error",
        `Belegdatum "${receipt.date.isoDate ?? "—"}" ist kein gültiges Datum (erwartet JJJJ-MM-TT).`
      );
    } else {
      // 1 Tag Puffer für Zeitzonen-Unterschiede.
      const maxDate = new Date(Date.now() + 24 * 60 * 60 * 1000);
      if (parsed.getTime() > maxDate.getTime()) {
        addIssue(
          "date",
          "error",
          `Belegdatum (${receipt.date.isoDate}) liegt in der Zukunft.`
        );
      } else if (parsed.getUTCFullYear() < MIN_RECEIPT_YEAR) {
        addIssue(
          "date",
          "error",
          `Belegdatum (${receipt.date.isoDate}) liegt vor ${MIN_RECEIPT_YEAR} und ist unplausibel.`
        );
      } else if (receipt.date.confidence < CONFIDENCE_THRESHOLDS.date) {
        addIssue(
          "date",
          "warning",
          `Belegdatum (${receipt.date.isoDate}) unsicher erkannt.`
        );
      }
    }
  }

  // --- Check 6b: Trinkgeld-Plausibilität ---------------------------------
  const tip = receipt.tipAmount ?? null;
  if (tip !== null) {
    if (tip < 0) {
      // Fehler-Severity muss isMathValid mitziehen: Die CSV-Statusspalte liest
      // ausschließlich isMathValid und würde sonst "Valide" ausweisen, obwohl
      // ein Fehler vorliegt.
      isMathValid = false;
      addIssue("totalAmount", "error", `Trinkgeld (${EUR(tip)}) ist negativ.`);
    } else if (gross > 0 && tip > gross) {
      // Ein Trinkgeld über dem Rechnungsbetrag ist fast immer ein Lesefehler —
      // typischerweise wurde der handschriftliche Gesamtbetrag als Tip erfasst.
      addIssue(
        "totalAmount",
        "warning",
        `Trinkgeld (${EUR(tip)}) übersteigt den Rechnungsbetrag (${EUR(gross)}) — bitte prüfen.`
      );
    }
  }

  // --- Check 7: Confidence Händler --------------------------------------
  if (receipt.merchant && receipt.merchant.confidence < CONFIDENCE_THRESHOLDS.merchant) {
    addIssue(
      "merchant",
      "warning",
      `Händlername (${receipt.merchant.name}) unsicher erkannt.`
    );
  } else if (receipt.merchant && isPlaceholderMerchantName(receipt.merchant.name)) {
    // Die Confidence allein reicht hier nicht: Ein Modell, das den Namen nicht
    // entziffern kann, ist sich seiner Sache oft trotzdem sicher und meldet
    // eine hohe Confidence zu einem Platzhalter wie "Nicht lesbar". Ohne diesen
    // Check landet der Platzhalter ungeprüft als Händlername in der Excel.
    addIssue(
      "merchant",
      "warning",
      `Händlername konnte nicht gelesen werden ("${receipt.merchant.name}") — bitte ergänzen.`
    );
  }

  // Errors zuerst, Reihenfolge innerhalb einer Stufe bleibt stabil.
  const sortedIssues = [
    ...issues.filter((issue) => issue.severity === "error"),
    ...issues.filter((issue) => issue.severity !== "error"),
  ];

  const primary = sortedIssues[0] ?? null;

  return {
    isMathValid,
    needsUserReview: sortedIssues.length > 0,
    reviewField: primary ? toReviewField(primary.field) : "none",
    reviewReason: primary ? primary.message : null,
    issues: sortedIssues,
    calculatedGross: gross,
    calculatedTaxSum: Number(calculatedTaxSum.toFixed(2)),
    calculatedNetSum: Number((calculatedNetFromTax || (net ?? 0)).toFixed(2)),
    calculatedItemsSum: Number(calculatedItemsSum.toFixed(2)),
  };
}
