import { ReceiptData, TaxBreakdownItem } from "../schema/receipt";
import { CONFIDENCE_THRESHOLDS, validateReceiptMath } from "./mathValidator";

/**
 * Feld, das der Benutzer gerade bearbeitet hat.
 * Steuert, welcher Wert beim Nachrechnen festgehalten wird.
 */
export type EditedReceiptField =
  | "totalAmount"
  | "netAmount"
  | "taxBreakdown"
  | "lineItems"
  | "date"
  | "merchant"
  | "hospitality"
  | "category"
  | "none";

export interface RecalculateOptions {
  /**
   * Welches Feld der Benutzer zuletzt geändert hat.
   * - `"totalAmount"` – Brutto neu gesetzt: Steueraufschlüsselung und Netto werden
   *   anteilig über die vorhandenen Steuersätze neu verteilt.
   * - `"netAmount"` – Netto neu gesetzt: Brutto bleibt, Steuern werden abgeleitet.
   * - `"taxBreakdown"` – Steuerbeträge neu gesetzt: Brutto bleibt, Netto wird abgeleitet.
   * - alle anderen / weggelassen – es werden keine Beträge umgerechnet,
   *   es wird nur neu validiert.
   */
  editedField?: EditedReceiptField;
}

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

/**
 * Rundet alle Werte auf Cent und legt die Rundungsdifferenz auf die größte
 * Position, damit die Summe exakt `target` ergibt.
 */
function roundToTarget(values: number[], target: number): number[] {
  if (values.length === 0) return [];
  const rounded = values.map(round2);
  const sum = rounded.reduce((acc, curr) => acc + curr, 0);
  const residual = round2(target - sum);
  if (residual !== 0) {
    let largest = 0;
    for (let i = 1; i < rounded.length; i++) {
      if (Math.abs(rounded[i]) > Math.abs(rounded[largest])) largest = i;
    }
    rounded[largest] = round2(rounded[largest] + residual);
  }
  return rounded;
}

/** Bruttoanteil einer Steuerzeile (Netto + Steuer, ggf. aus dem Satz abgeleitet). */
function grossPartOf(tax: TaxBreakdownItem): number {
  const rate = tax?.ratePercent ?? 0;
  const taxAmount = tax?.taxAmount ?? 0;
  if (tax?.netAmount !== null && tax?.netAmount !== undefined) {
    return tax.netAmount + taxAmount;
  }
  if (rate > 0) return (taxAmount * (100 + rate)) / rate;
  return taxAmount;
}

/** Gleichverteilung als Fallback, wenn keine sinnvollen Gewichte vorliegen. */
function sharesFrom(weights: number[]): number[] {
  const total = weights.reduce((acc, curr) => acc + curr, 0);
  if (!Number.isFinite(total) || total <= 0) {
    return weights.map(() => 1 / weights.length);
  }
  return weights.map((weight) => weight / total);
}

/**
 * Verteilt einen Bruttobetrag anteilig auf die vorhandenen Steuersätze und
 * rechnet je Zeile Netto und Steuer heraus. Die Summen sind cent-genau.
 */
function distributeGross(
  gross: number,
  taxes: TaxBreakdownItem[]
): TaxBreakdownItem[] {
  if (taxes.length === 0) return [];
  const shares = sharesFrom(taxes.map(grossPartOf));
  const grossParts = roundToTarget(
    shares.map((share) => gross * share),
    round2(gross)
  );

  return taxes.map((tax, index) => {
    const rate = tax?.ratePercent ?? 0;
    const grossPart = grossParts[index];
    const netPart = rate > 0 ? round2(grossPart / (1 + rate / 100)) : round2(grossPart);
    return {
      ...tax,
      ratePercent: rate,
      netAmount: netPart,
      taxAmount: round2(grossPart - netPart),
    };
  });
}

/**
 * Hält Brutto und Netto fest und leitet die Steuerzeilen daraus ab.
 */
function distributeFromNet(
  gross: number,
  net: number,
  taxes: TaxBreakdownItem[]
): TaxBreakdownItem[] {
  if (taxes.length === 0) return [];
  const totalTax = round2(gross - net);

  const netWeights = taxes.map((tax) => {
    if (tax?.netAmount !== null && tax?.netAmount !== undefined) return tax.netAmount;
    return Math.max(0, grossPartOf(tax));
  });
  const netParts = roundToTarget(
    sharesFrom(netWeights).map((share) => net * share),
    round2(net)
  );

  const taxWeights = taxes.map(
    (tax, index) => (netParts[index] * (tax?.ratePercent ?? 0)) / 100
  );
  const taxParts = roundToTarget(
    sharesFrom(taxWeights).map((share) => totalTax * share),
    totalTax
  );

  return taxes.map((tax, index) => ({
    ...tax,
    ratePercent: tax?.ratePercent ?? 0,
    netAmount: netParts[index],
    taxAmount: taxParts[index],
  }));
}

/**
 * Hält Brutto und die eingegebenen Steuerbeträge fest und leitet die
 * Nettoanteile daraus ab.
 */
function deriveNetsFromTaxes(
  gross: number,
  taxes: TaxBreakdownItem[]
): { taxBreakdown: TaxBreakdownItem[]; netAmount: number } {
  const totalTax = round2(
    taxes.reduce((acc, curr) => acc + (curr?.taxAmount ?? 0), 0)
  );
  const netAmount = round2(gross - totalTax);

  if (taxes.length === 0) {
    return { taxBreakdown: [], netAmount };
  }

  const netWeights = taxes.map((tax) => {
    const rate = tax?.ratePercent ?? 0;
    const taxAmount = tax?.taxAmount ?? 0;
    if (rate > 0) return (taxAmount * 100) / rate;
    if (tax?.netAmount !== null && tax?.netAmount !== undefined) {
      return Math.max(0, tax.netAmount);
    }
    return 0;
  });
  const netParts = roundToTarget(
    sharesFrom(netWeights).map((share) => netAmount * share),
    netAmount
  );

  return {
    taxBreakdown: taxes.map((tax, index) => ({
      ...tax,
      ratePercent: tax?.ratePercent ?? 0,
      netAmount: netParts[index],
      taxAmount: round2(tax?.taxAmount ?? 0),
    })),
    netAmount,
  };
}

/**
 * Rechnet abhängige Felder eines Belegs nach einer Benutzereingabe neu durch
 * und validiert ihn anschließend.
 *
 * **Grundregel:** Der Bruttobetrag (`totalAmount.value`) steht auf dem Beleg und
 * ist die Wahrheit. Er wird nie automatisch verändert.
 *
 * Verhalten je `editedField`:
 * - `"totalAmount"`: Das neue Brutto wird anteilig über die vorhandenen
 *   Steuersätze verteilt; `taxBreakdown[].netAmount`, `taxBreakdown[].taxAmount`
 *   und `netAmount` werden cent-genau neu berechnet.
 * - `"netAmount"`: Brutto bleibt unverändert, das eingegebene Netto wird
 *   gehalten, die Steuerbeträge werden daraus abgeleitet.
 * - `"taxBreakdown"`: Brutto bleibt unverändert, die eingegebenen Steuerbeträge
 *   werden gehalten, `netAmount` und die Nettoanteile werden abgeleitet.
 * - `"date"`, `"merchant"`, `"lineItems"`, `"hospitality"`, `"category"`,
 *   `"none"` oder weggelassen: Es werden keine Beträge umgerechnet.
 *
 * Zusätzlich:
 * - Die Confidence des bearbeiteten Feldes wird auf `1.0` gesetzt
 *   ({@link CONFIDENCE_THRESHOLDS.userEdited}) – ein vom Menschen eingegebener
 *   Wert ist sicher.
 * - `validation` wird immer über {@link validateReceiptMath} neu geschrieben
 *   (inkl. `issues`, `reviewField`, `reviewReason`).
 * - Ein zuvor gesetztes `validation.userConfirmed` bleibt erhalten; solange es
 *   `true` ist, bleibt `needsUserReview` auf `false`.
 * - Bei `ProcessedReceipt` wird `status` zwischen `"ready"` und
 *   `"needs_review"` mitgeführt (andere Status bleiben unangetastet).
 *
 * Die Funktion ist rein: Die Eingabe wird nicht mutiert, es wird ein neues
 * Objekt zurückgegeben. Zusatzfelder von `ProcessedReceipt` bleiben erhalten.
 *
 * @example
 * const updated = recalculateReceipt(receipt, { editedField: "totalAmount" });
 * // updated.taxBreakdown und updated.netAmount passen wieder zum neuen Brutto
 */
export function recalculateReceipt<T extends ReceiptData>(
  receipt: T,
  options: RecalculateOptions = {}
): T {
  const editedField = options.editedField ?? "none";

  const next = { ...receipt } as T;
  const gross = round2(next.totalAmount?.value ?? 0);
  const taxes = (next.taxBreakdown ?? []).map((tax) => ({ ...tax }));

  if (next.totalAmount) {
    next.totalAmount = { ...next.totalAmount, value: gross };
  }

  if (editedField === "totalAmount") {
    if (taxes.length > 0) {
      const redistributed = distributeGross(gross, taxes);
      next.taxBreakdown = redistributed;
      next.netAmount = round2(
        redistributed.reduce((acc, curr) => acc + (curr.netAmount ?? 0), 0)
      );
    } else if (next.netAmount !== null && next.netAmount !== undefined) {
      // Kein Steuerausweis vorhanden -> Netto folgt dem Brutto.
      next.netAmount = gross;
    }
  } else if (editedField === "netAmount") {
    const net = round2(next.netAmount ?? 0);
    next.netAmount = net;
    if (taxes.length > 0) {
      next.taxBreakdown = distributeFromNet(gross, net, taxes);
    }
  } else if (editedField === "taxBreakdown") {
    const derived = deriveNetsFromTaxes(gross, taxes);
    next.taxBreakdown = derived.taxBreakdown;
    next.netAmount = derived.netAmount;
  }

  // Vom Benutzer eingegebene Werte sind sicher.
  if (editedField === "totalAmount" && next.totalAmount) {
    next.totalAmount = {
      ...next.totalAmount,
      confidence: CONFIDENCE_THRESHOLDS.userEdited,
    };
  }
  if (editedField === "date" && next.date) {
    next.date = { ...next.date, confidence: CONFIDENCE_THRESHOLDS.userEdited };
  }
  if (editedField === "merchant" && next.merchant) {
    next.merchant = {
      ...next.merchant,
      confidence: CONFIDENCE_THRESHOLDS.userEdited,
    };
  }

  const result = validateReceiptMath(next);
  const userConfirmed = receipt.validation?.userConfirmed ?? false;

  next.validation = {
    ...(receipt.validation ?? {}),
    isMathValid: result.isMathValid,
    isDuplicateSuspected: receipt.validation?.isDuplicateSuspected ?? false,
    needsUserReview: userConfirmed ? false : result.needsUserReview,
    reviewField: userConfirmed ? "none" : result.reviewField,
    reviewReason: userConfirmed ? null : result.reviewReason,
    issues: result.issues,
    userConfirmed,
  };

  applyStatus(next, receiptNeedsAttention(next));

  return next;
}

/**
 * Markiert einen Beleg als vom Benutzer geprüft und akzeptiert
 * ("Sieht für mich korrekt aus").
 *
 * Setzt `validation.userConfirmed = true` und `needsUserReview = false` und
 * leert `reviewField` / `reviewReason` / `issues`. `isMathValid` wird bewusst
 * **nicht** verfälscht – ein rechnerisch abweichender Beleg bleibt als solcher
 * dokumentiert, verschwindet aber aus der Prüf-Warteschlange.
 *
 * Wichtig für Filter: statt `needsUserReview || !isMathValid` sollte
 * {@link receiptNeedsAttention} verwendet werden, damit bestätigte Belege die
 * Warteschlange tatsächlich verlassen.
 *
 * Rein: mutiert die Eingabe nicht.
 *
 * @example
 * setReceipts((rs) => rs.map((r) => (r.id === id ? confirmReceiptReviewed(r) : r)));
 */
export function confirmReceiptReviewed<T extends ReceiptData>(receipt: T): T {
  const next = { ...receipt } as T;
  next.validation = {
    ...(receipt.validation ?? {}),
    isMathValid: receipt.validation?.isMathValid ?? true,
    isDuplicateSuspected: receipt.validation?.isDuplicateSuspected ?? false,
    needsUserReview: false,
    reviewField: "none",
    reviewReason: null,
    issues: [],
    userConfirmed: true,
  };
  applyStatus(next, false);
  return next;
}

/**
 * Einheitliches Filter-Prädikat für die Prüf-Warteschlange.
 *
 * Ersetzt `r.validation.needsUserReview || !r.validation.isMathValid`, das
 * bestätigte Belege nie losgeworden ist, weil `isMathValid` dauerhaft `false`
 * blieb.
 *
 * @returns `true`, wenn der Beleg noch Aufmerksamkeit braucht.
 */
export function receiptNeedsAttention(
  receipt: Partial<ReceiptData> | null | undefined
): boolean {
  const validation = receipt?.validation;
  if (!validation) return false;
  if (validation.userConfirmed) return false;
  return validation.needsUserReview || !validation.isMathValid;
}

/** Führt `status` bei ProcessedReceipt mit, ohne den Typ hart zu verlangen. */
function applyStatus(receipt: unknown, needsReview: boolean): void {
  const candidate = receipt as { status?: string };
  if (!candidate || typeof candidate.status !== "string") return;
  if (candidate.status === "ready" || candidate.status === "needs_review") {
    candidate.status = needsReview ? "needs_review" : "ready";
  }
}
