import type { ProcessedReceipt, TaxBreakdownItem } from "../schema/receipt";

/**
 * Einfache Tabellen-CSV (Excel-freundlich): UTF-8-BOM, Semikolon als
 * Trennzeichen, deutsche Dezimalkomma-Schreibweise, CRLF-Zeilenenden.
 *
 * Eine Tabelle der erfassten Belegdaten – kein Buchungsformat.
 *
 * Grundsätze:
 *  - Steuerspalten werden aus den tatsächlich vorkommenden Sätzen gebildet.
 *    7 % und 19 % sind immer dabei; jeder weitere Satz mit Steuerbetrag
 *    (10 % AT, 8,1 % CH …) bekommt eine eigene Spalte, statt unsichtbar im
 *    Bruttobetrag zu verschwinden.
 *  - Fehlende Angaben bleiben leer. Es wird nichts erfunden.
 *  - Beträge unterschiedlicher Währungen werden nicht vermischt; die Währung
 *    steht in einer eigenen Spalte.
 */

const STANDARD_RATES = [7, 19];
const BOM = "﻿";
const KLEINBETRAG_LIMIT = 250;

/**
 * CSV / Excel formula-injection neutralizer (OWASP-recommended mitigation).
 *
 * A cell value whose first character is `=`, `+`, `-`, `@`, tab or CR can be
 * evaluated as a formula / DDE payload when the CSV is opened in Excel —
 * e.g. a merchant name of `=HYPERLINK("http://evil","x")` or the DDE probe
 * `+cmd|' /C calc'!A0`. Mitigation: prefix such values with a single quote
 * `'`. Quoting alone is insufficient (Excel still evaluates quoted `=…`
 * cells during CSV import), so this is applied to every cell before quoting.
 *
 * `-` / `+` are only dangerous when the remainder is not a plain number
 * literal: `-5,00` (credit note) and `+1,25` stay untouched so accounting
 * exports keep their exact figures, while `-cmd|…`, `+cmd|…`, `-SUM(…)` etc.
 * get the `'` prefix. The apostrophe is visible in Excel but the cell can
 * never execute.
 */
export function neutralizeFormulaPrefix(value: string): string {
  if (value.length === 0) return value;
  if (/^[=\t\r@]/.test(value)) return `'${value}`;
  if (/^[+-]/.test(value) && !/^[+-]?\d[\d.,]*$/.test(value)) return `'${value}`;
  return value;
}

function round2(value: number): number {
  return Math.round((value + Number.EPSILON) * 100) / 100;
}

function rateKey(rate: number): number {
  return Math.round(rate * 1000) / 1000;
}

function isNum(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value);
}

function num(value: unknown): number {
  return isNum(value) ? value : 0;
}

function text(value: unknown): string {
  return typeof value === "string" ? value : "";
}

function taxEntries(receipt: ProcessedReceipt): TaxBreakdownItem[] {
  const list = (receipt as { taxBreakdown?: unknown }).taxBreakdown;
  if (!Array.isArray(list)) return [];
  return list.filter((t): t is TaxBreakdownItem => !!t && typeof t === "object");
}

function grossOf(receipt: ProcessedReceipt): number {
  return round2(num(receipt?.totalAmount?.value));
}

function totalTaxOf(receipt: ProcessedReceipt): number {
  return round2(taxEntries(receipt).reduce((sum, t) => sum + num(t.taxAmount), 0));
}

function netOf(receipt: ProcessedReceipt): number {
  const declared = (receipt as { netAmount?: unknown }).netAmount;
  if (isNum(declared)) return round2(declared);
  return round2(grossOf(receipt) - totalTaxOf(receipt));
}

/**
 * Trinkgeld laut Beleg, oder null wenn keins erfasst wurde. Bewusst nicht Teil
 * von netOf/grossOf: Auf das Trinkgeld wird keine MwSt ausgewiesen.
 */
function tipOf(receipt: ProcessedReceipt): number | null {
  const declared = (receipt as { tipAmount?: unknown }).tipAmount;
  if (!isNum(declared)) return null;
  const rounded = round2(declared);
  return rounded === 0 ? null : rounded;
}

function taxAmountForRate(receipt: ProcessedReceipt, rate: number): number {
  return round2(
    taxEntries(receipt)
      .filter((t) => isNum(t.ratePercent) && rateKey(t.ratePercent) === rate)
      .reduce((sum, t) => sum + num(t.taxAmount), 0)
  );
}

function currencyOf(receipt: ProcessedReceipt): string {
  const raw = text((receipt as { currency?: unknown }).currency).trim().toUpperCase();
  return raw.length > 0 ? raw : "EUR";
}

function readHospitality(
  receipt: ProcessedReceipt
): { occasion: string; participants: string } | null {
  const raw = (receipt as { hospitality?: unknown }).hospitality;
  if (!raw || typeof raw !== "object") return null;
  const rec = raw as { occasion?: unknown; participants?: unknown };
  return { occasion: text(rec.occasion), participants: text(rec.participants) };
}

function isHospitalityReceipt(receipt: ProcessedReceipt): boolean {
  return receipt?.documentType === "BEWIRTUNGSBELEG" || receipt?.suggestedCategory === "Bewirtung";
}

function isUserConfirmed(receipt: ProcessedReceipt): boolean {
  return (receipt?.validation as { userConfirmed?: unknown } | undefined)?.userConfirmed === true;
}

function rateLabel(rate: number, locale: CsvLocale = "de"): string {
  const value = rateKey(rate);
  if (locale === "en") return `${value}%`;
  return `${Number.isInteger(value) ? String(value) : String(value).replace(".", ",")} %`;
}

/**
 * Kleinbetragsrechnung nach § 33 UStDV: bis 250 € brutto erleichterte
 * Pflichtangaben. Massgeblich ist der Rechnungsbetrag ohne Trinkgeld; bei
 * Gutschriften der Betrag ohne Vorzeichen. `null` bei Fremdwaehrung, weil die
 * Grenze ein Euro-Betrag ist und eine Aussage ohne Kurs geraten waere.
 */
function isSmallValueInvoice(receipt: ProcessedReceipt): boolean | null {
  if (currencyOf(receipt) !== "EUR") return null;
  return Math.abs(grossOf(receipt)) <= KLEINBETRAG_LIMIT;
}

function collectTaxRates(receipts: ProcessedReceipt[]): number[] {
  const rates = new Set<number>(STANDARD_RATES);
  for (const receipt of receipts) {
    for (const entry of taxEntries(receipt)) {
      if (!isNum(entry.ratePercent)) continue;
      if (round2(num(entry.taxAmount)) === 0) continue;
      rates.add(rateKey(entry.ratePercent));
    }
  }
  return Array.from(rates).sort((a, b) => a - b);
}

/**
 * Exportiert die erfassten Belegdaten als Tabelle.
 *
 * Enthält bewusst keine steuerliche Bewertung (etwa ob Vorsteuer abziehbar
 * ist) – die App liest Belege aus und rechnet Summen nach, sie beurteilt sie
 * nicht.
 */
export type CsvLocale = "de" | "en";

export interface CsvExportOptions {
  /** Sprache der Kopfzeile und Statuswerte. Zahlenformat bleibt deutsch (Semikolon-CSV). */
  locale?: CsvLocale;
}

/**
 * Beschriftungen der CSV. Geschlossene Tabelle wie im
 * Excel-Generator, damit im englischen Export nichts Deutsches uebrigbleibt.
 */
const CSV_LABELS: Record<CsvLocale, Record<string, string>> = {
  de: {
    nr: "Laufende Nr",
    date: "Belegdatum (TT.MM.JJJJ)",
    merchant: "Händler / Aussteller",
    category: "Kategorie",
    docType: "Dokumenttyp",
    receiptNo: "Belegnummer",
    currency: "Währung",
    gross: "Umsatz Brutto",
    net: "Netto",
    vat: "MwSt",
    vatTotal: "MwSt gesamt",
    tip: "Trinkgeld",
    paid: "Gesamt gezahlt",
    smallInvoice: "Kleinbetrag § 33 UStDV",
    smallInvoiceYes: "Ja",
    smallInvoiceNo: "Nein",
    note: "Notiz",
    status: "Status",
    occasion: "Anlass (Bewirtung)",
    participants: "Teilnehmer (Bewirtung)",
    confirmed: "Bestätigt",
    valid: "Valide",
    review: "Prüfung erforderlich",
  },
  en: {
    nr: "No",
    date: "Receipt date (DD.MM.YYYY)",
    merchant: "Merchant / Issuer",
    category: "Category",
    docType: "Document type",
    receiptNo: "Receipt number",
    currency: "Currency",
    gross: "Gross amount",
    net: "Net",
    vat: "VAT",
    vatTotal: "VAT total",
    tip: "Tip",
    paid: "Total paid",
    smallInvoice: "Small-value invoice (§ 33 UStDV)",
    smallInvoiceYes: "Yes",
    smallInvoiceNo: "No",
    note: "Note",
    status: "Status",
    occasion: "Occasion (hospitality)",
    participants: "Attendees (hospitality)",
    confirmed: "Confirmed",
    valid: "Valid",
    review: "Review required",
  },
};

export function generateAccountingCsv(
  receipts: ProcessedReceipt[],
  options: CsvExportOptions = {}
): string {
  const list = Array.isArray(receipts) ? receipts.filter((r): r is ProcessedReceipt => !!r) : [];
  const locale: CsvLocale = options.locale === "en" ? "en" : "de";
  const T = CSV_LABELS[locale];

  const rates = collectTaxRates(list);
  const currencies = Array.from(new Set(list.map(currencyOf)));
  const singleCurrency = currencies.length === 1 ? currencies[0] : null;
  const suffix = singleCurrency ? ` (${singleCurrency === "EUR" ? "€" : singleCurrency})` : "";
  const showHospitality = list.some((r) => isHospitalityReceipt(r) || readHospitality(r) !== null);
  // Spalten nur ausgeben, wenn im Export ueberhaupt ein Trinkgeld vorkommt.
  const showTips = list.some((r) => tipOf(r) !== null);

  const headers = [
    T.nr,
    T.date,
    T.merchant,
    T.category,
    T.docType,
    T.receiptNo,
    T.currency,
    `${T.gross}${suffix}`,
    `${T.net}${suffix}`,
    ...rates.map((rate) => `${T.vat} ${rateLabel(rate, locale)}${suffix}`),
    `${T.vatTotal}${suffix}`,
    ...(showTips ? [`${T.tip}${suffix}`, `${T.paid}${suffix}`] : []),
    T.smallInvoice,
    T.note,
    T.status,
    ...(showHospitality ? [T.occasion, T.participants] : []),
  ];

  const escapeCsv = (val: string | number | null | undefined): string => {
    if (val === null || val === undefined) return "";
    // Formula-injection guard BEFORE quoting: a leading = + - @ tab CR would
    // otherwise be evaluated by Excel on import even inside quotes.
    const str = neutralizeFormulaPrefix(String(val));
    return `"${str.replace(/"/g, '""')}"`;
  };

  const formatGermanNumber = (value: number | null | undefined): string => {
    if (!isNum(value)) return "0,00";
    return value.toFixed(2).replace(".", ",");
  };

  const formatGermanDate = (isoDate: string): string => {
    if (!isoDate || !isoDate.includes("-")) return isoDate;
    const [year, month, day] = isoDate.split("-");
    return `${day}.${month}.${year}`;
  };

  const statusLabel = (receipt: ProcessedReceipt): string => {
    if (isUserConfirmed(receipt)) return T.confirmed;
    return receipt?.validation?.isMathValid ? T.valid : T.review;
  };

  const rows = list.map((receipt, idx) => {
    const merchant = text(receipt?.merchant?.name);
    const category = text(receipt?.suggestedCategory);
    const hospitality = readHospitality(receipt);

    const cells: Array<string | number> = [
      idx + 1,
      formatGermanDate(text(receipt?.date?.isoDate)),
      merchant,
      category,
      text(receipt?.documentType),
      text(receipt?.receiptNumber),
      currencyOf(receipt),
      formatGermanNumber(grossOf(receipt)),
      formatGermanNumber(netOf(receipt)),
      ...rates.map((rate) => formatGermanNumber(taxAmountForRate(receipt, rate))),
      formatGermanNumber(totalTaxOf(receipt)),
      ...(showTips
        ? [
            // Leer statt "0,00", damit "kein Trinkgeld" und "Trinkgeld 0" in der
            // in der Tabelle unterscheidbar bleiben.
            tipOf(receipt) === null ? "" : formatGermanNumber(tipOf(receipt)),
            formatGermanNumber(grossOf(receipt) + (tipOf(receipt) ?? 0)),
          ]
        : []),
      (() => {
        const small = isSmallValueInvoice(receipt);
        if (small === null) return "";
        return small ? T.smallInvoiceYes : T.smallInvoiceNo;
      })(),
      `${merchant} - ${category}`,
      statusLabel(receipt),
    ];

    if (showHospitality) {
      // Leer lassen, wenn nicht erfasst – § 4 Abs. 5 EStG verlangt die Angaben,
      // erfinden dürfen wir sie nicht.
      cells.push(hospitality?.occasion ?? "", hospitality?.participants ?? "");
    }

    return cells.map(escapeCsv).join(";");
  });

  return BOM + [headers.map(escapeCsv).join(";"), ...rows].join("\r\n");
}
