import { z } from "zod";

export const TaxBreakdownItemSchema = z.object({
  ratePercent: z.number().describe("Steuersatz in Prozent (z.B. 19 oder 7)"),
  taxAmount: z.number().describe("Steuerbetrag in Euro"),
  netAmount: z.number().nullable().describe("Nettobetrag für diesen Steuersatz"),
});

export const LineItemSchema = z.object({
  description: z.string().describe("Artikelbeschreibung / Position"),
  quantity: z.number().describe("Menge"),
  price: z
    .number()
    .describe("Gesamtpreis dieser Position (Menge ist bereits eingerechnet)"),
  unitPrice: z
    .number()
    .nullable()
    .optional()
    .describe(
      "Einzelpreis pro Stück, nur falls auf dem Beleg separat ausgewiesen. Optional."
    ),
  taxRate: z.number().nullable().describe("Steuersatz für diese Position"),
  /** OCR provenance. Optional so existing receipts and user-created rows remain valid. */
  confidence: z.number().min(0).max(1).optional().describe("OCR-Sicherheit dieser Artikelzeile"),
  sourceView: z
    .number()
    .int()
    .min(1)
    .nullable()
    .optional()
    .describe("Nummer des Detailausschnitts, aus dem die Zeile stammt"),
  rowOrder: z
    .number()
    .int()
    .min(1)
    .nullable()
    .optional()
    .describe("Zeilenreihenfolge innerhalb des Detailausschnitts"),
});

export const ReceiptCategorySchema = z.enum([
  "Bewirtung",
  "Reisekosten & Hotel",
  "Tanken & KFZ",
  "Bürobedarf & IT",
  "Verpflegungsmehraufwand",
  "Material & Einkauf",
  "Sonstiges",
]);

export const DocumentTypeSchema = z.enum([
  "KASSENBON",
  "RECHNUNG",
  "TANKBELEG",
  "BEWIRTUNGSBELEG",
  "PARKTICKET",
  "SONSTIGES",
]);

export const PaymentMethodSchema = z.enum([
  "BAR",
  "EC_KARTE",
  "KREDITKARTE",
  "UEBERWEISUNG",
  "PAYPAL",
  "APPLE_PAY",
  "GOOGLE_PAY",
  "SONSTIGE",
]);

export const ReviewFieldSchema = z.enum([
  "none",
  "date",
  "totalAmount",
  "taxBreakdown",
  "merchant",
  "netAmount",
]);

/**
 * Feld-Kennung für einen einzelnen Validierungsbefund.
 * Obermenge von {@link ReviewFieldSchema} – erweitert um "lineItems".
 * ReviewFieldSchema bleibt bewusst unverändert, damit bestehende UI-Komponenten
 * nicht auf neue Enum-Werte reagieren müssen.
 */
export const ValidationIssueFieldSchema = z.enum([
  "none",
  "date",
  "totalAmount",
  "taxBreakdown",
  "merchant",
  "netAmount",
  "lineItems",
]);

export const ValidationSeveritySchema = z.enum(["error", "warning"]);

export const ValidationIssueSchema = z.object({
  field: ValidationIssueFieldSchema.describe("Betroffenes Feld"),
  severity: ValidationSeveritySchema.describe(
    "error = rechnerisch/inhaltlich falsch, warning = unsicher bzw. prüfenswert"
  ),
  message: z.string().describe("Beschreibung des Problems (deutsch)"),
});

/** Bewirtungsangaben nach § 4 Abs. 5 EStG (Anlass + Teilnehmer). */
export const HospitalitySchema = z.object({
  occasion: z.string().nullable().describe("Anlass der Bewirtung"),
  participants: z
    .string()
    .nullable()
    .describe("Teilnehmer der Bewirtung (Namen, kommasepariert)"),
});

export const ReceiptExtractionSchema = z.object({
  merchant: z.object({
    name: z.string().describe("Name des Händlers oder Ausstellers"),
    address: z.string().nullable().describe("Adresse oder Filiale"),
    taxId: z.string().nullable().describe("USt-IdNr oder Steuernummer"),
    confidence: z.number().min(0).max(1).describe("Confidence Score 0.0 - 1.0"),
  }),
  date: z.object({
    isoDate: z.string().describe("Belegdatum im Format YYYY-MM-DD"),
    time: z.string().nullable().describe("Uhrzeit HH:MM"),
    confidence: z.number().min(0).max(1).describe("Confidence Score 0.0 - 1.0"),
  }),
  documentType: DocumentTypeSchema,
  receiptNumber: z.string().nullable().describe("Beleg- oder Rechnungsnummer"),
  currency: z.string().describe("Währung z.B. EUR"),
  totalAmount: z.object({
    value: z.number().describe("Bruttogesamtbetrag"),
    confidence: z.number().min(0).max(1).describe("Confidence Score 0.0 - 1.0"),
  }),
  netAmount: z.number().nullable().describe("Nettobetrag ohne Steuern"),
  /**
   * Trinkgeld, meist handschriftlich auf Bewirtungsbelegen ergänzt. Bewusst
   * NICHT Teil von totalAmount: Auf das Trinkgeld wird keine MwSt ausgewiesen,
   * es würde die Gegenprobe Netto + MwSt = Brutto zerstören. Der tatsächlich
   * gezahlte Betrag ist totalAmount.value + tipAmount (siehe grossWithTip()).
   * Optional, weil Altdatensätze aus IndexedDB das Feld nicht kennen.
   */
  tipAmount: z
    .number()
    .nullable()
    .optional()
    .describe("Trinkgeld in Euro, zusätzlich zum Rechnungsbetrag. null, wenn keins."),
  taxBreakdown: z.array(TaxBreakdownItemSchema).describe("Aufschlüsselung nach Steuersätzen"),
  lineItems: z.array(LineItemSchema).describe("Einzelpositionen"),
  suggestedCategory: ReceiptCategorySchema.describe("Kategorie"),
  paymentMethod: PaymentMethodSchema.nullable()
    .optional()
    .describe(
      "Zahlungsart, falls auf dem Beleg erkennbar (Bar, EC-Karte, Kreditkarte, …). null, wenn nicht gedruckt."
    ),
  hospitality: HospitalitySchema.nullable()
    .optional()
    .describe(
      "Bewirtungsangaben nach § 4 Abs. 5 EStG. Optional – nur bei Bewirtungsbelegen."
    ),
  validation: z.object({
    isMathValid: z.boolean().describe("Mathematisch valide"),
    isDuplicateSuspected: z.boolean().describe("Duplikatverdacht"),
    needsUserReview: z.boolean().describe("Benötigt Benutzerprüfung"),
    reviewField: ReviewFieldSchema.describe(
      "Zu prüfendes Feld (Befund mit der höchsten Priorität – Legacy-Feld)"
    ),
    reviewReason: z
      .string()
      .nullable()
      .describe("Grund für die Prüfung (Legacy-Feld, spiegelt issues[0])"),
    issues: z
      .array(ValidationIssueSchema)
      .nullable()
      .optional()
      .describe(
        "Alle Validierungsbefunde. Optional – fehlt bei Altdatensätzen aus IndexedDB."
      ),
    userConfirmed: z
      .boolean()
      .nullable()
      .optional()
      .describe(
        "true, wenn der Benutzer den Beleg trotz Befunden ausdrücklich bestätigt hat. Fehlt/null = false."
      ),
  }),
});

/**
 * Modell-Variante von {@link ReceiptExtractionSchema} für Structured Outputs.
 *
 * OpenAI-kompatible Provider (gpt-5.6-*, gpt-4o-*) erzwingen im strict-Modus,
 * dass `required` **jeden** Key aus `properties` enthält. Ein `.optional()` im
 * Zod-Schema erzeugt aber genau eine Lücke darin und der Request scheitert mit
 * HTTP 400 `invalid_json_schema` — die Extraktion fiel dadurch stumm auf den
 * Demo-Generator zurück. Gemini war toleranter, deshalb fiel es vorher nicht auf.
 *
 * Lösung: dem Modell gegenüber sind die vier Felder `nullable` statt `optional`
 * (das ist auch OpenAIs empfohlenes Muster). Nach innen bleibt
 * ReceiptExtractionSchema unverändert, damit Altdatensätze aus IndexedDB, in
 * denen `issues` / `userConfirmed` schlicht fehlen, weiterhin parsen.
 * Der abgeleitete Typ ist zu {@link ReceiptData} zuweisbar (`T | null` ist
 * Teilmenge von `T | null | undefined`).
 */
export const LineItemModelSchema = LineItemSchema.extend({
  unitPrice: z
    .number()
    .nullable()
    .describe(
      "Einzelpreis pro Stück, nur falls auf dem Beleg separat ausgewiesen. null, wenn nicht ausgewiesen."
    ),
  confidence: z.number().min(0).max(1).describe("Realistische OCR-Sicherheit dieser Zeile"),
  sourceView: z
    .number()
    .int()
    .min(1)
    .nullable()
    .describe("Detailbildnummer; null bei einer Gesamtansicht"),
  rowOrder: z
    .number()
    .int()
    .min(1)
    .nullable()
    .describe("Sichtbare Reihenfolge innerhalb des Detailbilds; null bei einer Gesamtansicht"),
});

/** Small strict schema for the independent line-table OCR verification pass. */
export const LineItemVerificationModelSchema = z.object({
  lineItems: z
    .array(LineItemModelSchema)
    .describe("Alle gedruckten Artikelzeilen in exakter Reihenfolge von oben nach unten"),
});

/** One independently transcribed detail view. Views overlap by design. */
export const LineItemViewModelSchema = z.object({
  sourceView: z.number().int().min(1).describe("Die im Prompt angegebene Ausschnittsnummer"),
  lineItems: z
    .array(
      LineItemModelSchema.extend({
        sourceView: z.number().int().min(1),
        rowOrder: z.number().int().min(1),
      })
    )
    .describe("Nur die in diesem Ausschnitt sichtbaren Artikelzeilen von oben nach unten"),
});

/** Strict response used by bounded batches of long-receipt detail views. */
export const LineItemViewBatchModelSchema = z.object({
  views: z.array(LineItemViewModelSchema).describe("Ein Ergebnis je angefordertem Ausschnitt"),
});

/**
 * `validation` ist bewusst nicht Teil des Modell-Schemas: Das Ergebnis wird
 * direkt nach der Extraktion von validateReceiptMath() vollständig überschrieben.
 * Das Modell würde die Felder also nur erfinden, um sie sofort wegzuwerfen —
 * das kostet Output-Tokens und damit Latenz, ohne je gelesen zu werden.
 */
export const ReceiptExtractionModelSchema = ReceiptExtractionSchema.omit({
  validation: true,
}).extend({
  lineItems: z.array(LineItemModelSchema).describe("Einzelpositionen"),
  hospitality: HospitalitySchema.nullable().describe(
    "Bewirtungsangaben nach § 4 Abs. 5 EStG. null, wenn kein Bewirtungsbeleg."
  ),
  tipAmount: z
    .number()
    .nullable()
    .describe(
      "Trinkgeld in Euro, zusätzlich zum Rechnungsbetrag (oft handschriftlich). null, wenn keins ausgewiesen."
    ),
  paymentMethod: PaymentMethodSchema.nullable().describe(
    "Zahlungsart: BAR, EC_KARTE, KREDITKARTE, UEBERWEISUNG, PAYPAL, APPLE_PAY, GOOGLE_PAY, SONSTIGE. null, wenn nicht erkennbar."
  ),
});

/**
 * Tatsächlich gezahlter Betrag: Rechnungsbetrag plus Trinkgeld.
 * `totalAmount.value` bleibt der MwSt-tragende Rechnungsbetrag.
 */
export function grossWithTip(receipt: {
  totalAmount: { value: number };
  tipAmount?: number | null;
}): number {
  const tip = receipt.tipAmount ?? 0;
  return Math.round((receipt.totalAmount.value + tip + Number.EPSILON) * 100) / 100;
}

/** Neutraler Platzhalter, bis validateReceiptMath() den echten Befund setzt. */
export const PENDING_VALIDATION: ReceiptValidationShape = {
  isMathValid: true,
  isDuplicateSuspected: false,
  needsUserReview: false,
  reviewField: "none",
  reviewReason: null,
};

type ReceiptValidationShape = z.infer<typeof ReceiptExtractionSchema>["validation"];

export const BoundingBoxRectSchema = z.object({
  x: z.number().min(0).max(100).describe("Left percentage 0-100"),
  y: z.number().min(0).max(100).describe("Top percentage 0-100"),
  width: z.number().min(0).max(100).describe("Width percentage 0-100"),
  height: z.number().min(0).max(100).describe("Height percentage 0-100"),
  page: z.number().int().min(1).optional().describe("Page number for multi-page PDFs"),
});

export const ReceiptBoundingBoxesSchema = z.object({
  merchant: BoundingBoxRectSchema.optional(),
  date: BoundingBoxRectSchema.optional(),
  receiptNumber: BoundingBoxRectSchema.optional(),
  totalAmount: BoundingBoxRectSchema.optional(),
  netAmount: BoundingBoxRectSchema.optional(),
  taxBreakdown: z.array(BoundingBoxRectSchema).optional(),
  lineItems: z.array(BoundingBoxRectSchema).optional(),
  paymentMethod: BoundingBoxRectSchema.optional(),
});

export type ReceiptData = z.infer<typeof ReceiptExtractionSchema>;
export type TaxBreakdownItem = z.infer<typeof TaxBreakdownItemSchema>;
export type LineItem = z.infer<typeof LineItemSchema>;
export type ReceiptCategory = z.infer<typeof ReceiptCategorySchema>;
export type DocumentType = z.infer<typeof DocumentTypeSchema>;
export type ReviewField = z.infer<typeof ReviewFieldSchema>;
export type ValidationIssueField = z.infer<typeof ValidationIssueFieldSchema>;
export type ValidationSeverity = z.infer<typeof ValidationSeveritySchema>;
export type ValidationIssue = z.infer<typeof ValidationIssueSchema>;
export type Hospitality = z.infer<typeof HospitalitySchema>;
export type ReceiptValidation = ReceiptData["validation"];
export type PaymentMethod = z.infer<typeof PaymentMethodSchema>;
export type BoundingBoxRect = z.infer<typeof BoundingBoxRectSchema>;
export type ReceiptBoundingBoxes = z.infer<typeof ReceiptBoundingBoxesSchema>;

export interface ProcessedReceipt extends ReceiptData {
  id: string;
  /** Pro-only folder this receipt is filed under, if any. */
  projectId?: string | null;
  imageHash: string;
  originalFileName: string;
  fileSizeBytes: number;
  previewUrl?: string;
  createdAt: string;
  updatedAt: string;
  status: "pending" | "processing" | "ready" | "needs_review" | "error";
  rawText?: string;
  boundingBoxes?: ReceiptBoundingBoxes;
  editedFields?: string[] | Record<string, boolean>;
  originalExtraction?: Partial<ReceiptData>;
  /** True for auto-seeded landing-page sample receipts — never set for real uploads. */
  isDemo?: boolean;
}
