import { Team } from 'src/teams/entities/team.entity';
import PDFDocument = require('pdfkit');

export interface CashboxExportRow {
  date: string;
  type: string;
  who: string;
  note: string;
  amount: number;
  runningTotal: number;
}

interface RawRow {
  date: string;
  type: string;
  who: string;
  note: string;
  amount: number;
}

export function buildRows(team: Team, from: string, to: string): CashboxExportRow[] {
  const fromTime = new Date(`${from}T00:00:00.000Z`).getTime();
  const toTime = new Date(`${to}T23:59:59.999Z`).getTime();

  const raw: RawRow[] = [];

  for (const transaction of team.transactions ?? []) {
    if (!transaction.type) continue;
    const amount = Number(transaction.amount);
    raw.push({
      date: transaction.date,
      type: transaction.type.name,
      who: 'Teamkasse',
      note: transaction.note,
      amount: transaction.type.name === 'expense' ? -amount : amount,
    });
  }

  for (const player of team.players ?? []) {
    for (const transaction of player.transactions ?? []) {
      if (!transaction.type || transaction.type.name !== 'payment') continue;
      raw.push({
        date: transaction.date,
        type: transaction.type.name,
        who: `${player.firstName} ${player.lastName}`,
        note: transaction.note,
        amount: Number(transaction.amount),
      });
    }
  }

  const filtered = raw
    .filter((row) => {
      const time = new Date(row.date).getTime();
      return time >= fromTime && time <= toTime;
    })
    .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));

  let runningTotal = 0;
  return filtered.map((row) => {
    runningTotal += row.amount;
    return { ...row, runningTotal };
  });
}

export interface CashboxReceivableRow {
  date: string;
  type: string;
  who: string;
  note: string;
  amount: number;
}

const RECEIVABLE_TYPES = new Set(['fine', 'levy', 'fee']);

export function buildReceivableRows(team: Team, from: string, to: string): CashboxReceivableRow[] {
  const fromTime = new Date(`${from}T00:00:00.000Z`).getTime();
  const toTime = new Date(`${to}T23:59:59.999Z`).getTime();

  const raw: CashboxReceivableRow[] = [];
  for (const player of team.players ?? []) {
    for (const transaction of player.transactions ?? []) {
      if (!transaction.type || !RECEIVABLE_TYPES.has(transaction.type.name)) continue;
      raw.push({
        date: transaction.date,
        type: transaction.type.name,
        who: `${player.firstName} ${player.lastName}`,
        note: transaction.note,
        amount: Number(transaction.amount),
      });
    }
  }

  return raw
    .filter((row) => {
      const time = new Date(row.date).getTime();
      return time >= fromTime && time <= toTime;
    })
    .sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
}

const TYPE_LABELS: Record<string, string> = {
  payment: 'Zahlung',
  credit: 'Guthaben',
  expense: 'Ausgabe',
  fine: 'Strafe',
  levy: 'Umlage',
  fee: 'Gebühr',
};

function formatGermanAmount(value: number): string {
  const rounded = Math.sign(value) * Math.round((Math.abs(value) + Number.EPSILON) * 100) / 100;
  const normalized = rounded === 0 ? 0 : rounded;
  return normalized.toFixed(2).replace('.', ',');
}

function escapeCsvField(value: string): string {
  if (/[;"\n\r]/.test(value)) {
    return `"${value.replace(/"/g, '""')}"`;
  }
  return value;
}

export function buildCsv(rows: CashboxExportRow[]): string {
  const lines = ['Datum;Typ;Wer;Notiz;Betrag;Periodensaldo'];
  if (rows.length === 0) {
    lines.push('Keine Buchungen im gewählten Zeitraum');
  } else {
    for (const row of rows) {
      lines.push(
        [
          row.date.slice(0, 10),
          TYPE_LABELS[row.type] ?? row.type,
          escapeCsvField(row.who),
          escapeCsvField(row.note),
          formatGermanAmount(row.amount),
          formatGermanAmount(row.runningTotal),
        ].join(';'),
      );
    }
  }
  return lines.join('\r\n');
}

const PAGE_MARGIN = 40;

const COLORS = {
  headerBand: '#4f8f46',
  headerText: '#ffffff',
  sectionTitle: '#3f7f3c',
  tableHeaderBg: '#e8f2e4',
  tableHeaderText: '#20251f',
  zebra: '#f7f8f2',
  border: '#dde3d8',
  text: '#20251f',
  muted: '#5b6357',
  positive: '#2e7d32',
  negative: '#c1121f',
  receivable: '#9e9e9e',
  footerText: '#8a9186',
};

interface Column {
  label: string;
  width: number;
  align?: 'left' | 'right';
}

const CASH_COLUMNS: Column[] = [
  { label: 'Datum', width: 60 },
  { label: 'Typ', width: 60 },
  { label: 'Wer', width: 110 },
  { label: 'Notiz', width: 150 },
  { label: 'Betrag', width: 65, align: 'right' },
  { label: 'Saldo', width: 65, align: 'right' },
];

const RECEIVABLE_COLUMNS: Column[] = [
  { label: 'Datum', width: 60 },
  { label: 'Typ', width: 70 },
  { label: 'Wer', width: 130 },
  { label: 'Notiz', width: 190 },
  { label: 'Betrag', width: 65, align: 'right' },
];

const ROW_HEIGHT = 20;
const HEADER_ROW_HEIGHT = 22;
const CELL_PADDING = 5;

interface TableRow {
  cells: string[];
  cellColors?: (string | undefined)[];
  boldCells?: boolean[];
}

function tableWidth(columns: Column[]): number {
  return columns.reduce((sum, col) => sum + col.width, 0);
}

function formatAmount(value: number): string {
  return `${formatGermanAmount(value)} €`;
}

// Character-count heuristic instead of doc.widthOfString: keeps row height
// fixed at one line without coupling truncation to the exact font metrics
// used at draw time.
function truncate(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  return `${text.slice(0, maxLength - 1)}…`;
}

function drawTableHeaderRow(doc: PDFKit.PDFDocument, columns: Column[], y: number): void {
  const width = tableWidth(columns);
  doc.rect(PAGE_MARGIN, y, width, HEADER_ROW_HEIGHT).fill(COLORS.tableHeaderBg);
  let colX = PAGE_MARGIN;
  for (const column of columns) {
    doc
      .fillColor(COLORS.tableHeaderText)
      .font('Helvetica-Bold')
      .fontSize(9)
      .text(column.label, colX + CELL_PADDING, y + 6, {
        width: column.width - CELL_PADDING * 2,
        align: column.align ?? 'left',
        lineBreak: false,
      });
    colX += column.width;
  }
  doc.rect(PAGE_MARGIN, y, width, HEADER_ROW_HEIGHT).stroke(COLORS.border);
}

function drawTable(
  doc: PDFKit.PDFDocument,
  columns: Column[],
  rows: TableRow[],
  startY: number,
  pageBottom: number,
): number {
  const width = tableWidth(columns);
  let y = startY;
  drawTableHeaderRow(doc, columns, y);
  y += HEADER_ROW_HEIGHT;

  rows.forEach((row, index) => {
    if (y + ROW_HEIGHT > pageBottom) {
      doc.addPage();
      y = PAGE_MARGIN;
      drawTableHeaderRow(doc, columns, y);
      y += HEADER_ROW_HEIGHT;
    }
    if (index % 2 === 1) {
      doc.rect(PAGE_MARGIN, y, width, ROW_HEIGHT).fill(COLORS.zebra);
    }
    let colX = PAGE_MARGIN;
    row.cells.forEach((cellText, colIndex) => {
      const column = columns[colIndex];
      doc
        .fillColor(row.cellColors?.[colIndex] ?? COLORS.text)
        .font(row.boldCells?.[colIndex] ? 'Helvetica-Bold' : 'Helvetica')
        .fontSize(9)
        .text(cellText, colX + CELL_PADDING, y + 5, {
          width: column.width - CELL_PADDING * 2,
          align: column.align ?? 'left',
          lineBreak: false,
        });
      colX += column.width;
    });
    doc.rect(PAGE_MARGIN, y, width, ROW_HEIGHT).stroke(COLORS.border);
    y += ROW_HEIGHT;
  });

  return y;
}

function drawSummaryRow(
  doc: PDFKit.PDFDocument,
  columns: Column[],
  label: string,
  value: string,
  startY: number,
  pageBottom: number,
  valueColor: string,
): number {
  let y = startY;
  if (y + ROW_HEIGHT > pageBottom) {
    doc.addPage();
    y = PAGE_MARGIN;
  }
  const width = tableWidth(columns);
  const valueColumnWidth = columns[columns.length - 1].width;
  const labelWidth = width - valueColumnWidth - CELL_PADDING * 2;
  doc.rect(PAGE_MARGIN, y, width, ROW_HEIGHT).fill(COLORS.tableHeaderBg);
  doc
    .fillColor(COLORS.tableHeaderText)
    .font('Helvetica-Bold')
    .fontSize(9)
    .text(label, PAGE_MARGIN + CELL_PADDING, y + 5, { width: labelWidth, lineBreak: false });
  doc
    .fillColor(valueColor)
    .font('Helvetica-Bold')
    .fontSize(9)
    .text(value, PAGE_MARGIN + width - valueColumnWidth + CELL_PADDING, y + 5, {
      width: valueColumnWidth - CELL_PADDING * 2,
      align: 'right',
      lineBreak: false,
    });
  doc.rect(PAGE_MARGIN, y, width, ROW_HEIGHT).stroke(COLORS.border);
  return y + ROW_HEIGHT;
}

function addFooters(doc: PDFKit.PDFDocument, teamName: string): void {
  const range = doc.bufferedPageRange();
  const generatedAt = new Date().toLocaleDateString('de-DE');
  for (let i = range.start; i < range.start + range.count; i++) {
    doc.switchToPage(i);
    const footerY = doc.page.height - 25;
    // footerY sits inside the reserved bottom margin (below pdfkit's page
    // maxY()). Without an explicit `height`, pdfkit's LineWrapper measures
    // overflow against the full-page maxY() and calls addPage() here on every
    // iteration - silently appending blank trailing pages. Bounding the text
    // to its own small box (well over the 8pt single-line height needed)
    // keeps the overflow check local and stops that auto-pagination.
    doc
      .fontSize(8)
      .font('Helvetica')
      .fillColor(COLORS.footerText)
      .text(`${teamName} – Kassenbuch-Report, erstellt am ${generatedAt}`, PAGE_MARGIN, footerY, {
        width: doc.page.width - PAGE_MARGIN * 2 - 60,
        height: 20,
        lineBreak: false,
      });
    doc
      .fontSize(8)
      .fillColor(COLORS.footerText)
      .text(`Seite ${i - range.start + 1} von ${range.count}`, doc.page.width - PAGE_MARGIN - 60, footerY, {
        width: 60,
        height: 20,
        align: 'right',
        lineBreak: false,
      });
  }
}

export function buildPdf(
  team: Pick<Team, 'name'>,
  rows: CashboxExportRow[],
  receivableRows: CashboxReceivableRow[],
  from: string,
  to: string,
): Promise<Buffer> {
  return new Promise((resolve, reject) => {
    const doc = new PDFDocument({ margin: PAGE_MARGIN, bufferPages: true, size: 'A4' });
    const chunks: Buffer[] = [];
    doc.on('data', (chunk) => chunks.push(chunk));
    doc.on('end', () => resolve(Buffer.concat(chunks)));
    doc.on('error', reject);

    const pageWidth = doc.page.width;
    const pageBottom = doc.page.height - PAGE_MARGIN - 30;

    doc.rect(0, 0, pageWidth, 90).fill(COLORS.headerBand);
    doc
      .fillColor(COLORS.headerText)
      .font('Helvetica-Bold')
      .fontSize(20)
      .text(team.name, PAGE_MARGIN, 28, { width: pageWidth - PAGE_MARGIN * 2, lineBreak: false });
    doc.font('Helvetica').fontSize(11).text('Kassenbuch-Report', PAGE_MARGIN, 55);
    doc.fontSize(10).text(`Zeitraum: ${from} bis ${to}`, PAGE_MARGIN, 70);

    let y = 110;

    doc.fontSize(13).font('Helvetica-Bold').fillColor(COLORS.sectionTitle);
    doc.text('Kassenbewegungen', PAGE_MARGIN, y);
    y += 20;
    doc
      .fontSize(9)
      .font('Helvetica')
      .fillColor(COLORS.muted)
      .text('Buchungen, die den tatsächlichen Kassenstand verändern.', PAGE_MARGIN, y);
    y += 18;

    if (rows.length === 0) {
      doc
        .fontSize(10)
        .font('Helvetica-Oblique')
        .fillColor(COLORS.muted)
        .text('Keine Buchungen im gewählten Zeitraum.', PAGE_MARGIN, y);
      y += 24;
    } else {
      const cashTableRows: TableRow[] = rows.map((row) => ({
        cells: [
          row.date.slice(0, 10),
          TYPE_LABELS[row.type] ?? row.type,
          truncate(row.who, 20),
          truncate(row.note, 26),
          formatAmount(row.amount),
          formatAmount(row.runningTotal),
        ],
        cellColors: [
          undefined,
          undefined,
          undefined,
          undefined,
          row.amount < 0 ? COLORS.negative : COLORS.positive,
          undefined,
        ],
        boldCells: [false, false, false, false, false, true],
      }));
      y = drawTable(doc, CASH_COLUMNS, cashTableRows, y, pageBottom);

      const endBalance = rows[rows.length - 1].runningTotal;
      y = drawSummaryRow(
        doc,
        CASH_COLUMNS,
        'Endsaldo Kassenbewegungen',
        formatAmount(endBalance),
        y,
        pageBottom,
        endBalance < 0 ? COLORS.negative : COLORS.positive,
      );
      y += 20;
    }

    y += 10;
    if (y + 70 > pageBottom) {
      doc.addPage();
      y = PAGE_MARGIN;
    }
    doc.fontSize(13).font('Helvetica-Bold').fillColor(COLORS.sectionTitle);
    doc.text('Forderungen (Strafen, Beiträge, Umlagen)', PAGE_MARGIN, y);
    y += 20;
    doc
      .fontSize(9)
      .font('Helvetica')
      .fillColor(COLORS.muted)
      .text(
        'Im Zeitraum angelegte Forderungen gegen Mitglieder. Diese verändern den tatsächlichen Kassenstand nicht, solange sie nicht bezahlt wurden.',
        PAGE_MARGIN,
        y,
        { width: tableWidth(RECEIVABLE_COLUMNS) },
      );
    y += 28;

    if (receivableRows.length === 0) {
      doc
        .fontSize(10)
        .font('Helvetica-Oblique')
        .fillColor(COLORS.muted)
        .text('Keine Forderungen im gewählten Zeitraum.', PAGE_MARGIN, y);
      y += 24;
    } else {
      const receivableTableRows: TableRow[] = receivableRows.map((row) => ({
        cells: [
          row.date.slice(0, 10),
          TYPE_LABELS[row.type] ?? row.type,
          truncate(row.who, 24),
          truncate(row.note, 34),
          formatAmount(row.amount),
        ],
        cellColors: [undefined, undefined, undefined, undefined, COLORS.receivable],
      }));
      y = drawTable(doc, RECEIVABLE_COLUMNS, receivableTableRows, y, pageBottom);

      const total = receivableRows.reduce((sum, row) => sum + row.amount, 0);
      y = drawSummaryRow(
        doc,
        RECEIVABLE_COLUMNS,
        'Summe Forderungen',
        formatAmount(total),
        y,
        pageBottom,
        COLORS.receivable,
      );
    }

    addFooters(doc, team.name);

    doc.end();
  });
}
