"""Generador del PDF de Cotización — diseño profesional, sin dependencias de sistema.

Compone un PDF 1.4 byte a byte (Helvetica / Helvetica-Bold) con barras de sección,
tabla de costos con bordes y filas alternadas, caja de totales y logo incrustado
(JPEG /DCTDecode vía Pillow). El branding (emisor, color) viene de la config por tenant.
"""

from __future__ import annotations

import io
from decimal import Decimal

_W = 612
_H = 792
_ML = 50           # margen izquierdo
_MR = 562          # margen derecho (x)

CONCEPT_LABELS = {
    "flete_internacional": "Flete internacional",
    "transporte_terrestre": "Transporte terrestre",
    "despacho_aduanal": "Despacho aduanal",
    "gastos_destino": "Gastos en destino",
    "otros": "Otros cargos",
}

_TRANSLATE = str.maketrans({"—": "-", "–": "-", "“": '"', "”": '"', "‘": "'", "’": "'", "•": "-", "…": "...", "\t": " "})


def _esc(text) -> str:
    s = ("" if text is None else str(text)).translate(_TRANSLATE)
    s = s.encode("latin-1", "replace").decode("latin-1")
    return s.replace("\\", r"\\").replace("(", r"\(").replace(")", r"\)")


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


def _num(value) -> str:
    return f"{Decimal(str(value or 0)):,.2f}"


# Ancho aprox de una cadena en Helvetica (para alinear a la derecha / truncar)
def _text_w(s: str, size: float, bold: bool = False) -> float:
    return len(s) * size * (0.56 if bold else 0.52)


def _fit(s: str, size: float, max_w: float) -> str:
    s = s or ""
    if _text_w(s, size) <= max_w:
        return s
    while s and _text_w(s + "…", size) > max_w:
        s = s[:-1]
    return s + "…"


def _wrap(text: str, width_chars: int) -> list[str]:
    words = (text or "").split()
    if not words:
        return []
    out, cur = [], ""
    for w in words:
        cand = f"{cur} {w}".strip()
        if len(cand) > width_chars and cur:
            out.append(cur)
            cur = w
        else:
            cur = cand
    if cur:
        out.append(cur)
    return out


def _hex_rgb(hexs: str | None) -> tuple[float, float, float]:
    try:
        h = (hexs or "#12294c").lstrip("#")
        return tuple(int(h[i : i + 2], 16) / 255 for i in (0, 2, 4))  # type: ignore[return-value]
    except Exception:
        return (0.07, 0.16, 0.30)


def _prep_logo(logo_bytes: bytes | None):
    if not logo_bytes:
        return None
    try:
        from PIL import Image

        im = Image.open(io.BytesIO(logo_bytes)).convert("RGB")
        im.thumbnail((600, 300))
        buf = io.BytesIO()
        im.save(buf, format="JPEG", quality=88)
        return buf.getvalue(), im.width, im.height
    except Exception:
        return None


class _Canvas:
    """Acumula operadores de contenido con paginación simple."""

    def __init__(self):
        self.pages: list[list[str]] = [[]]
        self.y = _H

    @property
    def ops(self) -> list[str]:
        return self.pages[-1]

    def new_page(self):
        self.pages.append([])
        self.y = _H - 50

    def ensure(self, needed: float):
        if self.y - needed < 50:
            self.new_page()

    def rect(self, x, y, w, h, rgb):
        r, g, b = rgb
        self.ops.append(f"{r:.3f} {g:.3f} {b:.3f} rg {x:.1f} {y:.1f} {w:.1f} {h:.1f} re f")

    def line(self, x1, y1, x2, y2, rgb, width=0.6):
        r, g, b = rgb
        self.ops.append(f"{width} w {r:.3f} {g:.3f} {b:.3f} RG {x1:.1f} {y1:.1f} m {x2:.1f} {y2:.1f} l S")

    def text(self, x, y, s, size=10, rgb=(0, 0, 0), bold=False, right=False):
        font = "F2" if bold else "F1"
        r, g, b = rgb
        tx = x - _text_w(str(s), size, bold) if right else x
        self.ops.append(f"BT /{font} {size} Tf {r:.3f} {g:.3f} {b:.3f} rg 1 0 0 1 {tx:.1f} {y:.1f} Tm ({_esc(s)}) Tj ET")


def build_quote_pdf(
    *,
    emitter: dict,
    head: dict,
    client: dict,
    cargo: list[tuple[str, str]],
    route: list[tuple[str, str]],
    items: list[dict],
    currency: str,
    subtotal,
    terms: str | None,
    footer: str | None,
    logo_bytes: bytes | None = None,
    accent: str | None = "#12294c",
) -> bytes:
    ACC = _hex_rgb(accent)
    INK = (0.10, 0.15, 0.24)
    GRAY = (0.42, 0.47, 0.55)
    LINE = (0.80, 0.84, 0.90)
    ZEBRA = (0.955, 0.965, 0.980)
    logo = _prep_logo(logo_bytes)

    c = _Canvas()

    # ---------------- Encabezado ----------------
    c.rect(0, _H - 12, _W, 12, ACC)  # banda superior
    logo_bottom = _H - 95
    if logo:
        _, lw, lh = logo
        dw, dh = 150.0, 150.0 * lh / lw
        if dh > 55:
            dh, dw = 55.0, 55.0 * lw / lh
        c.ops.append(f"q {dw:.1f} 0 0 {dh:.1f} {_ML} {logo_bottom:.1f} cm /Im0 Do Q")
    else:
        c.text(_ML, _H - 55, emitter.get("name") or "Emisor", 16, INK, bold=True)

    # Emisor (derecha)
    ex, ey = 320, _H - 42
    c.text(ex, ey, emitter.get("name") or "Emisor", 12, INK, bold=True)
    ey -= 14
    em_lines = []
    if emitter.get("rfc"):
        em_lines.append(f"RFC: {emitter['rfc']}")
    for a in (emitter.get("address") or "").splitlines():
        if a.strip():
            em_lines.append(a.strip())
    contact = "   ".join([x for x in [emitter.get("phone"), emitter.get("email"), emitter.get("website")] if x])
    if contact:
        em_lines.append(contact)
    for ln in em_lines[:5]:
        c.text(ex, ey, _fit(ln, 8.5, _MR - ex), 8.5, GRAY)
        ey -= 11

    # Título + regla
    c.text(_ML, _H - 150, "COTIZACIÓN", 26, INK, bold=True)
    c.line(_ML, _H - 158, _ML + 190, _H - 158, ACC, 2)

    # Panel de datos (derecha)
    px, pw = 320, _MR - 320
    py_top = _H - 128
    ph = 74
    c.rect(px, py_top - ph, pw, ph, ZEBRA)
    c.line(px, py_top, px, py_top - ph, LINE)
    hy = py_top - 15
    info = [
        ("No.", head.get("reference") or "-"),
        ("Fecha", head.get("issue_date") or "-"),
        ("Vigencia", head.get("valid_until") or "-"),
        ("Ejecutivo", head.get("owner") or "-"),
        ("Estatus", str(head.get("status") or "-").capitalize()),
    ]
    for k, v in info:
        c.text(px + 10, hy, f"{k}:", 8.5, GRAY, bold=True)
        c.text(px + 66, hy, _fit(str(v), 9, pw - 76), 9, INK)
        hy -= 12.5

    c.y = _H - 215

    # ---------------- Helpers de sección ----------------
    def section(title: str):
        c.ensure(30)
        c.rect(_ML, c.y - 18, _MR - _ML, 18, ACC)
        c.text(_ML + 8, c.y - 13, title.upper(), 9.5, (1, 1, 1), bold=True)
        c.y -= 26

    def kv_block(pairs: list[tuple[str, str]]):
        rows = [(k, v) for k, v in pairs if v not in (None, "", "None")]
        if not rows:
            return False
        col_w = (_MR - _ML) / 2
        i = 0
        while i < len(rows):
            c.ensure(16)
            for col in range(2):
                if i + col < len(rows):
                    k, v = rows[i + col]
                    x = _ML + 6 + col * col_w
                    c.text(x, c.y - 11, f"{k}:", 9, GRAY, bold=True)
                    c.text(x + _text_w(f"{k}: ", 9, True), c.y - 11, _fit(str(v), 9, col_w - 90), 9, INK)
            c.y -= 16
            i += 2
        c.y -= 4
        return True

    # ---------------- Cliente ----------------
    section("Cliente")
    if not kv_block([
        ("Cliente", client.get("name")), ("RFC", client.get("rfc")),
        ("Correo", client.get("email")), ("Teléfono", client.get("phone")),
    ]):
        c.text(_ML + 6, c.y - 11, "—", 9, GRAY)
        c.y -= 16

    # ---------------- Carga / Ruta (solo si hay datos) ----------------
    if [v for _, v in cargo if v not in (None, "", "None")]:
        section("Información de la carga")
        kv_block(cargo)
    if [v for _, v in route if v not in (None, "", "None")]:
        section("Ruta logística")
        kv_block(route)

    # ---------------- Costos ----------------
    section("Costos cotizados")
    x_con, x_cant, x_tar, x_imp = _ML, 372, 460, _MR - 6
    row_h = 18
    # encabezado de tabla
    c.ensure(row_h)
    c.rect(_ML, c.y - row_h, _MR - _ML, row_h, ACC)
    c.text(x_con + 6, c.y - 13, "Concepto", 9, (1, 1, 1), bold=True)
    c.text(x_cant, c.y - 13, "Cant.", 9, (1, 1, 1), bold=True, right=True)
    c.text(x_tar, c.y - 13, "Tarifa", 9, (1, 1, 1), bold=True, right=True)
    c.text(x_imp, c.y - 13, "Importe", 9, (1, 1, 1), bold=True, right=True)
    c.y -= row_h
    z = False
    for it in items:
        code = str(it.get("concept") or "")
        label = CONCEPT_LABELS.get(code, code)
        desc = str(it.get("description") or "")
        if desc:
            label = f"{label} - {desc}"
        qty = Decimal(str(it.get("quantity") or 0))
        unit = Decimal(str(it.get("unit_sale") or 0))
        amount = (qty * unit).quantize(Decimal("0.01"))
        c.ensure(row_h)
        if z:
            c.rect(_ML, c.y - row_h, _MR - _ML, row_h, ZEBRA)
        c.text(x_con + 6, c.y - 13, _fit(label, 9, x_cant - x_con - 40), 9, INK)
        c.text(x_cant, c.y - 13, _num(qty), 9, INK, right=True)
        c.text(x_tar, c.y - 13, _money(unit), 9, INK, right=True)
        c.text(x_imp, c.y - 13, _money(amount), 9, INK, right=True)
        c.y -= row_h
        z = not z
    if not items:
        c.text(_ML + 6, c.y - 13, "Sin conceptos.", 9, GRAY)
        c.y -= row_h
    # borde de la tabla
    c.line(_ML, c.y, _MR, c.y, LINE)
    c.y -= 12

    # ---------------- Totales (caja derecha) ----------------
    tb_x, tb_w = 360, _MR - 360
    c.ensure(58)
    c.rect(tb_x, c.y - 58, tb_w, 58, ZEBRA)
    c.line(tb_x, c.y, tb_x, c.y - 58, LINE)
    ty = c.y - 16
    c.text(tb_x + 10, ty, "Subtotal", 9.5, GRAY, bold=True)
    c.text(_MR - 8, ty, f"{currency} {_money(subtotal)}", 9.5, INK, right=True)
    ty -= 15
    c.text(tb_x + 10, ty, "IVA", 9.5, GRAY, bold=True)
    c.text(_MR - 8, ty, "según aplique", 9, GRAY, right=True)
    ty -= 6
    c.rect(tb_x, ty - 20, tb_w, 20, ACC)
    c.text(tb_x + 10, ty - 14, "TOTAL", 10, (1, 1, 1), bold=True)
    c.text(_MR - 8, ty - 14, f"{currency} {_money(subtotal)} + IVA", 10, (1, 1, 1), bold=True, right=True)
    c.y -= 70

    # ---------------- Condiciones ----------------
    if terms:
        section("Condiciones comerciales")
        for para in terms.splitlines():
            for ln in (_wrap(para, 108) or [""]):
                c.ensure(13)
                c.text(_ML + 6, c.y - 10, ln, 8.8, GRAY)
                c.y -= 12
        c.y -= 4

    # pie en todas las páginas
    for ops in c.pages:
        if footer:
            r, g, b = GRAY
            ops.append(f"BT /F1 8 Tf {r:.3f} {g:.3f} {b:.3f} rg 1 0 0 1 {_ML} 34 Tm ({_esc(_fit(footer, 8, _MR - _ML))}) Tj ET")
        ops.append(f"{ACC[0]:.3f} {ACC[1]:.3f} {ACC[2]:.3f} rg 0 0 {_W} 6 re f")

    # ---------------- Ensamblado ----------------
    streams = ["\n".join(ops).encode("latin-1", "replace") for ops in c.pages]
    objects: list[bytes] = []

    def add(obj: bytes):
        objects.append(obj)

    n_pages = len(c.pages)
    has_img = 1 if logo else 0
    # numeración: 1 catalog, 2 pages, 3 F1, 4 F2, [5 img], luego páginas y streams
    img_num = 5 if has_img else None
    base = 6 if has_img else 5
    page_nums = list(range(base, base + n_pages))
    content_nums = list(range(base + n_pages, base + 2 * n_pages))

    kids = " ".join(f"{n} 0 R" for n in page_nums)
    add(b"<< /Type /Catalog /Pages 2 0 R >>")
    add(f"<< /Type /Pages /Kids [{kids}] /Count {n_pages} >>".encode("latin-1"))
    add(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>")
    add(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding >>")
    if logo:
        jpeg, lw, lh = logo
        add(
            (
                f"<< /Type /XObject /Subtype /Image /Width {lw} /Height {lh} "
                f"/ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length {len(jpeg)} >>\n"
            ).encode("latin-1") + b"stream\n" + jpeg + b"\nendstream"
        )
    for i in range(n_pages):
        res = "/Font << /F1 3 0 R /F2 4 0 R >>"
        if has_img and i == 0:
            res += f" /XObject << /Im0 {img_num} 0 R >>"
        add(
            (
                f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {_W} {_H}] "
                f"/Resources << {res} >> /Contents {content_nums[i]} 0 R >>"
            ).encode("latin-1")
        )
    for stream in streams:
        add(b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream")

    out = bytearray(b"%PDF-1.4\n%\xe2\xe3\xcf\xd3\n")
    offsets = []
    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)
    total = len(objects) + 1
    out += f"xref\n0 {total}\n".encode("latin-1") + b"0000000000 65535 f \n"
    for off in offsets:
        out += f"{off:010d} 00000 n \n".encode("latin-1")
    out += f"trailer\n<< /Size {total} /Root 1 0 R >>\nstartxref\n{xref_pos}\n%%EOF".encode("latin-1")
    return bytes(out)
