<?php
// ============================================================
// api/pdf.php — Server-side PDF generation using FPDF
// ============================================================
// Usage:
//   GET /api/pdf.php?type=invoice&id=123&token=...
//   GET /api/pdf.php?type=quote&id=456&token=...
//
// Returns: application/pdf as a download attachment.
// Selectable text, small file size, no rasterization.
// ============================================================

require_once __DIR__ . '/../core/auth.php';
require_once __DIR__ . '/../core/response.php';
require_once __DIR__ . '/../vendor/fpdf/fpdf.php';

// ── Auth (token comes via ?token= for GET URLs since this is a download link) ──
$user = Auth::requireToken();
$db   = DB::get();

// ── Inputs ──
$type = strtolower($_GET['type'] ?? '');
$id   = (int)($_GET['id'] ?? 0);
if (!in_array($type, ['invoice', 'quote'], true) || $id <= 0) {
    http_response_code(400);
    header('Content-Type: text/plain');
    echo 'Bad request: type must be invoice|quote and id must be a positive integer.';
    exit;
}

try {
    if ($type === 'invoice') {
        $rec = $db->selectOne(
            "SELECT i.*, c.clients_name, c.email, c.cell, c.address, c.vat,
                    COALESCE((SELECT SUM(inc.amount) FROM income inc WHERE inc.invoices_id = i.record_id), 0) AS amount_paid
             FROM invoices i
             LEFT JOIN clients c ON c.record_id = i.clients_id
             WHERE i.record_id = ?", 'i', $id
        );
        if (!$rec) { http_response_code(404); echo 'Invoice not found'; exit; }
        $payments = $db->select(
            "SELECT inc.* FROM income inc
             WHERE inc.invoices_id = ? ORDER BY inc.record_id ASC", 'i', $id
        );
    } else {
        $rec = $db->selectOne(
            "SELECT q.*, c.clients_name, c.email, c.cell, c.address, c.vat
             FROM quotes q
             LEFT JOIN clients c ON c.record_id = q.clients_id
             WHERE q.record_id = ?", 'i', $id
        );
        if (!$rec) { http_response_code(404); echo 'Quote not found'; exit; }
        $payments = [];
    }
    $company = $db->selectOne("SELECT * FROM company WHERE record_id = 1") ?: [];

    // ── Audit trail (matches the rest of the codebase pattern) ──
    Auth::log(
        (int)$user['user_id'],
        'DOWNLOADED PDF FOR ' . strtoupper($type) . ' #' . ($type === 'invoice' ? $rec['invoice_no'] : $rec['quote_no']),
        $type === 'invoice' ? 'invoices' : 'quotes',
        $id
    );

    // ── Build the PDF ──
    $pdf = new InvoicePdf($type, $rec, $company, $payments);
    $pdf->BuildDocument();

    // ── Build filename: Invoice-1234-ClientName.pdf ──
    $clientSlug = preg_replace('/[^A-Za-z0-9_-]+/', '-', trim($rec['clients_name'] ?? ''));
    $clientSlug = trim($clientSlug, '-');
    $number     = $type === 'invoice' ? $rec['invoice_no'] : $rec['quote_no'];
    $filename   = ($type === 'invoice' ? 'Invoice-' : 'Quote-') . (int)$number
                . ($clientSlug !== '' ? '-' . $clientSlug : '') . '.pdf';

    // ── Stream as download (Content-Disposition triggers browser Save dialog) ──
    $pdf->Output('D', $filename);
    exit;

} catch (Exception $e) {
    http_response_code(500);
    header('Content-Type: text/plain');
    echo 'PDF generation failed: ' . $e->getMessage();
    exit;
}


// ════════════════════════════════════════════════════════════════════════════
//  InvoicePdf — builds a complete invoice/quote PDF using FPDF primitives
// ════════════════════════════════════════════════════════════════════════════
class InvoicePdf extends FPDF
{
    /** 'invoice' or 'quote' */
    private string $docType;
    private array  $rec;
    private array  $company;
    private array  $payments;
    private array  $lines;          // parsed line items
    private bool   $isExpired = false;

    // ════════════════════════════════════════════════════════════════════════
    //  CONFIG — edit these to tweak the PDF look. Everything is grouped here
    //  so you don't have to hunt through the rendering methods.
    // ════════════════════════════════════════════════════════════════════════

    // ── Page geometry (mm) ──
    private const PAGE_W    = 210.0;             // A4 portrait width
    private const PAGE_H    = 297.0;             // A4 portrait height
    private const MARGIN    = 10.0;              // outer margin (all 4 sides)
    private const CONTENT_W = self::PAGE_W - self::MARGIN * 2;  // 190mm usable width

    // ── Header (top of page 1) ──
    private const HEADER_LEFT_W       = 118;    // mm — width of company-info column
    private const HEADER_RIGHT_BLOCK_W= 68;     // mm — width of date / number meta block
    private const HEADER_DOCTYPE_PT   = 22;     // pt — "INVOICE" / "QUOTATION" font size
    private const HEADER_LOGO_W       = 50;     // mm — logo max width
    private const HEADER_LOGO_H       = 22;     // mm — logo max height
    private const HEADER_COMPANY_PT   = 8.5;    // pt — company info text size
    private const HEADER_STATUS_PT    = 8;      // pt — status badge text size
    private const HEADER_META_LBL_PT  = 7.5;    // pt — "INVOICE #" / "DATE" labels
    private const HEADER_META_VAL_PT  = 9;      // pt — meta values

    // ── Validity bar (quotes only) ──
    private const VALIDITY_BAR_PT     = 9;      // pt — text size in the bar

    // ── Bill To / Prepared For panel ──
    private const CLIENT_LABEL_PT     = 7.5;    // pt — "BILL TO" / "PREPARED FOR" label
    private const CLIENT_NAME_PT      = 12;     // pt — client name
    private const CLIENT_DETAIL_PT    = 9;      // pt — address, email, phone

    // ── Description / notes panel ──
    private const DESC_PT             = 9;      // pt — description text size

    // ── Items table ──
    private const ITEMS_HEADER_PT     = 8;      // pt — column header text
    private const ITEMS_ROW_PT        = 9.5;    // pt — row text
    private const ITEMS_CELL_MARGIN   = 0.5;    // mm — internal cell padding (default FPDF: 1.0)
    private const ITEMS_LINE_H        = 5;      // mm — line height for wrapped descriptions
    private const ITEMS_ROW_MIN_H     = 7;      // mm — minimum row height for one-line items
    private const ITEMS_COL_QTY_W     = 18;     // mm
    private const ITEMS_COL_PRICE_W   = 28;     // mm
    private const ITEMS_COL_DISC_W    = 22;     // mm
    private const ITEMS_COL_SUB_W     = 30;     // mm
    // 'desc' column gets the rest:  CONTENT_W - QTY - PRICE - DISC - SUB

    // ── Total ──
    private const TOTAL_LBL_PT        = 8;      // pt — "INVOICE TOTAL" label
    private const TOTAL_VAL_PT        = 18;     // pt — currency amount

    // ── Payments / balance ──
    private const PAY_HEADING_PT      = 9;      // pt — "PAYMENT HISTORY" label
    private const PAY_TABLE_HEAD_PT   = 8;      // pt — column headers
    private const PAY_TABLE_ROW_PT    = 9;      // pt — row text
    private const PAY_BAR_PT          = 11;     // pt — "BALANCE DUE" / "PAID IN FULL"

    // ── T&Cs page ──
    private const TERMS_INTRO_PT      = 9;      // pt — italic intro line
    private const TERMS_TITLE_PT      = 10;     // pt — "TERMS & CONDITIONS"
    private const TERMS_BODY_PT       = 9;      // pt — terms body

    // ── Footer ──
    private const FOOTER_PT           = 7.5;    // pt — page-footer text

    // ── Colours (RGB triples) ──
    private const BRAND     = [59, 91, 219];     // #3b5bdb — link / accent
    private const TEXT      = [26, 29, 35];      // #1a1d23 — primary text
    private const TEXT_DIM  = [92, 99, 110];     // #5c636e — secondary text
    private const TEXT_FAINT= [173, 181, 189];   // #adb5bd — labels, footer
    private const BORDER    = [229, 231, 235];   // #e5e7eb — table separators
    private const BG_SOFT   = [248, 249, 250];   // #f8f9fa — Bill To panel bg
    private const PAID      = [43, 138, 62];     // #2b8a3e — green for paid
    private const DUE       = [201, 42, 42];     // #c92a2a — red for due / expired

    // ════════════════════════════════════════════════════════════════════════
    //  END CONFIG
    // ════════════════════════════════════════════════════════════════════════

    public function __construct(string $type, array $rec, array $company, array $payments)
    {
        parent::__construct('P', 'mm', 'A4');
        $this->docType  = $type;
        $this->rec      = $rec;
        $this->company  = $company;
        $this->payments = $payments;
        $this->lines    = $this->parseLines(
            $type === 'invoice' ? ($rec['invoice_content'] ?? '') : ($rec['quote_content'] ?? '')
        );
        if ($type === 'quote' && !empty($rec['date_exp'])) {
            $this->isExpired = strtotime($rec['date_exp']) < strtotime(date('Y-m-d'));
        }

        $this->SetMargins(self::MARGIN, self::MARGIN, self::MARGIN);     // L, T, R in mm
        $this->SetAutoPageBreak(true, self::MARGIN);
        $this->AliasNbPages();             // enables {nb} → total-page-count substitution in Footer()
        $this->SetCreator('ProArt Accounting');
        $this->SetAuthor($company['company_name'] ?? 'ProArt');
        $this->SetTitle(
            ucfirst($type) . ' #' . ($type === 'invoice' ? $rec['invoice_no'] : $rec['quote_no'])
        );
    }

    public function BuildDocument(): void
    {
        $this->AddPage();
        $this->renderHeader();
        $this->Ln(2);
        if ($this->docType === 'quote') $this->renderValidityBar();
        $this->renderClientBlock();
        $this->renderDescription();
        $this->renderItemsTable();
        $this->renderTotal();
        if ($this->docType === 'invoice' && !empty($this->payments)) {
            $this->renderPaymentsBlock();
        }

        // T&Cs always on a new page
        if (!empty($this->company['Tc_Cs'])) {
            $this->AddPage();
            $this->renderTermsPage();
        }
    }

    // ─────────────────── LAYOUT SECTIONS ───────────────────

    private function renderHeader(): void
    {
        $startY = $this->GetY();
        // Right meta block extends HEADER_RIGHT_BLOCK_W left from the right margin.
        // Left column ends with a small gutter before the right block starts.
        $leftW  = self::HEADER_LEFT_W;
        $rightX = self::PAGE_W - self::MARGIN - self::HEADER_RIGHT_BLOCK_W;

        // ── LEFT: logo + company info ──
        $this->logoOrSpacer();
        $this->SetXY(self::MARGIN, $this->GetY() + 2);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->SetFont('Helvetica', '', self::HEADER_COMPANY_PT);
        $companyLines = [];
        if (!empty($this->company['address'])) $companyLines[] = $this->safeText($this->company['address']);
        $emailPhone = trim(
            ($this->company['email'] ?? '')
            . (!empty($this->company['email']) && !empty($this->company['phone']) ? '  |  ' : '')
            . ($this->company['phone'] ?? '')
        );
        if ($emailPhone) $companyLines[] = $this->safeText($emailPhone);
        if (!empty($this->company['reg_no'])) $companyLines[] = 'Reg: ' . $this->safeText($this->company['reg_no']);
        if (!empty($this->company['bank'])) {
            $bankLine = 'Bank: ' . $this->safeText($this->company['bank'])
                . ' | Acc: ' . $this->safeText($this->company['acc'] ?? '')
                . ' | Branch: ' . $this->safeText($this->company['branch'] ?? '');
            $companyLines[] = $bankLine;
        }
        foreach ($companyLines as $line) {
            $this->SetX(self::MARGIN);
            $this->Cell($leftW, 4, $line, 0, 1);
        }
        $leftEndY = $this->GetY();

        // ── RIGHT: doctype heading + meta rows ──
        $rightY = $startY;
        $this->SetXY($rightX, $rightY);
        $this->SetTextColor(...self::TEXT);
        $this->SetFont('Helvetica', 'B', self::HEADER_DOCTYPE_PT);
        $heading = $this->docType === 'invoice' ? 'INVOICE' : 'QUOTATION';
        $this->Cell(0, 9, $heading, 0, 2, 'R');

        // status badge
        $status = strtoupper(trim($this->rec['status'] ?? ''));
        if ($status !== '') {
            $this->SetFont('Helvetica', 'B', self::ITEMS_HEADER_PT);
            $this->SetTextColor(...self::TEXT_DIM);
            $this->Cell(0, 5, $status, 0, 2, 'R');
        }

        $this->Ln(2);

        // meta rows
        $metaRows = $this->getMetaRows();
        $labelW = 28;
        $valueW = 40;
        foreach ($metaRows as $row) {
            $rowY = $this->GetY();
            // label
            $this->SetXY(self::PAGE_W - self::MARGIN - $labelW - $valueW, $rowY);
            $this->SetFont('Helvetica', '', self::HEADER_META_LBL_PT);
            $this->SetTextColor(...self::TEXT_FAINT);
            $this->Cell($labelW, 4.5, strtoupper($row['label']), 0, 0, 'R');
            // value
            $this->SetFont('Courier', 'B', self::HEADER_META_VAL_PT);
            if (!empty($row['expired'])) $this->SetTextColor(...self::DUE);
            else                          $this->SetTextColor(...self::TEXT);
            $this->Cell($valueW, 4.5, $this->safeText($row['value']), 0, 1, 'R');
        }

        // Move Y to whichever column ended lower
        $this->SetY(max($leftEndY, $this->GetY()) + 4);
    }

    private function logoOrSpacer(): void
    {
        // FPDF supports JPG/PNG/GIF (and BMP via extensions). SVG is NOT supported.
        // We try uploaded company logo first (must be PNG/JPG), then logo.png at root,
        // then fall back to the company name as text.
        $candidates = [];
        if (!empty($this->company['logo_path'])) {
            $candidates[] = __DIR__ . '/../' . ltrim($this->company['logo_path'], '/');
        }
        $candidates[] = __DIR__ . '/../logo.png';
        $candidates[] = __DIR__ . '/../assets/logo.png';

        $logoFile = null;
        foreach ($candidates as $c) {
            if (is_file($c) && in_array(strtolower(pathinfo($c, PATHINFO_EXTENSION)), ['png', 'jpg', 'jpeg', 'gif'])) {
                $logoFile = $c;
                break;
            }
        }

        if ($logoFile) {
            try {
                $this->Image($logoFile, self::MARGIN, $this->GetY(), self::HEADER_LOGO_W);
                $this->SetY($this->GetY() + self::HEADER_LOGO_H);
            } catch (Exception $e) {
                // Image load failure → fall back to text
                $this->writeCompanyNameAsHeading();
            }
        } else {
            $this->writeCompanyNameAsHeading();
        }
    }

    private function writeCompanyNameAsHeading(): void
    {
        $this->SetTextColor(...self::TEXT);
        $this->SetFont('Helvetica', 'B', self::TOTAL_VAL_PT);
        $name = $this->safeText($this->company['company_name'] ?? 'Pro Art Moldings');
        $this->Cell(self::HEADER_LEFT_W, 10, $name, 0, 1);
    }

    private function renderValidityBar(): void
    {
        if (empty($this->rec['date_exp'])) return;
        $text = ($this->isExpired ? 'Expired on ' : 'Valid until ')
              . $this->fmtDate($this->rec['date_exp']);
        if ($this->isExpired) {
            $this->SetFillColor(255, 241, 242);
            $this->SetTextColor(...self::DUE);
        } else {
            $this->SetFillColor(238, 242, 255);
            $this->SetTextColor(...self::BRAND);
        }
        $this->SetFont('Helvetica', '', self::VALIDITY_BAR_PT);
        $this->Cell(0, 6, ' ' . $this->safeText($text), 0, 1, 'L', true);
        $this->Ln(3);
    }

    private function renderClientBlock(): void
    {
        $label = $this->docType === 'invoice' ? 'BILL TO' : 'PREPARED FOR';

        // Background panel
        $startY = $this->GetY();
        $this->SetFillColor(...self::BG_SOFT);
        $blockH = 16;
        if (!empty($this->rec['address'])) $blockH += 4;
        $hasContact = !empty($this->rec['email']) || !empty($this->rec['cell']);
        if ($hasContact) $blockH += 4;
        $this->Rect(self::MARGIN, $startY, self::CONTENT_W, $blockH, 'F');

        // Label
        $this->SetXY(18, $startY + 2);
        $this->SetFont('Helvetica', 'B', self::CLIENT_LABEL_PT);
        $this->SetTextColor(...self::TEXT_FAINT);
        $this->Cell(0, 4, $label, 0, 1);

        // Name
        $this->SetX(18);
        $this->SetFont('Helvetica', 'B', self::CLIENT_NAME_PT);
        $this->SetTextColor(...self::TEXT);
        $this->Cell(0, 6, $this->safeText($this->rec['clients_name'] ?? '—'), 0, 1);

        // Address
        if (!empty($this->rec['address'])) {
            $this->SetX(18);
            $this->SetFont('Helvetica', '', self::CLIENT_DETAIL_PT);
            $this->SetTextColor(...self::TEXT_DIM);
            $this->Cell(0, 4, $this->safeText($this->rec['address']), 0, 1);
        }

        // Contact (email + cell)
        if ($hasContact) {
            $this->SetX(18);
            $this->SetFont('Helvetica', '', self::CLIENT_DETAIL_PT);
            $this->SetTextColor(...self::TEXT_DIM);
            $contactBits = [];
            if (!empty($this->rec['email'])) $contactBits[] = $this->rec['email'];
            if (!empty($this->rec['cell']))  $contactBits[] = $this->rec['cell'];
            $this->Cell(0, 4, $this->safeText(implode('  -  ', $contactBits)), 0, 1);
        }

        $this->SetY($startY + $blockH + 4);
    }

    private function renderDescription(): void
    {
        $desc = trim($this->rec['description'] ?? '');
        if ($desc === '') return;
        $this->SetFillColor(252, 252, 253);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->SetFont('Helvetica', 'I', self::DESC_PT);
        $availW = self::CONTENT_W;
        $startY = $this->GetY();
        $this->SetX(self::MARGIN);
        // Use MultiCell so long descriptions wrap properly
        $this->MultiCell($availW, 5, ' ' . $this->safeText($desc), 0, 'L', true);
        $this->Ln(2);
    }

    private function renderItemsTable(): void
    {
        $availW = self::CONTENT_W;     // 159.2mm

        // Tighten cell padding for this table — default 1mm per side gives 2mm
        // gutter between adjacent columns; 0.5mm gives a more compact look.
        $savedCMargin   = $this->cMargin;
        $this->cMargin  = self::ITEMS_CELL_MARGIN;

        // Column widths (mm) — desc gets the leftover. All sourced from CONFIG.
        $colW = [
            'desc'  => $availW - self::ITEMS_COL_QTY_W - self::ITEMS_COL_PRICE_W
                              - self::ITEMS_COL_DISC_W - self::ITEMS_COL_SUB_W,
            'qty'   => self::ITEMS_COL_QTY_W,
            'price' => self::ITEMS_COL_PRICE_W,
            'disc'  => self::ITEMS_COL_DISC_W,
            'sub'   => self::ITEMS_COL_SUB_W,
        ];

        // Header row
        $this->SetFont('Helvetica', 'B', self::ITEMS_HEADER_PT);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->SetFillColor(255, 255, 255);
        $this->SetX(self::MARGIN);
        $this->Cell($colW['desc'],  7, 'DESCRIPTION', 'B', 0, 'L');
        $this->Cell($colW['qty'],   7, 'QTY',         'B', 0, 'R');
        $this->Cell($colW['price'], 7, 'UNIT PRICE',  'B', 0, 'R');
        $this->Cell($colW['disc'],  7, 'DISCOUNT',    'B', 0, 'R');
        $this->Cell($colW['sub'],   7, 'SUBTOTAL',    'B', 1, 'R');

        // Body rows
        $this->SetFont('Helvetica', '', self::ITEMS_ROW_PT);
        $this->SetTextColor(...self::TEXT);
        $this->SetDrawColor(...self::BORDER);

        if (empty($this->lines)) {
            $this->SetX(self::MARGIN);
            $this->SetFont('Helvetica', 'I', self::DESC_PT);
            $this->SetTextColor(...self::TEXT_FAINT);
            $this->Cell($availW, 12, 'No line items recorded', 'B', 1, 'C');
        } else {
            foreach ($this->lines as $line) {
                $descText = $this->safeText($line['description']);

                // Compute how many wrapped lines the description will take so
                // the right-hand columns can be sized to match the row height.
                $this->SetFont('Helvetica', '', self::ITEMS_ROW_PT);
                $nLines = $this->countWrappedLines($colW['desc'], $descText);
                $rowH   = max(self::ITEMS_ROW_MIN_H, $nLines * self::ITEMS_LINE_H);

                $startX = $this->GetX();
                $startY = $this->GetY();

                // Description column (may wrap into multiple lines)
                $this->SetTextColor(...self::TEXT);
                $this->MultiCell($colW['desc'], self::ITEMS_LINE_H, $descText, 0, 'L');

                // Now position back to the row start, just past the desc col,
                // and draw the four right-hand columns at full row height.
                $this->SetXY($startX + $colW['desc'], $startY);

                $this->SetFont('Courier', '', self::ITEMS_ROW_PT);
                $disc = ($line['discount'] > 0) ? round($line['discount'] * 100) . '%' : '-';

                $this->Cell($colW['qty'],   $rowH, $this->fmtNum($line['quantity'], 0), 0, 0, 'R');
                $this->Cell($colW['price'], $rowH, $this->fmtCurrency($line['price']),  0, 0, 'R');
                $this->Cell($colW['disc'],  $rowH, $disc,                                0, 0, 'R');
                $this->SetFont('Courier', 'B', self::ITEMS_ROW_PT);
                $this->Cell($colW['sub'],   $rowH, $this->fmtCurrency($line['total']),  0, 1, 'R');

                // Bottom border on the row (use the deeper of the two: where
                // MultiCell ended vs where the right cells ended).
                $this->SetY(max($startY + $rowH, $startY + $nLines * self::ITEMS_LINE_H));
                $this->Line(self::MARGIN, $this->GetY(), self::MARGIN + $availW, $this->GetY());
            }
        }

        // Restore the default cell padding for any cells drawn after this
        $this->cMargin = $savedCMargin;
        $this->Ln(2);
    }

    private function renderTotal(): void
    {
        $availW   = self::CONTENT_W;
        $totalLbl = $this->docType === 'invoice' ? 'INVOICE TOTAL' : 'QUOTATION TOTAL';

        $this->Ln(2);
        $this->SetX(self::MARGIN);
        $this->SetDrawColor(...self::TEXT);
        $this->SetLineWidth(0.4);
        $this->Line(self::MARGIN, $this->GetY(), self::MARGIN + $availW, $this->GetY());
        $this->SetLineWidth(0.2);
        $this->Ln(3);

        $this->SetX(self::MARGIN);
        $this->SetFont('Helvetica', 'B', self::ITEMS_HEADER_PT);
        $this->SetTextColor(...self::TEXT_FAINT);
        $this->Cell($availW, 4, $totalLbl, 0, 1, 'R');

        $this->SetX(self::MARGIN);
        $this->SetFont('Courier', 'B', self::TOTAL_VAL_PT);
        $this->SetTextColor(...self::TEXT);
        $this->Cell($availW, 9, $this->fmtCurrency((float)$this->rec['total']), 0, 1, 'R');

        $this->Ln(3);
    }

    private function renderPaymentsBlock(): void
    {
        $availW = self::CONTENT_W;

        $this->Ln(4);
        $this->SetX(self::MARGIN);
        $this->SetDrawColor(...self::BORDER);
        $this->Line(self::MARGIN, $this->GetY(), self::MARGIN + $availW, $this->GetY());
        $this->Ln(3);

        $this->SetX(self::MARGIN);
        $this->SetFont('Helvetica', 'B', self::PAY_HEADING_PT);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->Cell(0, 5, 'PAYMENT HISTORY', 0, 1);
        $this->Ln(1);

        // Tighten cell padding to match the items table above
        $savedCMargin   = $this->cMargin;
        $this->cMargin  = self::ITEMS_CELL_MARGIN;

        // Headers
        $colDate = 30; $colNotes = $availW - 30 - 35; $colAmt = 35;
        $this->SetX(self::MARGIN);
        $this->SetFont('Helvetica', 'B', self::ITEMS_HEADER_PT);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->Cell($colDate,  6, 'DATE',   'B', 0, 'L');
        $this->Cell($colNotes, 6, 'NOTES',  'B', 0, 'L');
        $this->Cell($colAmt,   6, 'AMOUNT', 'B', 1, 'R');

        $totalPaid = 0.0;
        $this->SetFont('Helvetica', '', self::PAY_TABLE_ROW_PT);
        $this->SetTextColor(...self::TEXT);
        foreach ($this->payments as $p) {
            $this->SetX(self::MARGIN);
            $amt = (float)($p['amount'] ?? 0);
            $totalPaid += $amt;
            $date = $this->fmtDate($p['date_paid'] ?? $p['payment_date'] ?? $p['date_created'] ?? '');
            $note = $this->safeText($p['notes'] ?? $p['description'] ?? '');
            $this->Cell($colDate,  5, $date, 0, 0, 'L');
            $this->Cell($colNotes, 5, $note, 0, 0, 'L');
            $this->SetFont('Courier', '', self::PAY_TABLE_ROW_PT);
            $this->Cell($colAmt,   5, $this->fmtCurrency($amt), 0, 1, 'R');
            $this->SetFont('Helvetica', '', self::PAY_TABLE_ROW_PT);
        }

        // Restore default cell padding before the balance-due bar
        $this->cMargin = $savedCMargin;

        // Balance due / Paid in full bar
        $balance = (float)$this->rec['total'] - $totalPaid;
        $isPaid  = $balance <= 0.005;

        $this->Ln(3);
        if ($isPaid) {
            $this->SetFillColor(211, 249, 216);
            $this->SetTextColor(...self::PAID);
            $label = 'PAID IN FULL';
            $value = $this->fmtCurrency(0);
        } else {
            $this->SetFillColor(255, 245, 245);
            $this->SetTextColor(...self::DUE);
            $label = 'BALANCE DUE';
            $value = $this->fmtCurrency($balance);
        }
        $this->SetX(self::MARGIN);
        $this->SetFont('Helvetica', 'B', self::PAY_BAR_PT);
        $this->Cell($availW * 0.6, 9, ' ' . $label, 0, 0, 'L', true);
        $this->SetFont('Courier', 'B', self::PAY_BAR_PT);
        $this->Cell($availW * 0.4, 9, $value . ' ', 0, 1, 'R', true);
    }

    private function renderTermsPage(): void
    {
        $availW = self::CONTENT_W;

        $intro = $this->docType === 'invoice'
            ? 'This invoice is payable on the due date stated above. Thank you for your business.'
            : 'This quotation is valid for the period stated above unless otherwise agreed.';

        $this->SetFont('Helvetica', 'I', self::TERMS_INTRO_PT);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->MultiCell($availW, 5, $this->safeText($intro), 0, 'L');
        $this->Ln(4);

        $this->SetFont('Helvetica', 'B', self::TERMS_TITLE_PT);
        $this->SetTextColor(...self::TEXT_DIM);
        $this->Cell(0, 6, 'TERMS & CONDITIONS', 0, 1);
        $this->Ln(2);

        $this->SetFont('Helvetica', '', self::TERMS_BODY_PT);
        $this->SetTextColor(...self::TEXT);
        $this->MultiCell($availW, 4.5, $this->safeText($this->company['Tc_Cs'] ?? ''), 0, 'L');
    }

    // ─────────────────── PAGE FOOTER ───────────────────

    public function Footer(): void
    {
        $this->SetY(-12);
        $this->SetFont('Helvetica', '', self::FOOTER_PT);
        $this->SetTextColor(...self::TEXT_FAINT);
        $left = $this->safeText(
            ($this->company['company_name'] ?? 'Pro Art Moldings')
            . (!empty($this->company['email']) ? '   ' . $this->company['email'] : '')
        );
        $this->Cell(120, 4, $left, 0, 0, 'L');
        $this->Cell(0, 4, 'Page ' . $this->PageNo() . ' of {nb}', 0, 0, 'R');
    }

    // ─────────────────── HELPERS ───────────────────

    /**
     * Parse "desc,qty,price,discount,total;..." into structured array.
     * Mirrors the parseLines() function in the front-end view pages.
     */
    private function parseLines(string $content): array
    {
        $content = trim($content);
        if ($content === '') return [];
        $out = [];
        foreach (explode(';', $content) as $raw) {
            $raw = trim($raw);
            if ($raw === '') continue;
            $p = explode(',', $raw);
            $out[] = [
                'description' => $p[0] ?? '',
                'quantity'    => (float)($p[1] ?? 0),
                'price'       => (float)($p[2] ?? 0),
                'discount'    => (float)($p[3] ?? 0),
                'total'       => (float)($p[4] ?? 0),
            ];
        }
        return $out;
    }

    private function getMetaRows(): array
    {
        if ($this->docType === 'invoice') {
            return [
                ['label' => 'Invoice #', 'value' => (string)($this->rec['invoice_no'] ?? '')],
                ['label' => 'Date',      'value' => $this->fmtDate($this->rec['date_created'] ?? $this->rec['date_sent'] ?? '')],
                ['label' => 'Due Date',  'value' => $this->fmtDate($this->rec['date_due'] ?? '')],
            ];
        }
        return [
            ['label' => 'Quote #',     'value' => (string)($this->rec['quote_no'] ?? '')],
            ['label' => 'Date',        'value' => $this->fmtDate($this->rec['date_created'] ?? $this->rec['date_sent'] ?? '')],
            ['label' => 'Valid Until', 'value' => $this->fmtDate($this->rec['date_exp'] ?? ''), 'expired' => $this->isExpired],
        ];
    }

    private function fmtDate(string $iso): string
    {
        if (!$iso || $iso === '0000-00-00') return '-';
        $t = strtotime($iso);
        if ($t === false) return $iso;
        return date('d/m/Y', $t);
    }

    private function fmtCurrency(float $v): string
    {
        return 'R ' . number_format($v, 2, '.', ',');
    }

    private function fmtNum(float $v, int $dec = 0): string
    {
        return number_format($v, $dec, '.', ',');
    }

    /**
     * FPDF uses ISO-8859-1 by default. Unicode chars (em-dash, smart quotes,
     * etc.) become "?" or garbage. Convert to safe ASCII/Latin-1 equivalents.
     */
    private function safeText(string $s): string
    {
        // Replacements for common Unicode chars
        $map = [
            "\xE2\x80\x93" => '-',  // en-dash
            "\xE2\x80\x94" => '-',  // em-dash
            "\xE2\x80\x98" => "'",  // left single quote
            "\xE2\x80\x99" => "'",  // right single quote
            "\xE2\x80\x9C" => '"',  // left double quote
            "\xE2\x80\x9D" => '"',  // right double quote
            "\xE2\x80\xA6" => '...', // ellipsis
            "\xC2\xB7"     => '-',  // middle dot
            "\xE2\x80\xA2" => '*',  // bullet
        ];
        $s = strtr($s, $map);
        // Convert anything else from UTF-8 to Windows-1252 (FPDF's encoding),
        // dropping anything that doesn't map.
        if (function_exists('iconv')) {
            $conv = @iconv('UTF-8', 'Windows-1252//TRANSLIT//IGNORE', $s);
            if ($conv !== false) return $conv;
        }
        return mb_convert_encoding($s, 'Windows-1252', 'UTF-8');
    }

    /**
     * Count how many lines a string will wrap to inside a given column width,
     * using FPDF's word-breaking rules. Doesn't draw anything — purely a
     * dry-run measurement using GetStringWidth.
     */
    private function countWrappedLines(float $colW, string $text): int
    {
        if ($text === '') return 1;
        $cw = $this->CurrentFont['cw'] ?? null;
        // Inner width available for text (FPDF uses ~2*cMargin padding inside cells)
        $maxW = $colW - 2 * $this->cMargin;

        $lines = 1;
        $cur   = '';
        $words = preg_split('/(\s+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
        foreach ($words as $word) {
            if ($word === '') continue;
            $candidate = $cur . $word;
            if ($this->GetStringWidth($candidate) <= $maxW) {
                $cur = $candidate;
            } else {
                // Word doesn't fit on current line
                if ($cur !== '') {
                    $lines++;
                    $cur = ltrim($word);
                } else {
                    // A single token longer than the column — count one line and continue
                    $lines++;
                    $cur = '';
                }
            }
            // Hard newlines in source
            if (strpos($word, "\n") !== false) {
                $extra = substr_count($word, "\n");
                $lines += $extra;
                $cur = '';
            }
        }
        return max(1, $lines);
    }
}