import shutil
import base64
import pdfkit
from pathlib import Path
from decimal import Decimal
from typing import Tuple, List, Callable, Optional

from jinja2 import Environment, FileSystemLoader, select_autoescape
from fastapi import HTTPException
from pydantic import ValidationError
from sqlalchemy.orm import Session

# --- MODELOS ---
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics 
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
from api.v1.modules.a76.items.line_customs.models import LineCustom
from api.v1.modules.a76.clients_and_providers.models import (
    ClientProvider, ClientProviderAddress, ClientProviderPrograms
)
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.pedmientos.models import Pedimentos
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
from api.v1.modules.a76.items.models import LineItem 

# --- TRANSPORTATION MODELS ---
from api.v1.modules.a76.transportation.transporters.models import Transporter
from api.v1.modules.a76.transportation.vehicles.models import Vehicle
from api.v1.modules.a76.transportation.trailers.models import Trailer
from api.v1.modules.a76.transportation.drivers.models import Driver

# --- MODELO DE FRACCIONES ---
from api.v1.modules.a76.general_catalogs.fractions.tariff_fractions.models import TariffFraction

# --- SCHEMAS ---
from .schemas import (
    ClienteSchema, PartidaSchema, TotalesSchema, 
    FacturaSchema, PackingListSchema
)

class PackingListService:
    def __init__(self):
        self.template_dir = Path(__file__).parent / "templates"
        self.jinja_env = Environment(
            loader=FileSystemLoader(self.template_dir),
            autoescape=select_autoescape(['html', 'xml'])
        )
        self.template = self.jinja_env.get_template('packing_list.html')

    def _get_wkhtmltopdf_config(self):
        # List of possible paths
        paths = [
             shutil.which("wkhtmltopdf"),
             "/usr/local/bin/wkhtmltopdf",
             "/usr/bin/wkhtmltopdf",
             "C:\\Program Files\\wkhtmltopdf\\bin\\wkhtmltopdf.exe"
        ]
        
        path = next((p for p in paths if p and Path(p).exists()), None)

        if not path:
             if shutil.which("echo"): 
                 print("WARNING: wkhtmltopdf not found, PDF generation will fail.")
             raise RuntimeError(f"wkhtmltopdf binary not found. Searched in: {paths}")
             
        return pdfkit.configuration(wkhtmltopdf=path)

    def formatear_numero(self, valor, decimales: int = 2):
        """
        Formatea un número con separadores de miles y decimales especificados.
        Retorna una cadena formateada para mostrar en reportes.
        """
        if valor is None:
            valor = 0.0
        try:
            num = round(float(valor), decimales)
            # Formatear con separadores de miles y decimales
            return f"{num:,.{decimales}f}"
        except:
            return f"0.{'0' * decimales}"

    def _format_fraccion_fallback(self, fraccion_raw: str) -> str:
        if not fraccion_raw or len(fraccion_raw) < 8:
            return fraccion_raw or ""
        return f"{fraccion_raw[:4]}.{fraccion_raw[4:6]}.{fraccion_raw[6:]}"

    def _obtener_datos_cliente(self, db: Session, client_id: int, rol: str) -> ClienteSchema:
        main = db.query(ClientProvider).filter(ClientProvider.id == client_id).first()
        if not main:
            return ClienteSchema(header=rol, nombre="Desconocido", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="MEX")
        
        addr = db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first()
        prog = db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first()

        return ClienteSchema(
            header=rol,
            nombre=(main.name or main.short_name) or "S/N",
            direccion=(addr.streets or "") if addr else "", 
            num_exterior=(addr.exterior_number or "") if addr else "",
            num_interior=(addr.interior_number or "") if addr else "",
            colonia=(addr.neighborhood or "") if addr else "",
            codigo_postal=(addr.postal_code or "") if addr else "",
            ciudad=(addr.city or "") if addr else "",
            estado=(addr.state or "") if addr else "",
            pais=(addr.country or "MEX") if addr else "MEX",
            tax_id=prog.tax_id if (prog and prog.tax_id) else (getattr(main, 'rfc', "") or ""),
            programa="IMMEX" if (prog and prog.program) else "", 
            autorizacion=prog.program_number if prog else "",
            prosec=prog.prosec_authorization if (prog and prog.prosec and prog.prosec_authorization) else "",
            reg_emp=prog.val_certified_company_registry if (prog and hasattr(prog, 'val_certified_company_registry')) else (
                prog.certified_company_registry if (prog and prog.certified_company_registry) else ""
            ),
            cert=prog.is_certified_company if (prog and prog.is_certified_company) else "" 
        )

    def get_packing_list_data(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> PackingListSchema:
        try:
            if progress_callback: progress_callback(10, "Buscando factura...")
            header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id, InvoiceHeader.company_id == company_id).first()
            if not header: raise HTTPException(status_code=404, detail="Factura no encontrada")

            compliance = header.compliance_mx 
            logistics = header.logistics if header.logistics else None
            financials = header.financials if header.financials else None
            
            if progress_callback: progress_callback(20, "Obteniendo datos de pedimento...")
            pedimento_id = compliance.pedimento_id if (compliance and compliance.pedimento_id) else header.related_doc_id
            pedimento = db.query(Pedimentos).filter(Pedimentos.id == pedimento_id).first() if pedimento_id else None
            
            if progress_callback: progress_callback(30, "Obteniendo cliente y proveedor...")
            proveedor_id = compliance.provider_id if compliance else None
            cliente_proveedor = self._obtener_datos_cliente(db, proveedor_id, "Proveedor / supplier:") if proveedor_id else ClienteSchema(header="Proveedor", nombre="No Asignado", direccion="", tax_id="", codigo_postal="", ciudad="", estado="", pais="")

            nombre_agente = ""
            if compliance and compliance.customs_broker_id:
                broker = db.query(CustomsBroker).filter(CustomsBroker.id == compliance.customs_broker_id).first()
                if broker: nombre_agente = broker.name

            company = db.query(Company).filter(Company.id == header.company_id).first()
            # Datos Default (Company/Importer)
            cliente_default = ClienteSchema(
                header="Importer / Consignee:",
                nombre=getattr(company, 'name', "Empresa Local"),
                direccion="DOMICILIO FISCAL",
                num_exterior="", colonia="", codigo_postal="", ciudad="", estado="", pais="MEX",
                tax_id=getattr(company, 'rfc', ""),
                programa=getattr(company, 'program', "IMMEX"), autorizacion=getattr(company, 'program_number', "")
            )

            # Left Side Logic (Consignatario / Sold To)
            cliente_vendido = cliente_default
            if compliance and compliance.sold_to_id:
                raw = (compliance.sold_to_header or "").upper()
                if "CONSIGN" in raw:
                    clean_header = "Consignee / Consignatario:"
                else:
                    clean_header = "Sold To / Vendido a:"
                
                cliente_vendido = self._obtener_datos_cliente(db, compliance.sold_to_id, clean_header)
            
            # Right Side Logic (Enviado A / Shipped To)
            cliente_enviado = cliente_default
            if compliance and compliance.shipped_to_id:
                clean_header_shipped = "Shipped To / Enviado a:"
                cliente_enviado = self._obtener_datos_cliente(db, compliance.shipped_to_id, clean_header_shipped)

            remesa_valor = str(compliance.remesa) if (compliance and compliance.remesa) else ""
            acuse_valor = str(compliance.edocument) if (compliance and compliance.edocument) else "N/A"

            patente_val = ""
            if pedimento and pedimento.license:
                patente_val = pedimento.license
            elif 'broker' in locals() and broker and broker.license:
                patente_val = broker.license

            # --- Transport Data Fetching ---
            transporte_txt = str(logistics.transport_type) if (logistics and logistics.transport_type) else ""
            num_transporte_val = (logistics.trailer_num or "") if logistics else ""
            
            placas_val = (logistics.license_plate or "") if logistics else "" 
            placas_remolque_val = ""
            transportista_val = (logistics.carrier_id or "") if logistics else ""
            caat_val = ""
            scac_val = ""
            licencia_cond_val = ""

            if logistics:
                # 1. Transporter (CAAT / SCAC)
                if logistics.carrier_id:
                    transporter_obj = db.query(Transporter).filter(Transporter.transporter_key == logistics.carrier_id).first()
                    if transporter_obj:
                        caat_val = transporter_obj.caat_code or ""
                        scac_val = transporter_obj.transport_code or "" 
                        transportista_val = transporter_obj.name or logistics.carrier_id

                # 2. Vehicle (Placas Tracto)
                if logistics.transport_id:
                    veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.transport_id).first()
                    if veh_obj:
                         placas_val = veh_obj.plate_number or placas_val
                elif logistics.vehicle_num:
                     veh_obj = db.query(Vehicle).filter(Vehicle.vehicle_key == logistics.vehicle_num).first()
                     if veh_obj:
                         placas_val = veh_obj.plate_number or placas_val

                # 3. Trailer (Placas Remolque)
                if logistics.trailer_num:
                    trl_obj = db.query(Trailer).filter(Trailer.trailer_number == logistics.trailer_num).first()
                    if trl_obj:
                        placas_remolque_val = trl_obj.plate_number or ""

                # 4. Driver (License)
                if logistics.carrier_id and logistics.driver_name:
                    drv_obj = db.query(Driver).filter(
                        Driver.transporter_key == logistics.carrier_id,
                        Driver.driver_name == logistics.driver_name
                    ).first()
                    if drv_obj:
                         licencia_cond_val = drv_obj.license_number or ""

            factura_schema = FacturaSchema(
                numero=header.invoice_number or "S/N",
                fecha=str(header.invoice_date) if header.invoice_date else "",
                tipo_cambio=float(financials.exchange_rate) if (financials and financials.exchange_rate) else (float(pedimento.exchange_rate) if pedimento and pedimento.exchange_rate else 1.0),
                moneda=getattr(header, 'currency', "USD") or "USD",
                incoterm=(logistics.incoterm or "") if logistics else "",
                observaciones=header.observation_es or header.observation_en or "",
                pedimento=f"{pedimento.year}  {pedimento.customs_office[:2] if pedimento.customs_office else ''}  {pedimento.license}  {pedimento.pedimento_number}" if pedimento else "",
                clave_pedimento=pedimento.pedimento_code if pedimento else "",
                regimen=header.document_type or "",
                patente=patente_val,
                agente_aduanal=nombre_agente,
                transporte=transporte_txt,
                num_transporte=num_transporte_val,
                placas=placas_val,
                placas_remolque=placas_remolque_val,
                transportista=transportista_val,
                caat=caat_val,
                scac=scac_val,
                licencia_conductor=licencia_cond_val,
                aduana=compliance.aduana if (compliance and compliance.aduana) else (pedimento.customs_office[:2] if (pedimento and pedimento.customs_office) else ""),
                precinto=(logistics.seal_number or "") if logistics else "",
                destino=(logistics.destination_goods or "") if logistics else "",
                remesa=remesa_valor, acuse_electronico=acuse_valor
            )
            
            if progress_callback: progress_callback(50, "Procesando partidas...")
            lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all()
            partidas_list = []
            
            for line in lines:
                weight_type = db.query(InvoiceLogistics.weight_type).filter(InvoiceLogistics.invoice_id == line.invoice_id).scalar()
                qty = db.query(LineQuantity).filter(LineQuantity.item_line_id == line.id).first()                
                
                # --- WEIGHT CALCULATION LOGIC ---
                peso_neto_kg = 0.0
                peso_bruto_kg = 0.0
                peso_neto_lb = 0.0
                peso_bruto_lb = 0.0                
                
                raw_net = float(qty.net_weight or 0)
                raw_gross = float(qty.gross_weight or 0)
                unit = (weight_type or "KGS").upper()
                
                if unit == "LBS":
                    peso_neto_lb = raw_net
                    peso_bruto_lb = raw_gross
                    peso_neto_kg = raw_net / 2.20462
                    peso_bruto_kg = raw_gross / 2.20462
                else: # Default KG
                    peso_neto_kg = raw_net
                    peso_bruto_kg = raw_gross
                    peso_neto_lb = raw_net * 2.20462
                    peso_bruto_lb = raw_gross * 2.20462
                # --------------------------------

                custom_obj = db.query(LineCustom).filter(LineCustom.item_line_id == line.id).first()
                part_master = db.query(Part).filter(Part.id == line.part_number_id).first()

                desc_final = "S/D"
                num_parte_final = str(line.part_number_id or "S/N")
                fraccion_raw = "" 
                origen_final = "MEX"
                uom_comercial = "PZA" # Default UOM

                if part_master:
                    desc_final = part_master.description_spanish or part_master.description_english or "Sin Desc."
                    num_parte_final = part_master.part_number
                    fraccion_raw = part_master.fraction if part_master.fraction else ""
                    # Commercial UOM from Part Master
                    uom_comercial = part_master.unit_of_measure or "PZA"
                    
                    if part_master.fa_data and part_master.fa_data.origin_country:
                        origen_final = part_master.fa_data.origin_country

                fraccion_limpia = fraccion_raw.replace(".", "").strip()
                if fraccion_limpia:
                    fraccion_limpia = fraccion_limpia[:8].zfill(8)

                fraccion_db = db.query(TariffFraction).filter(TariffFraction.code == fraccion_limpia).first()
                
                fraccion_imprimir = fraccion_raw
                if fraccion_db:
                    fraccion_imprimir = fraccion_db.fraction or fraccion_raw
                else:
                    fraccion_imprimir = self._format_fraccion_fallback(fraccion_limpia)

                # FOR PACKING LIST: FINANCIALS ARE HIDDEN/EMPTY
                v_unitario = ""
                v_total = ""

                partidas_list.append(PartidaSchema(
                    numero_parte=num_parte_final,
                    descripcion=desc_final,
                    fraccion=fraccion_imprimir,
                    fraccion_americana=custom_obj.american_fraction if custom_obj and custom_obj.american_fraction else "",
                    origen=origen_final,
                    advalorem="", # Hidden
                    preferencia="", # Hidden
                    cantidad_importacion=qty.quantity if qty else 0,
                    unidad_medida=uom_comercial, # Commercial UOM (PCS, EA)
                    cantidad_bultos=int(qty.package_quantity) if qty and qty.package_quantity else 0,
                    clave_bultos=(qty.package_info.key if qty and qty.package_info else "") if qty else "",
                    peso_neto=self.formatear_numero(peso_neto_kg),
                    peso_bruto=self.formatear_numero(peso_bruto_kg),
                    peso_neto_lbs=self.formatear_numero(peso_neto_lb),
                    peso_bruto_lbs=self.formatear_numero(peso_bruto_lb),
                    valor_costo_unitario=v_unitario, # Hidden
                    valor_total=v_total # Hidden
                ))

            totales = self.calcular_totales(partidas_list, Decimal(factura_schema.tipo_cambio))

            return PackingListSchema(
                cliente_proveedor=cliente_proveedor, cliente_vendido=cliente_vendido,
                cliente_enviado=cliente_enviado, factura=factura_schema,
                partidas=partidas_list, totales=totales
            )

        except ValidationError as e:
            print(f"Validation Error: {e.json()}")
            raise HTTPException(status_code=500, detail=f"Schema Error: {e}")
        except Exception as e:
            print(f"Error Service A76: {e}")
            raise HTTPException(status_code=500, detail=f"Error: {str(e)}")

    def calcular_totales(self, partidas: List[PartidaSchema], tipo_cambio: Decimal) -> TotalesSchema:
        cant = sum(float(p.cantidad_importacion) for p in partidas)
        # Financial totals hidden
        peso_n = sum(float(p.peso_neto) for p in partidas)
        peso_b = sum(float(p.peso_bruto) for p in partidas)
        peso_n_lbs = sum(float(p.peso_neto_lbs) for p in partidas)
        peso_b_lbs = sum(float(p.peso_bruto_lbs) for p in partidas)

        bultos = sum(p.cantidad_bultos for p in partidas)
        
        claves = [p.clave_bultos for p in partidas if p.clave_bultos]
        clave_comun = max(set(claves), key=claves.count) if claves else ""
        if bultos > 1 and clave_comun and not clave_comun.endswith("S"): clave_comun += "S"
        
        return TotalesSchema(   
            cantidad_total=self.formatear_numero(cant), bultos_total=bultos, clave_bultos=clave_comun,
            peso_neto_total=self.formatear_numero(peso_n), peso_bruto_total=self.formatear_numero(peso_b),
            peso_neto_total_lbs=self.formatear_numero(peso_n_lbs), peso_bruto_total_lbs=self.formatear_numero(peso_b_lbs),
            valor_total_total="", valor_total_dolares=""
        )

    def generate_pdf(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> Tuple[bytes, str]:
        if progress_callback: progress_callback(5, "Iniciando servicio de reporte...")
        data = self.get_packing_list_data(db, invoice_id, company_id, progress_callback)
        
        if progress_callback: progress_callback(80, "Renderizando plantilla...")
        
        # LOGO LOGIC
        logo_b64 = None
        try:
            comp_logo = db.query(Company).filter(Company.id == company_id).first()
            if comp_logo and comp_logo.logo:
                p = Path(comp_logo.logo)
                target_path = p
                if not target_path.exists():
                    fallback = Path(f"app_data/logos/{company_id}") / p.name
                    if fallback.exists():
                        target_path = fallback

                if target_path.exists():
                    with open(target_path, "rb") as image_file:
                        encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
                        mime = "image/png"
                        if target_path.suffix.lower() in ['.jpg', '.jpeg']: mime = "image/jpeg"
                        logo_b64 = f"data:{mime};base64,{encoded_string}"
        except Exception as e:
            print(f"Error loading logo: {e}")

        data.logo_b64 = logo_b64 # Assign logo to schema

        context = data.model_dump()
        html_content = self.template.render(**context)
        
        if progress_callback: progress_callback(90, "Generando PDF final...")
        
        options = {'page-size': 'Letter', 'margin-top': '0.5in', 'margin-right': '0.5in', 'margin-bottom': '0.5in', 'margin-left': '0.5in', 'encoding': "UTF-8", 'enable-local-file-access': None}
        pdf = pdfkit.from_string(html_content, False, options=options, configuration=self._get_wkhtmltopdf_config())
        
        if progress_callback: progress_callback(100, "Completado")
        
        filename = f"PackingList_{data.factura.numero}.pdf"
        return pdf, filename
