type VatRateCode = '23' | '22' | '8' | '7' | '5' | '4' | '3' | '0' | 'zw' | 'oo' | 'np';

interface InvoiceItemXmlInput {
  name: string;
  quantity: number;
  unit?: string;
  unitPrice: number;
  vatRate: number | string;
  netValue: number;
  vatValue: number;
  grossValue: number;
}

interface PartyXmlInput {
  name: string;
  nip?: string;
  country?: string;
  addressLine1?: string;
  addressLine2?: string;
  postalCode?: string;
  city?: string;
}

interface InvoiceXmlInput {
  invoiceNumber: string;
  issueDate: string;
  saleDate?: string;
  currency: string;
  seller: PartyXmlInput;
  buyer: PartyXmlInput;
  totalNet: number;
  totalVat: number;
  totalGross: number;
  items: InvoiceItemXmlInput[];
}

interface TaxSummaryBucket {
  net: number;
  vat: number;
}

const VAT_CODES = new Set<VatRateCode>(['23', '22', '8', '7', '5', '4', '3', '0', 'zw', 'oo', 'np']);

function escapeXml(value: string): string {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&apos;');
}

function cleanText(value: string | undefined | null, fallback: string): string {
  const trimmed = value?.trim();
  return trimmed ? escapeXml(trimmed) : escapeXml(fallback);
}

function formatAmount(value: number): string {
  return value.toFixed(2);
}

function formatQuantity(value: number): string {
  return value.toFixed(3).replace(/\.?0+$/, '');
}

function formatDate(value: string): string {
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) {
    throw new Error(`Nieprawidłowa data dla KSeF FA(2): ${value}`);
  }

  return date.toISOString().slice(0, 10);
}

function formatDateTime(value: string): string {
  return `${formatDate(value)}T00:00:00Z`;
}

function normalizeCountry(value: string | undefined): string {
  const fallback = value?.trim().toUpperCase() || 'PL';
  return escapeXml(fallback.slice(0, 2));
}

function normalizeVatRate(rate: number | string): VatRateCode {
  if (typeof rate === 'string') {
    const normalized = rate.trim().toLowerCase();
    if (VAT_CODES.has(normalized as VatRateCode)) {
      return normalized as VatRateCode;
    }
  }

  const numeric = Number(rate);
  if (!Number.isFinite(numeric)) {
    throw new Error(`Nieobsługiwana stawka VAT dla KSeF FA(2): ${String(rate)}`);
  }

  const rounded = Math.round(numeric * 100) / 100;
  const integerCode = String(Math.round(rounded));
  if (Math.abs(rounded - Math.round(rounded)) < 0.000001 && VAT_CODES.has(integerCode as VatRateCode)) {
    return integerCode as VatRateCode;
  }

  throw new Error(`Nieobsługiwana stawka VAT dla KSeF FA(2): ${String(rate)}`);
}

function buildAddressXml(party: PartyXmlInput, required: boolean): string {
  const line1 = party.addressLine1?.trim() || `${party.postalCode?.trim() ?? ''} ${party.city?.trim() ?? ''}`.trim();

  if (!line1 && !required) {
    return '';
  }

  const cityPostal = `${party.postalCode?.trim() ?? ''} ${party.city?.trim() ?? ''}`.trim();
  const line2 = party.addressLine2?.trim() || (party.addressLine1?.trim() ? cityPostal : '');

  return `
    <Adres>
      <KodKraju>${normalizeCountry(party.country)}</KodKraju>
      <AdresL1>${cleanText(line1, 'Brak adresu')}</AdresL1>${line2 ? `
      <AdresL2>${escapeXml(line2)}</AdresL2>` : ''}
    </Adres>`;
}

function buildBuyerIdentificationXml(buyer: PartyXmlInput): string {
  const buyerName = cleanText(buyer.name, 'Nabywca');
  const nip = buyer.nip?.trim();
  const idXml = nip && /^\d{10}$/.test(nip) ? `<NIP>${escapeXml(nip)}</NIP>` : '<BrakID>1</BrakID>';
  return `
      <DaneIdentyfikacyjne>
        ${idXml}
        <Nazwa>${buyerName}</Nazwa>
      </DaneIdentyfikacyjne>`;
}

function buildTaxSummary(items: InvoiceItemXmlInput[]): Map<VatRateCode, TaxSummaryBucket> {
  const buckets = new Map<VatRateCode, TaxSummaryBucket>();

  for (const item of items) {
    const rate = normalizeVatRate(item.vatRate);
    const existing = buckets.get(rate) ?? { net: 0, vat: 0 };
    existing.net += item.netValue;
    existing.vat += item.vatValue;
    buckets.set(rate, existing);
  }

  return buckets;
}

function buildFaTaxSections(summary: Map<VatRateCode, TaxSummaryBucket>): string {
  const bucket23 = summary.get('23') ?? { net: 0, vat: 0 };
  const bucket22 = summary.get('22') ?? { net: 0, vat: 0 };
  const net23 = bucket23.net + bucket22.net;
  const vat23 = bucket23.vat + bucket22.vat;

  const bucket8 = summary.get('8') ?? { net: 0, vat: 0 };
  const bucket7 = summary.get('7') ?? { net: 0, vat: 0 };
  const net8 = bucket8.net + bucket7.net;
  const vat8 = bucket8.vat + bucket7.vat;

  const bucket5 = summary.get('5') ?? { net: 0, vat: 0 };
  const bucket4 = summary.get('4') ?? { net: 0, vat: 0 };
  const bucket3 = summary.get('3') ?? { net: 0, vat: 0 };
  const bucket0 = summary.get('0') ?? { net: 0, vat: 0 };
  const bucketZw = summary.get('zw') ?? { net: 0, vat: 0 };
  const bucketNp = summary.get('np') ?? { net: 0, vat: 0 };
  const bucketOo = summary.get('oo') ?? { net: 0, vat: 0 };

  const blocks: string[] = [];

  if (net23 !== 0 || vat23 !== 0) {
    blocks.push(`
      <P_13_1>${formatAmount(net23)}</P_13_1>
      <P_14_1>${formatAmount(vat23)}</P_14_1>`);
  }

  if (net8 !== 0 || vat8 !== 0) {
    blocks.push(`
      <P_13_2>${formatAmount(net8)}</P_13_2>
      <P_14_2>${formatAmount(vat8)}</P_14_2>`);
  }

  if (bucket5.net !== 0 || bucket5.vat !== 0) {
    blocks.push(`
      <P_13_3>${formatAmount(bucket5.net)}</P_13_3>
      <P_14_3>${formatAmount(bucket5.vat)}</P_14_3>`);
  }

  if (bucket4.net !== 0 || bucket4.vat !== 0 || bucket3.net !== 0 || bucket3.vat !== 0) {
    blocks.push(`
      <P_13_4>${formatAmount(bucket4.net + bucket3.net)}</P_13_4>
      <P_14_4>${formatAmount(bucket4.vat + bucket3.vat)}</P_14_4>`);
  }

  if (bucket0.net !== 0) {
    blocks.push(`
      <P_13_6_1>${formatAmount(bucket0.net)}</P_13_6_1>`);
  }

  if (bucketZw.net !== 0) {
    blocks.push(`
      <P_13_7>${formatAmount(bucketZw.net)}</P_13_7>`);
  }

  if (bucketNp.net !== 0) {
    blocks.push(`
      <P_13_8>${formatAmount(bucketNp.net)}</P_13_8>`);
  }

  if (bucketOo.net !== 0) {
    blocks.push(`
      <P_13_10>${formatAmount(bucketOo.net)}</P_13_10>`);
  }

  return blocks.join('');
}

function buildFaRowsXml(items: InvoiceItemXmlInput[]): string {
  return items
    .map((item, index) => {
      const rate = normalizeVatRate(item.vatRate);
      const unit = cleanText(item.unit, 'szt');
      return `
    <FaWiersz>
      <NrWierszaFa>${index + 1}</NrWierszaFa>
      <P_7>${cleanText(item.name, 'Pozycja')}</P_7>
      <P_8A>${unit}</P_8A>
      <P_8B>${formatQuantity(item.quantity)}</P_8B>
      <P_9A>${item.unitPrice.toFixed(8)}</P_9A>
      <P_11>${formatAmount(item.netValue)}</P_11>
      <P_12>${rate}</P_12>
    </FaWiersz>`;
    })
    .join('');
}

export function buildInvoiceXml(input: InvoiceXmlInput): string {
  const sellerNipRaw = input.seller.nip?.trim() ?? '';
  if (!/^\d{10}$/.test(sellerNipRaw)) {
    throw new Error('Brak poprawnego NIP sprzedawcy dla KSeF FA(2)');
  }

  if (input.items.length === 0) {
    throw new Error('Faktura KSeF FA(2) musi zawierać co najmniej jedną pozycję');
  }

  const issueDate = formatDate(input.issueDate);
  const saleDate = input.saleDate ? formatDate(input.saleDate) : issueDate;
  const currency = escapeXml((input.currency || 'PLN').trim().toUpperCase());
  const sellerName = cleanText(input.seller.name, 'Sprzedawca');
  const sellerNip = escapeXml(sellerNipRaw);
  const taxSectionsXml = buildFaTaxSections(buildTaxSummary(input.items));
  const rowsXml = buildFaRowsXml(input.items);
  const placeOfIssue = (input.seller.city ?? '').trim();

  return `<?xml version="1.0" encoding="UTF-8"?>
<Faktura xmlns="http://crd.gov.pl/wzor/2023/06/29/12648/">
  <Naglowek>
    <KodFormularza kodSystemowy="FA (2)" wersjaSchemy="1-0E">FA</KodFormularza>
    <WariantFormularza>2</WariantFormularza>
    <DataWytworzeniaFa>${formatDateTime(input.issueDate)}</DataWytworzeniaFa>
  </Naglowek>
  <Podmiot1>
    <DaneIdentyfikacyjne>
      <NIP>${sellerNip}</NIP>
      <Nazwa>${sellerName}</Nazwa>
    </DaneIdentyfikacyjne>${buildAddressXml(input.seller, true)}
  </Podmiot1>
  <Podmiot2>${buildBuyerIdentificationXml(input.buyer)}${buildAddressXml(input.buyer, false)}
  </Podmiot2>
  <Fa>
    <KodWaluty>${currency}</KodWaluty>
    <P_1>${issueDate}</P_1>${placeOfIssue ? `
    <P_1M>${escapeXml(placeOfIssue)}</P_1M>` : ''}
    <P_2>${cleanText(input.invoiceNumber, 'FV/1/2026')}</P_2>
    <P_6>${saleDate}</P_6>${taxSectionsXml}
    <P_15>${formatAmount(input.totalGross)}</P_15>
    <Adnotacje>
      <P_16>2</P_16>
      <P_17>2</P_17>
      <P_18>2</P_18>
      <P_18A>2</P_18A>
      <Zwolnienie>
        <P_19N>1</P_19N>
      </Zwolnienie>
      <NoweSrodkiTransportu>
        <P_22N>1</P_22N>
      </NoweSrodkiTransportu>
      <P_23>2</P_23>
      <PMarzy>
        <P_PMarzyN>1</P_PMarzyN>
      </PMarzy>
    </Adnotacje>
    <RodzajFaktury>VAT</RodzajFaktury>${rowsXml}
  </Fa>
</Faktura>`;
}

// Lightweight schema sanity checks before real XSD validation pipeline.
export function validateKsefXmlShape(xml: string): void {
  const requiredTags = ['<Faktura', '<Naglowek>', '<Podmiot1>', '<Podmiot2>', '<Fa>', '<Adnotacje>', '<RodzajFaktury>'];
  for (const tag of requiredTags) {
    if (!xml.includes(tag)) {
      throw new Error(`Missing required XML tag: ${tag}`);
    }
  }
}
