"""Generador de PDF de factura sin dependencias externas.

Se evita ``pdfkit`` (requiere el binario ``wkhtmltopdf``, ausente en el contenedor)
y librerías extra. Produce un PDF válido de una o varias páginas con la fuente
estándar Helvetica (no requiere incrustar fuentes). El texto se codifica en
WinAnsi/Latin-1; los caracteres fuera de ese rango se sustituyen para no romper
el flujo de contenido.
"""

from __future__ import annotations

from decimal import Decimal
from typing import Sequence

_PAGE_W = 612  # carta (8.5in) en puntos
_PAGE_H = 792  # carta (11in)
_MARGIN = 56
_LINE_H = 16
_LINES_PER_PAGE = 42


def _esc(text: str) -> str:
    """Escapa y codifica una cadena para un literal de texto PDF (WinAnsi)."""
    out = (text or "").encode("latin-1", "replace").decode("latin-1")
    return out.replace("\\", r"\\").replace("(", r"\(").replace(")", r"\)")


def _money(value, currency: str) -> str:
    d = Decimal(str(value or 0)).quantize(Decimal("0.01"))
    return f"{currency} {d:,.2f}"


def _wrap(text: str, width: int) -> list[str]:
    text = text or ""
    words = text.split()
    if not words:
        return [""]
    lines: list[str] = []
    current = ""
    for word in words:
        candidate = f"{current} {word}".strip()
        if len(candidate) > width and current:
            lines.append(current)
            current = word
        else:
            current = candidate
    if current:
        lines.append(current)
    return lines


def _build_lines(
    *,
    folio: str,
    issue_date: str,
    due_date: str,
    account_name: str,
    currency: str,
    items: Sequence[dict],
    subtotal,
    tax_rate,
    tax_amount,
    total,
    paid,
    balance,
    bank_info: str | None,
    notes: str | None,
) -> list[tuple[str, int]]:
    """Devuelve una lista de (texto, tamaño_fuente) que compone el cuerpo."""
    L: list[tuple[str, int]] = []
    L.append(("FACTURA", 20))
    L.append((f"Folio: {folio or 's/f'}", 11))
    L.append((f"Fecha de emision: {issue_date or '-'}    Vencimiento: {due_date or '-'}", 11))
    L.append(("", 11))
    L.append((f"Cliente: {account_name or '-'}", 12))
    L.append(("", 11))
    L.append(("Conceptos", 13))
    L.append(("-" * 78, 10))
    L.append(("Cant.  Concepto                                   P. unitario        Importe", 10))
    L.append(("-" * 78, 10))
    for it in items:
        concept = str(it.get("concept") or "")
        desc = str(it.get("description") or "")
        qty = Decimal(str(it.get("quantity") or 0))
        unit = Decimal(str(it.get("unit_amount") or 0))
        amount = (qty * unit).quantize(Decimal("0.01"))
        label = concept if not desc else f"{concept} — {desc}"
        label = label[:42].ljust(42)
        row = f"{qty:>5.2f}  {label}  {unit:>12,.2f}  {amount:>12,.2f}"
        L.append((row, 10))
    L.append(("-" * 78, 10))
    L.append(("", 11))
    L.append((f"Subtotal: {_money(subtotal, currency)}", 11))
    L.append((f"IVA ({Decimal(str(tax_rate or 0)):.2f}%): {_money(tax_amount, currency)}", 11))
    L.append((f"Total: {_money(total, currency)}", 13))
    L.append((f"Pagado: {_money(paid, currency)}", 11))
    L.append((f"Saldo: {_money(balance, currency)}", 12))
    if bank_info:
        L.append(("", 11))
        L.append(("Datos bancarios / de pago", 12))
        for line in _wrap(bank_info, 90):
            L.append((line, 10))
    if notes:
        L.append(("", 11))
        L.append(("Notas", 12))
        for line in _wrap(notes, 90):
            L.append((line, 10))
    return L


def build_invoice_pdf(**kwargs) -> bytes:
    """Construye el PDF de la factura y devuelve los bytes."""
    lines = _build_lines(**kwargs)

    # Paginar el cuerpo
    pages: list[list[tuple[str, int]]] = []
    for i in range(0, len(lines), _LINES_PER_PAGE):
        pages.append(lines[i : i + _LINES_PER_PAGE])
    if not pages:
        pages = [[("FACTURA", 20)]]

    # Un content stream por página
    content_streams: list[bytes] = []
    for page_lines in pages:
        parts = ["BT", f"/F1 11 Tf", f"1 0 0 1 {_MARGIN} {_PAGE_H - _MARGIN} Tm", f"{_LINE_H} TL"]
        first = True
        for text, size in page_lines:
            parts.append(f"/F1 {size} Tf")
            if first:
                parts.append(f"({_esc(text)}) Tj")
                first = False
            else:
                parts.append(f"T* ({_esc(text)}) Tj")
        parts.append("ET")
        content_streams.append("\n".join(parts).encode("latin-1", "replace"))

    # Ensamblado de objetos PDF
    objects: list[bytes] = []

    def add(obj: bytes) -> int:
        objects.append(obj)
        return len(objects)  # número de objeto (1-indexado)

    # Reservamos números: catalog(1), pages(2), font(3), luego páginas y streams
    font_obj_num = 3
    page_obj_nums: list[int] = []
    content_obj_nums: list[int] = []
    # Precalcular números de páginas y streams
    next_num = 4
    for _ in pages:
        page_obj_nums.append(next_num)
        next_num += 1
    for _ in pages:
        content_obj_nums.append(next_num)
        next_num += 1

    kids = " ".join(f"{n} 0 R" for n in page_obj_nums)
    add(f"<< /Type /Catalog /Pages 2 0 R >>".encode("latin-1"))
    add(f"<< /Type /Pages /Kids [{kids}] /Count {len(pages)} >>".encode("latin-1"))
    add(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
    for i, _ in enumerate(pages):
        page_dict = (
            f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {_PAGE_W} {_PAGE_H}] "
            f"/Resources << /Font << /F1 {font_obj_num} 0 R >> >> "
            f"/Contents {content_obj_nums[i]} 0 R >>"
        )
        add(page_dict.encode("latin-1"))
    for stream in content_streams:
        obj = b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream"
        add(obj)

    # Serialización con tabla xref
    out = bytearray()
    out += b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n"
    offsets: list[int] = []
    for i, obj in enumerate(objects, start=1):
        offsets.append(len(out))
        out += f"{i} 0 obj\n".encode("latin-1") + obj + b"\nendobj\n"
    xref_pos = len(out)
    n = len(objects) + 1
    out += f"xref\n0 {n}\n".encode("latin-1")
    out += b"0000000000 65535 f \n"
    for off in offsets:
        out += f"{off:010d} 00000 n \n".encode("latin-1")
    out += f"trailer\n<< /Size {n} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF".encode("latin-1")
    return bytes(out)
