#!/usr/bin/env python3
"""
Reemplazo de wkhtmltopdf usando Playwright (Chromium headless).
No requiere Chrome instalado en el servidor — Playwright incluye su propio Chromium.

Uso idéntico al comando wkhtmltopdf:
  python3 gheadless.py [opciones] <html_input> <pdf_output>

Opciones soportadas:
  -L, --margin-left <mm>      Margen izquierdo en mm
  -R, --margin-right <mm>     Margen derecho en mm
  -T, --margin-top <mm>       Margen superior en mm
  -B, --margin-bottom <mm>    Margen inferior en mm
  -s, --page-size <size>      Tamaño de página: A4 (def), Letter, Legal, A3
  -O, --orientation <o>       Portrait (def) o Landscape
  --header-right <text>       Texto de encabezado derecho ([page], [toPage])
  --header-left <text>        Texto de encabezado izquierdo
  --header-font-size <n>      Tamaño de fuente del header/footer en pt
  --footer-right <text>       Texto pie derecho ([page], [toPage])
  --footer-left <text>        Texto pie izquierdo
  --js-delay <segundos>       Tiempo extra para renderizado JS (def: 2.5)
  --enable-local-file-access, --viewport-size, --zoom, -d, -q  (ignorados)

Requisitos:
  pip install playwright
  PLAYWRIGHT_BROWSERS_PATH=/usr/local/ms-playwright python3 -m playwright install chromium
"""

import sys
import os
import argparse
import tempfile
import time
import base64
import urllib.request
import re as _re

# En el servidor: Chromium de Playwright en path compartido accesible por apache.
# Localmente: si /usr/local/ms-playwright no existe, Playwright usa su path default (~/.cache/ms-playwright).
_SHARED_BROWSERS = '/usr/local/ms-playwright'
if os.path.isdir(_SHARED_BROWSERS) and 'PLAYWRIGHT_BROWSERS_PATH' not in os.environ:
    os.environ['PLAYWRIGHT_BROWSERS_PATH'] = _SHARED_BROWSERS

from playwright.sync_api import sync_playwright


# ---------------------------------------------------------------------------
# Argumentos
# ---------------------------------------------------------------------------

def parse_args():
    p = argparse.ArgumentParser(add_help=False)
    p.add_argument('-L', '--margin-left',   type=float, default=10)
    p.add_argument('-R', '--margin-right',  type=float, default=10)
    p.add_argument('-T', '--margin-top',    type=float, default=10)
    p.add_argument('-B', '--margin-bottom', type=float, default=10)
    p.add_argument('-s', '--page-size',   default='A4')
    p.add_argument('-O', '--orientation', default='Portrait')
    p.add_argument('--header-right',     default='')
    p.add_argument('--header-left',      default='')
    p.add_argument('--header-font-size', type=float, default=8)
    p.add_argument('--footer-right',     default='')
    p.add_argument('--footer-left',      default='')
    p.add_argument('--js-delay',         type=float, default=2.5)
    p.add_argument('-d', '--dpi',                 default=None)
    p.add_argument('--enable-local-file-access', action='store_true')
    p.add_argument('--viewport-size',            default=None)
    p.add_argument('--zoom',                     default=None)
    p.add_argument('-q', '--quiet',              action='store_true')
    p.add_argument('-h', '--help',               action='store_true')
    p.add_argument('html_input', nargs='?', default=None)
    p.add_argument('pdf_output', nargs='?', default=None)
    args, _ = p.parse_known_args()
    if args.help:
        print(__doc__)
        sys.exit(0)
    return args


# ---------------------------------------------------------------------------
# CSS inyectado
# ---------------------------------------------------------------------------

def wk_to_css_content(text):
    """Convierte '[page] de [toPage]' a valor CSS content con counter()."""
    text = text.replace('[page]',   '\x01counter(page)\x01')
    text = text.replace('[toPage]', '\x01counter(pages)\x01')
    text = text.replace('[topage]', '\x01counter(pages)\x01')
    parts = text.split('\x01')
    css_parts = []
    for part in parts:
        if not part:
            continue
        if part.startswith('counter('):
            css_parts.append(part)
        else:
            escaped = part.replace('"', '\\"')
            css_parts.append(f'"{escaped}"')
    return ' '.join(css_parts) if css_parts else 'none'


def build_page_css(args):
    orientation = 'landscape' if args.orientation.lower() == 'landscape' else 'portrait'
    margin_boxes = ''
    font = args.header_font_size
    if args.header_right or args.header_left:
        left  = wk_to_css_content(args.header_left)  if args.header_left  else 'none'
        right = wk_to_css_content(args.header_right) if args.header_right else 'none'
        margin_boxes += f"""
    @top-left  {{ content: {left};  font-size: {font}pt; font-family: Arial, sans-serif; }}
    @top-right {{ content: {right}; font-size: {font}pt; font-family: Arial, sans-serif; }}"""
    if args.footer_right or args.footer_left:
        left  = wk_to_css_content(args.footer_left)  if args.footer_left  else 'none'
        right = wk_to_css_content(args.footer_right) if args.footer_right else 'none'
        margin_boxes += f"""
    @bottom-left  {{ content: {left};  font-size: {font}pt; font-family: Arial, sans-serif; }}
    @bottom-right {{ content: {right}; font-size: {font}pt; font-family: Arial, sans-serif; }}"""

    return f"""
@page {{
    size: {args.page_size} {orientation};
    margin: {args.margin_top}mm {args.margin_right}mm {args.margin_bottom}mm {args.margin_left}mm;
    {margin_boxes}
}}
* {{
    -webkit-print-color-adjust: exact !important;
    print-color-adjust: exact !important;
    color-adjust: exact !important;
}}
html, body {{
    width: 100% !important;
    margin: 0 !important;
    padding: 0 !important;
}}
.container, .container-fluid,
.container-sm, .container-md,
.container-lg, .container-xl, .container-xxl {{
    max-width: 100% !important;
    width: 100% !important;
    padding-left: 0 !important;
    padding-right: 0 !important;
}}
.row {{
    margin-left: 0 !important;
    margin-right: 0 !important;
}}
"""


PAGE_SIZES_INCHES = {
    'a4':     (8.27,  11.69),
    'a3':     (11.69, 16.54),
    'letter': (8.5,   11.0),
    'legal':  (8.5,   14.0),
}


def embed_http_images(html_content):
    """Reemplaza src="http://..." en <img> con data URIs base64."""
    import mimetypes

    LOCAL_SEARCH_DIRS = [
        '/var/www/html/clientes/public/img/seguros',
        '/var/www/html/clientes/public/2/seguros',
        '/var/www/html/clientes/public/img',
    ]

    seen = {}

    def _find_local(url):
        fname = os.path.basename(url.split('?')[0])
        if not fname:
            return None
        for d in LOCAL_SEARCH_DIRS:
            candidate = os.path.join(d, fname)
            if os.path.isfile(candidate):
                return candidate
        return None

    def _replace(m):
        url = m.group(1)
        if url in seen:
            ct, b64 = seen[url]
            return f'src="data:{ct};base64,{b64}"'

        local_path = _find_local(url)
        if local_path:
            try:
                with open(local_path, 'rb') as fh:
                    data = fh.read()
                ct = mimetypes.guess_type(local_path)[0] or 'image/png'
                b64 = base64.b64encode(data).decode()
                seen[url] = (ct, b64)
                return f'src="data:{ct};base64,{b64}"'
            except Exception as e:
                print(f"Warning: no se pudo leer {local_path}: {e}", file=sys.stderr)

        try:
            req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
            with urllib.request.urlopen(req, timeout=10) as resp:
                data = resp.read()
                ct = resp.headers.get('Content-Type', 'image/png').split(';')[0].strip()
            b64 = base64.b64encode(data).decode()
            seen[url] = (ct, b64)
            return f'src="data:{ct};base64,{b64}"'
        except Exception as e:
            print(f"Warning: no se pudo cargar imagen {url}: {e}", file=sys.stderr)
            return m.group(0)

    return _re.sub(r'src=["\']((https?://)[^"\']+)["\']', _replace, html_content)


def inject_css_and_meta(html_content, css):
    meta = '<meta charset="UTF-8">'
    style = f'<style type="text/css">\n{css}\n</style>'
    inject = meta + '\n' + style
    if '</head>' in html_content:
        return html_content.replace('</head>', inject + '\n</head>', 1)
    if '<head>' in html_content:
        return html_content.replace('<head>', '<head>\n' + inject, 1)
    return inject + '\n' + html_content


# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

def main():
    args = parse_args()

    if not args.html_input or not args.pdf_output:
        print("Error: se requiere <html_input> y <pdf_output>", file=sys.stderr)
        sys.exit(1)

    if not os.path.exists(args.html_input):
        print(f"Error: no se encontró {args.html_input}", file=sys.stderr)
        sys.exit(1)

    try:
        with open(args.html_input, 'r', encoding='latin-1') as f:
            html_content = f.read()
    except Exception:
        with open(args.html_input, 'r', encoding='utf-8', errors='replace') as f:
            html_content = f.read()

    css = build_page_css(args)
    html_content = embed_http_images(html_content)

    # Corregir entidades HTML doblemente escapadas.
    # El blade PHP usa {{ }} (auto-escape) en nombres de marcas que la BD ya
    # guarda como entidades (ej: "&#211;" = Ó), resultando en "&amp;#211;" en
    # el HTML. Chrome lo muestra como texto literal "&#211;" en vez de "Ó".
    html_content = _re.sub(r'&amp;(#(?:\d+|x[\da-fA-F]+);)', r'&\1', html_content)
    html_content = _re.sub(r'&amp;([a-zA-Z][a-zA-Z0-9]*;)', r'&\1', html_content)

    html_modified = inject_css_and_meta(html_content, css)

    tmp_fd, tmp_path = tempfile.mkstemp(suffix='.html')

    try:
        with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f:
            f.write(html_modified)

        pdf_abs = os.path.abspath(args.pdf_output)
        file_url = f'file://{os.path.abspath(tmp_path)}'

        # Viewport = ancho efectivo de impresión: paper_width_px / scale.
        # Bootstrap renderiza las columnas al ancho real del PDF.
        _PRINT_SCALE = 0.70
        _size_key = args.page_size.lower()
        _pw, _ph = PAGE_SIZES_INCHES.get(_size_key, (8.27, 11.69))
        if args.orientation.lower() == 'landscape':
            _pw, _ph = _ph, _pw
        _effective_w = max(992, int(_pw * 96 / _PRINT_SCALE))

        # Chrome necesita HOME escribible; el usuario apache tiene HOME=/var/www.
        chrome_env = dict(os.environ)
        chrome_env['HOME'] = '/tmp'
        chrome_env['TMPDIR'] = '/tmp'

        with sync_playwright() as pw:
            browser = pw.chromium.launch(
                args=[
                    '--no-sandbox',
                    '--disable-setuid-sandbox',
                    '--disable-gpu',
                    '--disable-dev-shm-usage',
                    '--no-zygote',
                ],
                env=chrome_env,
                headless=True,
            )
            try:
                context = browser.new_context(
                    viewport={'width': _effective_w, 'height': 1080},
                )
                page = context.new_page()

                # Emular media 'screen' antes de navegar.
                # wkhtmltopdf renderizaba en screen por defecto; sin esto
                # Bootstrap aplica @media print que borra todos los background-color.
                page.emulate_media(media='screen')

                page.goto(file_url, wait_until='load', timeout=30000)

                # Tiempo adicional para JS (charts, DataTables, vis.js, etc.)
                if args.js_delay > 0:
                    time.sleep(args.js_delay)

                # Esperar a que vis.js termine de renderizar el mapa de relaciones.
                vis_state = 'none'
                for _vis_attempt in range(20):
                    vis_state = page.evaluate(
                        "() => window.__vis_initialized"
                        "  ? (window.__vis_rendered ? 'done' : 'waiting')"
                        "  : 'none'"
                    )
                    if vis_state in ('done', 'none'):
                        break
                    time.sleep(1)

                # Capturar canvas vis.js y reemplazar el div con una imagen estática.
                if vis_state == 'done':
                    _data_url = page.evaluate(
                        "() => {"
                        "  var c = document.querySelector('#graf_sociedades canvas');"
                        "  return window.__vis_png || (c ? c.toDataURL('image/png') : null);"
                        "}"
                    )
                    if _data_url and _data_url.startswith('data:image/png'):
                        page.evaluate(
                            "(src) => {"
                            "  var el = document.getElementById('graf_sociedades');"
                            "  if (!el) return;"
                            "  var img = document.createElement('img');"
                            "  img.src = src;"
                            "  img.style.cssText = 'width:100%;display:block;';"
                            "  el.style.height = 'auto';"
                            "  el.innerHTML = '';"
                            "  el.appendChild(img);"
                            "}",
                            _data_url,
                        )

                # Limpiar heights fijos en card-just-text (generan grandes blancos en print).
                page.evaluate(
                    "() => {"
                    "  document.querySelectorAll('.card-just-text').forEach(el => {"
                    "    el.style.removeProperty('height');"
                    "    el.style.removeProperty('min-height');"
                    "  });"
                    "}"
                )

                # Tamaño de página en pulgadas (para que Playwright lo use como fallback
                # si el CSS @page no define size; prefer_css_page_size=True prioriza CSS).
                size_key = args.page_size.lower()
                w, h = PAGE_SIZES_INCHES.get(size_key, (8.27, 11.69))
                if args.orientation.lower() == 'landscape':
                    w, h = h, w

                pdf_bytes = page.pdf(
                    landscape=args.orientation.lower() == 'landscape',
                    display_header_footer=False,
                    print_background=True,
                    width=f'{w}in',
                    height=f'{h}in',
                    margin={'top': '0mm', 'bottom': '0mm', 'left': '0mm', 'right': '0mm'},
                    prefer_css_page_size=True,
                    scale=0.70,
                )

                with open(pdf_abs, 'wb') as f:
                    f.write(pdf_bytes)

            finally:
                browser.close()

        if not os.path.exists(pdf_abs) or os.path.getsize(pdf_abs) == 0:
            raise RuntimeError("PDF vacío o no generado")

        if not args.quiet:
            print(f"OK:{pdf_abs}")
        sys.exit(0)

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)
    finally:
        if os.path.exists(tmp_path):
            os.unlink(tmp_path)


if __name__ == '__main__':
    main()
