// ==================== GUTSCHRIFTEN (CREDIT NOTES) ====================
// CRUD für Vertrags-Gutschriften (Subventionen: Geld/Sachwert) inkl.
// USt-Berechnung (pro Gutschrift wählbar: vatRelevant + Basis Netto/Brutto).

import prisma from '../lib/prisma.js';
import { ApiError } from '../utils/apiError.js';
import { assignNextNumber } from './creditNoteNumberRange.service.js';
import { assignNextNumber as assignNextDeliveryNoteNumber } from './deliveryNoteNumberRange.service.js';
import { deleteUploadByRelativePath } from '../utils/fileCleanup.js';
import { Prisma, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '@prisma/client';

const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;

export interface AmountResult {
  amountNet: number;
  amountVat: number;
  amountGross: number;
}

/**
 * Rechnet aus dem eingegebenen Betrag Netto/USt/Brutto aus.
 *  - vatRelevant = false → keine USt: net = brutto = Betrag, USt = 0.
 *  - vatRelevant = true, Basis NETTO  → USt aufschlagen.
 *  - vatRelevant = true, Basis BRUTTO → USt herausrechnen.
 */
export function computeAmounts(params: {
  amount: number;
  vatRelevant: boolean;
  amountBasis: CreditNoteAmountBasis;
  vatRate: number;
}): AmountResult {
  const amount = round2(params.amount);
  if (!params.vatRelevant || params.vatRate <= 0) {
    return { amountNet: amount, amountVat: 0, amountGross: amount };
  }
  const rate = params.vatRate / 100;
  if (params.amountBasis === 'NETTO') {
    const net = amount;
    const vat = round2(net * rate);
    return { amountNet: net, amountVat: vat, amountGross: round2(net + vat) };
  }
  // BRUTTO
  const gross = amount;
  const net = round2(gross / (1 + rate));
  return { amountNet: net, amountVat: round2(gross - net), amountGross: gross };
}

// Betragsloser Sachwert = reine Übergabe/Lieferschein (keine Rechnung,
// keine USt, kein ZUGFeRD; eigene Lieferscheinnummer statt Gutschriftsnummer).
export function isNonMonetary(n: { type: CreditNoteType; amountGross: number }): boolean {
  return n.type === 'SACHWERT' && n.amountGross === 0;
}

// Die je nach aktuellem Typ „gültige" Belegnummer: Lieferscheinnummer bei
// betragslosem Sachwert, sonst Gutschriftsnummer.
export function effectiveNumber(cn: {
  type: CreditNoteType;
  amountGross: number;
  number: string | null;
  deliveryNoteNumber: string | null;
}): string | null {
  return isNonMonetary(cn) ? cn.deliveryNoteNumber : cn.number;
}

const ALLOWED_TYPES = new Set(['GELD', 'SACHWERT']);
const ALLOWED_CUSTOMER_TYPES = new Set(['PRIVAT', 'FIRMA']);
const ALLOWED_BASIS = new Set(['NETTO', 'BRUTTO']);

export interface CreateCreditNoteInput {
  type: string;
  sachwertDescription?: string | null;
  customerType?: string;
  vatRelevant?: boolean;
  amountBasis?: string;
  vatRate?: number;
  amount: number; // eingegebener Betrag (Basis siehe amountBasis)
  currency?: string;
  creditDate: string;
  place?: string | null;
  signedAt?: string | null;
  goodsReceived?: boolean;
  payoutBankCardId?: number | null; // nur GELD: Auszahlungskonto des Kunden
  notes?: string | null;
}

function validateAndNormalize(input: CreateCreditNoteInput) {
  if (!ALLOWED_TYPES.has(input.type)) {
    throw new ApiError(400, 'Ungültige Gutschrift-Art');
  }
  const type = input.type as CreditNoteType;

  if (type === 'SACHWERT' && (!input.sachwertDescription || !input.sachwertDescription.trim())) {
    throw new ApiError(400, 'Bei Sachwerten bitte beschreiben, was gewährt wird.');
  }

  const customerType = (input.customerType && ALLOWED_CUSTOMER_TYPES.has(input.customerType)
    ? input.customerType
    : 'PRIVAT') as CreditNoteCustomerType;

  const amountBasis = (input.amountBasis && ALLOWED_BASIS.has(input.amountBasis)
    ? input.amountBasis
    : 'BRUTTO') as CreditNoteAmountBasis;

  // Leeres/fehlendes Betragsfeld = 0. Sachwerte dürfen betragslos sein
  // (reine Übergabe des Gegenstands als Subvention → keine Rechnung, s.u.);
  // Geld-Gutschriften brauchen dagegen einen echten Betrag.
  const amount = input.amount === undefined || input.amount === null || (input.amount as unknown) === ''
    ? 0
    : Number(input.amount);
  if (!Number.isFinite(amount) || amount < 0) {
    throw new ApiError(400, 'Ungültiger Betrag');
  }
  if (type === 'GELD' && amount <= 0) {
    throw new ApiError(400, 'Bei Geld-Gutschriften bitte einen Betrag größer 0 angeben.');
  }

  // Ohne Betrag gibt es keine USt (nicht-monetärer Sachwert).
  const vatRelevant = amount > 0 && !!input.vatRelevant;
  const vatRate = Number.isFinite(Number(input.vatRate)) ? Number(input.vatRate) : 19;
  if (vatRate < 0 || vatRate > 100) {
    throw new ApiError(400, 'Ungültiger USt-Satz');
  }

  const creditDate = new Date(input.creditDate);
  if (isNaN(creditDate.getTime())) {
    throw new ApiError(400, 'Ungültiges Datum');
  }

  const signedAt = input.signedAt ? new Date(input.signedAt) : null;
  if (signedAt && isNaN(signedAt.getTime())) {
    throw new ApiError(400, 'Ungültiges Unterschriftsdatum');
  }

  const amounts = computeAmounts({ amount, vatRelevant, amountBasis, vatRate });

  // Auszahlungskonto nur bei GELD relevant; bei Sachwert immer leeren.
  let payoutBankCardId: number | null = null;
  if (type === 'GELD' && input.payoutBankCardId != null && input.payoutBankCardId !== ('' as unknown)) {
    const parsed = Number(input.payoutBankCardId);
    if (!Number.isInteger(parsed) || parsed < 1) {
      throw new ApiError(400, 'Ungültiges Auszahlungskonto');
    }
    payoutBankCardId = parsed;
  }

  return {
    type,
    sachwertDescription: type === 'SACHWERT' ? input.sachwertDescription!.trim() : null,
    customerType,
    vatRelevant,
    amountBasis,
    vatRate,
    ...amounts,
    currency: (input.currency || 'EUR').slice(0, 3).toUpperCase(),
    creditDate,
    place: input.place?.trim() || null,
    signedAt,
    goodsReceived: !!input.goodsReceived,
    payoutBankCardId,
    notes: input.notes?.trim() || null,
  };
}

// Stellt sicher, dass die gewählte Bankkarte dem Kunden des Vertrags gehört
// (kein Fremdkonto unterschieben).
async function assertBankCardBelongsToContract(contractId: number, bankCardId: number) {
  const [contract, card] = await Promise.all([
    prisma.contract.findUnique({ where: { id: contractId }, select: { customerId: true } }),
    prisma.bankCard.findUnique({ where: { id: bankCardId }, select: { customerId: true } }),
  ]);
  if (!card || !contract || card.customerId !== contract.customerId) {
    throw new ApiError(400, 'Das gewählte Auszahlungskonto gehört nicht zum Kunden dieses Vertrags.');
  }
}

export async function getCreditNotesByContract(contractId: number) {
  return prisma.creditNote.findMany({
    where: { contractId },
    orderBy: { createdAt: 'desc' },
  });
}

// Gesamtübersicht aller Belege (Gutschriften + Lieferscheine). `customerIds`
// scoped die Liste (Portal: eigene + vertretene Kunden); ohne = alle (Staff).
export async function getAllCreditNotes(opts: {
  customerIds?: number[];
  page?: number;
  limit?: number;
  search?: string;
}) {
  const { customerIds, page = 1, limit = 50, search } = opts;
  const skip = (Math.max(page, 1) - 1) * limit;

  const where: Prisma.CreditNoteWhereInput = {};
  if (customerIds) {
    where.contract = { customerId: { in: customerIds } };
  }
  if (search && search.trim()) {
    const s = search.trim();
    where.OR = [
      { number: { contains: s } },
      { deliveryNoteNumber: { contains: s } },
      { sachwertDescription: { contains: s } },
      { contract: { contractNumber: { contains: s } } },
      { contract: { customer: { customerNumber: { contains: s } } } },
      { contract: { customer: { lastName: { contains: s } } } },
      { contract: { customer: { companyName: { contains: s } } } },
    ];
  }

  const [items, total] = await Promise.all([
    prisma.creditNote.findMany({
      where,
      include: {
        contract: {
          select: {
            id: true,
            contractNumber: true,
            type: true,
            customer: {
              select: { id: true, customerNumber: true, firstName: true, lastName: true, companyName: true },
            },
          },
        },
      },
      orderBy: { createdAt: 'desc' },
      skip,
      take: limit,
    }),
    prisma.creditNote.count({ where }),
  ]);

  return { items, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } };
}

export async function getCreditNoteById(id: number) {
  return prisma.creditNote.findUnique({ where: { id } });
}

// Ermittelt die Formular-Vorbelegung aus dem Kunden:
//  - customerType: Firma vs. Privat (aus Kunde.type)
//  - vatRelevant : Default nur bei Firmenkunde OHNE USt-Befreiung
//    (Kleinunternehmer §19 → wie Privat, keine USt-Vorbelegung).
// WICHTIG: Das ist nur die Vorbelegung. Jede angelegte Gutschrift speichert
// ihren eigenen Snapshot; ein späterer Statuswechsel des Kunden ändert
// bestehende Gutschriften nicht.
export async function getCreditNoteDefaults(contractId: number) {
  const contract = await prisma.contract.findUnique({
    where: { id: contractId },
    select: {
      bankCardId: true,
      addressId: true,
      billingAddressId: true,
      customer: {
        select: {
          type: true,
          vatExempt: true,
          bankCards: {
            where: { isActive: true },
            select: { id: true, iban: true, accountHolder: true, bankName: true, description: true },
            orderBy: { createdAt: 'asc' },
          },
        },
      },
    },
  });
  const isBusiness = contract?.customer?.type === 'BUSINESS';
  const vatExempt = !!contract?.customer?.vatExempt;
  return {
    customerType: (isBusiness ? 'FIRMA' : 'PRIVAT') as CreditNoteCustomerType,
    vatRelevant: isBusiness && !vatExempt,
    // Bankkarten des Kunden für das Auszahlungskonto-Dropdown; die
    // Vertrags-Abbuchkarte als Default-Vorschlag markiert.
    bankCards: contract?.customer?.bankCards ?? [],
    contractBankCardId: contract?.bankCardId ?? null,
    // Für die Empfängeradresse auf dem Beleg: Rechnungsadresse hat Vorrang,
    // sonst Lieferadresse. Ohne beide kann kein Beleg erstellt werden.
    hasRecipientAddress: !!(contract?.billingAddressId || contract?.addressId),
  };
}

export async function createCreditNote(
  contractId: number,
  input: CreateCreditNoteInput,
  createdBy?: string,
) {
  const contract = await prisma.contract.findUnique({
    where: { id: contractId },
    select: { id: true, addressId: true, billingAddressId: true },
  });
  if (!contract) {
    throw new ApiError(404, 'Vertrag nicht gefunden');
  }
  // Empfängeradresse für den Beleg: Rechnungsadresse bevorzugt, sonst
  // Lieferadresse. Ohne beide kann kein Beleg erzeugt werden.
  if (!contract.billingAddressId && !contract.addressId) {
    throw new ApiError(
      400,
      'Keine Rechnungs- oder Lieferadresse am Vertrag hinterlegt. Bitte zuerst eine Adresse zuordnen.',
    );
  }

  const normalized = validateAndNormalize(input);
  if (normalized.payoutBankCardId) {
    await assertBankCardBelongsToContract(contractId, normalized.payoutBankCardId);
  }
  // Betragsloser Sachwert = Lieferschein → eigene Lieferscheinnummer aus dem
  // separaten Kreis; der Gutschrift-Zähler bleibt unangetastet. Sonst echte
  // Gutschrift → Gutschriftsnummer.
  const nonMonetary = isNonMonetary(normalized);
  const number = nonMonetary ? null : await assignNextNumber();
  const deliveryNoteNumber = nonMonetary ? await assignNextDeliveryNoteNumber() : null;

  return prisma.creditNote.create({
    data: {
      contractId,
      number,
      deliveryNoteNumber,
      ...normalized,
      createdBy,
    },
  });
}

export async function updateCreditNote(id: number, input: CreateCreditNoteInput) {
  const existing = await prisma.creditNote.findUnique({ where: { id } });
  if (!existing) {
    throw new ApiError(404, 'Gutschrift nicht gefunden');
  }
  const normalized = validateAndNormalize(input);
  if (normalized.payoutBankCardId) {
    await assertBankCardBelongsToContract(existing.contractId, normalized.payoutBankCardId);
  }

  // Nummern lazy pro Serie vergeben und NIE wieder freigeben (keine Lücken,
  // GoBD): je nach aktuellem Typ bekommt der Beleg bei Bedarf die fehlende
  // Nummer der passenden Serie; eine bereits vergebene Nummer der anderen
  // Serie bleibt am Beleg reserviert (nicht angezeigt), damit ein späteres
  // Zurückwechseln keine neue Nummer verbraucht.
  const nonMonetary = isNonMonetary(normalized);
  let number = existing.number;
  let deliveryNoteNumber = existing.deliveryNoteNumber;
  if (nonMonetary) {
    if (deliveryNoteNumber === null) deliveryNoteNumber = await assignNextDeliveryNoteNumber();
  } else {
    if (number === null) number = await assignNextNumber();
  }

  // Ein evtl. schon erzeugtes PDF ist nach inhaltlicher Änderung veraltet →
  // Pfad leeren. Reihenfolge (R140): erst DB-Update, DANN die alte Datei
  // löschen – schlägt das Update fehl, bleibt die Datei.
  const updated = await prisma.creditNote.update({ where: { id }, data: { ...normalized, number, deliveryNoteNumber, pdfPath: null } });
  deleteUploadByRelativePath(existing.pdfPath);
  return updated;
}

export async function deleteCreditNote(id: number) {
  const existing = await prisma.creditNote.findUnique({ where: { id } });
  if (!existing) {
    throw new ApiError(404, 'Gutschrift nicht gefunden');
  }
  // Reihenfolge (R140): erst den DB-Datensatz löschen, DANN die Dateien –
  // schlägt das DB-Delete fehl, bleiben PDF + Beleg erhalten (kein
  // ins-Leere-zeigender Eintrag).
  const deleted = await prisma.creditNote.delete({ where: { id } });
  deleteUploadByRelativePath(existing.pdfPath);
  deleteUploadByRelativePath(existing.receiptPath);
  return deleted;
}

// Setzt/aktualisiert den Pfad des hochgeladenen Überweisungsbelegs.
export async function setReceiptPath(id: number, receiptPath: string | null) {
  return prisma.creditNote.update({ where: { id }, data: { receiptPath } });
}
