#!/usr/bin/env python3
import argparse, json, re, sys
from pathlib import Path

BASE = Path(__file__).resolve().parent
VENDOR = BASE / 'vendor'
if str(VENDOR) not in sys.path:
    sys.path.insert(0, str(VENDOR))
from pypdf import PdfReader, PdfWriter
from pypdf.generic import ArrayObject, DecodedStreamObject, DictionaryObject, NameObject

MARKER = 'PT_V6_1_24_FORMATOS_GNP'
# PT_V6_1_24_FIX2_VENDOR_TYPING_EXTENSIONS: dependencia pypdf embebida para Python 3.9+ sin pip global.

def clean_text(v, n=160):
    return re.sub(r'\s+', ' ', str(v or '').strip())[:n]

def digits(v, n):
    return re.sub(r'\D+', '', str(v or ''))[:n]

def alnum(v, n):
    return ''.join(ch for ch in str(v or '').strip().upper() if ch.isalnum() or ch in '&Ñ')[:n]

def field_chars(text, names):
    return {name: (text[i] if i < len(text) else '') for i,name in enumerate(names)}

def set_fields(writer, page_idx, values):
    vals={k:v for k,v in values.items() if v is not None and str(v)!=''}
    if vals:
        writer.update_page_form_field_values(writer.pages[page_idx], vals, auto_regenerate=True)

def winansi_literal(value: str) -> bytes:
    raw = value.encode('cp1252', errors='replace')
    raw = raw.replace(b'\\', b'\\\\').replace(b'(', b'\\(').replace(b')', b'\\)')
    return b'(' + raw + b')'

def add_financial_line(writer, bank, clabe):
    if len(writer.pages) < 2 or (not bank and not clabe):
        return
    parts=['Datos financieros']
    if bank: parts.append('Banco: '+bank)
    if clabe: parts.append('CLABE: '+clabe)
    text=' - '.join(parts)
    font_size=7.5 if len(text)<=92 else (6.7 if len(text)<=108 else 6.0)
    page=writer.pages[1]
    font_ref=writer._add_object(DictionaryObject({NameObject('/Type'):NameObject('/Font'),NameObject('/Subtype'):NameObject('/Type1'),NameObject('/BaseFont'):NameObject('/Helvetica'),NameObject('/Encoding'):NameObject('/WinAnsiEncoding')}))
    resources_obj=page.get('/Resources')
    if resources_obj is None:
        resources=DictionaryObject(); page[NameObject('/Resources')]=resources
    else: resources=resources_obj.get_object()
    font_obj=resources.get('/Font')
    if font_obj is None:
        fonts=DictionaryObject(); resources[NameObject('/Font')]=fonts
    else: fonts=font_obj.get_object()
    fonts[NameObject('/FPTFIN')]=font_ref
    content=(b'q 0 0 0 rg BT /FPTFIN '+f'{font_size:.1f}'.encode('ascii')+b' Tf 34 28 Td '+winansi_literal(text)+b' Tj ET Q\n')
    stream=DecodedStreamObject(); stream.set_data(content); ref=writer._add_object(stream)
    existing=page.get('/Contents')
    if existing is None: page[NameObject('/Contents')]=ref
    elif isinstance(existing,ArrayObject): existing.append(ref)
    else: page[NameObject('/Contents')]=ArrayObject([existing,ref])

def gen_convenio(reader, writer, d):
    if len(reader.pages)!=3: raise RuntimeError(f'Convenio esperado 3 paginas; actual={len(reader.pages)}')
    rfc=alnum(d.get('rfc'),13); fiscal=clean_text(d.get('situacion_fiscal'),120).upper()
    if rfc:
        if 'MORAL' in fiscal:
            names=[f'RFFCC{i}' for i in range(1,13)]; value=rfc[:12]; section='PM'
        else:
            names=['rfc']+[f'rfc{i}' for i in range(2,14)]; value=rfc[:13]; section='PF'
        set_fields(writer,0,field_chars(value,names))
    else: section='NONE'
    add_financial_line(writer,clean_text(d.get('banco'),80),digits(d.get('clabe'),18))
    return {'RFC_SECTION':section,'RFC_LEN':len(rfc),'BANK_PRESENT':1 if clean_text(d.get('banco'),80) else 0,'CLABE_LEN':len(digits(d.get('clabe'),18))}

def gen_aviso(reader, writer, d):
    if len(reader.pages)!=1: raise RuntimeError('Aviso a la Delegacion esperado 1 pagina')
    vals={
      'MARCA': clean_text(d.get('marca'),40),
      'TIPO': clean_text(d.get('tipo') or d.get('submarca'),40),
      'RFV': clean_text(d.get('rfv'),40),
      'MOTOR': clean_text(d.get('motor'),40),
      'SERIE': clean_text(d.get('serie'),50),
      'MODELO': clean_text(d.get('modelo'),30),
      'PLACAS': clean_text(d.get('placas'),30),
    }
    set_fields(writer,0,vals)
    return {'FIELDS_FILLED':sum(1 for v in vals.values() if v),'MARCA':1 if vals['MARCA'] else 0,'SERIE':1 if vals['SERIE'] else 0,'PLACAS':1 if vals['PLACAS'] else 0}

def gen_cfdi(reader, writer, d):
    if len(reader.pages)!=1: raise RuntimeError('Carta CFDI esperada 1 pagina')
    name=clean_text(d.get('nombre'),120)
    siniestro=clean_text(d.get('siniestro'),60)
    poliza=clean_text(d.get('poliza'),60)
    rfc=alnum(d.get('rfc'),13)
    curp=alnum(d.get('curp'),18)
    vals={
      'Nombre completo': name,
      'Lugar y Fecha de expedición 12': name,
      'Text489': siniestro,
      'Text485': poliza,
      'Text40': clean_text(d.get('codigo_postal'),15),
      'Text41': clean_text(d.get('email'),100),
      'Text39': clean_text(d.get('tipo_identificacion'),80),
      'Text42': clean_text(d.get('numero_identificacion'),80),
      'Text44': clean_text(d.get('marca'),60),
      'Text47': clean_text(d.get('tipo') or d.get('submarca'),60),
      'Text45': clean_text(d.get('modelo'),40),
      'Text49': clean_text(d.get('placas'),40),
      'Text46': clean_text(d.get('serie'),80),
      'Text468': clean_text(d.get('motor'),60),
    }
    lugar_fecha=clean_text(d.get('lugar_fecha'),120)
    if lugar_fecha: vals['Lugar y Fecha de expedición 1']=lugar_fecha
    vals.update(field_chars(rfc,['Text50','Text501','Text5013','4','5','6','7','8','9','10','11','12','13']))
    vals.update(field_chars(curp,['Text51','2_','3_','4_','5_','6_','7_','8_','9_','10_','11_','12_','13_','14_','15_','16_','Text51187_','18_']))
    set_fields(writer,0,vals)
    # Only mark tax regime when an explicit regimen_fiscal value exists.
    reg=clean_text(d.get('regimen_fiscal'),120).upper()
    checks={}
    if reg:
        if 'SUELD' in reg or 'SALARIO' in reg: checks['Sueldo y Salarios']='/On'
        elif 'SIN OBLIG' in reg: checks['Sin obligaciones fiscales']='/On'
        elif 'PREMIO' in reg: checks['Ingresos por la obtención de premios']='/On'
        elif 'SOCIO' in reg or 'ACCIONISTA' in reg: checks['Sueldo y Salarios4']='/On'
        else: vals2={'Text1':clean_text(d.get('regimen_fiscal'),100)}; set_fields(writer,0,vals2)
    if checks: set_fields(writer,0,checks)
    return {'RFC_LEN':len(rfc),'CURP_LEN':len(curp),'SINIESTRO':1 if siniestro else 0,'POLIZA':1 if poliza else 0,'VEHICULO':1 if vals['Text44'] or vals['Text46'] else 0}

def main():
    ap=argparse.ArgumentParser(); ap.add_argument('--tipo',required=True,choices=['convenio','aviso_delegacion','carta_cfdi']); ap.add_argument('--template',required=True); ap.add_argument('--data',required=True); ap.add_argument('--output',required=True); a=ap.parse_args()
    try:
        d=json.loads(Path(a.data).read_text(encoding='utf-8'))
        reader=PdfReader(a.template); writer=PdfWriter(); writer.clone_document_from_reader(reader)
        if a.tipo=='convenio': info=gen_convenio(reader,writer,d)
        elif a.tipo=='aviso_delegacion': info=gen_aviso(reader,writer,d)
        else: info=gen_cfdi(reader,writer,d)
        out=Path(a.output); out.parent.mkdir(parents=True,exist_ok=True)
        with out.open('wb') as f: writer.write(f)
        if out.stat().st_size<5000 or out.read_bytes()[:5]!=b'%PDF-': raise RuntimeError('PDF generado invalido')
        print(MARKER); print('PT_V624_TIPO='+a.tipo)
        for k,v in info.items(): print(f'PT_V624_{k}={v}')
        print('PT_V6_1_24_FORMATO_GENERATED_OK')
        return 0
    except Exception as e:
        print('ERROR_GENERAR_FORMATO='+str(e),file=sys.stderr); return 2
if __name__=='__main__': raise SystemExit(main())
