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 sqlalchemy.orm import Session

# --- MODELOS ---
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceLogistics
from api.v1.modules.a76.items.line_financials.models import LineFinancial
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
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
from api.v1.modules.a76.items.line_customs.models import LineCustom
from api.v1.modules.a76.items.line_descriptions.models import LineDescription
from api.v1.modules.a76.items.series.models import Serie

# --- 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 ---
# Reuse schemas from neighbor package as they fit the same data structure
from ..mex.schemas import (
    ClienteSchema,
    PartidaSchema,
    TotalesSchema,
    FacturaSchema,
    FacturaImportacionCompleta,
)


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

    def _get_document_title(self, invoice_type: str, is_american: bool = True) -> str:
        """
        Determina el título del documento basado en el tipo de factura.

        Args:
            invoice_type: Tipo de factura (TEM, DEF, MEX, CR)
            is_american: Si es factura americana (True) o mexicana (False)

        Returns:
            Título formateado para la factura
        """
        # Mapeo para facturas mexicanas
        mexican_titles = {
            "MEX": "Factura Importación Compras Mexicanas",
            "DEF": "Importación Definitiva",
            "TEM": "Importación Temporal",
            "CR": "Importación de Cambio de Régimen",
        }

        # Mapeo para facturas americanas
        american_titles = {
            "MEX": "Mexican Purchases Import Invoice",
            "DEF": "Definitive Importation",
            "TEM": "Temporary Importation",
            "CR": "Regime Change Importation",
        }

        # Seleccionar el mapa correcto
        titles = american_titles if is_american else mexican_titles

        # Obtener el título (normalizar a mayúsculas)
        invoice_type_upper = invoice_type.upper() if invoice_type else ""
        title = titles.get(invoice_type_upper, "")

        # Fallback a genéricos si no se encuentra
        if not title:
            return "Commercial Invoice" if is_american else "Factura de Importación"

        return title

    def _get_wkhtmltopdf_config(self):
        path = shutil.which("wkhtmltopdf") or "/usr/local/bin/wkhtmltopdf"
        if not Path(path).exists():
            raise RuntimeError("wkhtmltopdf no encontrado.")
        return pdfkit.configuration(wkhtmltopdf=path)

    def formatear_numero(self, valor, decimales: int = 2):
        if valor is None:
            valor = 0.0
        try:
            num = round(float(valor), 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
        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="Unknown",
                direccion="",
                tax_id="",
                codigo_postal="",
                ciudad="",
                estado="",
                pais="USA",
            )

        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 "N/A",
            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 "USA") if addr else "USA",
            tax_id=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 obtener_datos(
        self,
        db: Session,
        invoice_id: int,
        company_id: int,
        progress_callback: Optional[Callable] = None,
        currency_code: str = "ORIGINAL",
    ) -> FacturaImportacionCompleta:
        try:
            if progress_callback:
                progress_callback(10, "Searching invoice...")
            header = (
                db.query(InvoiceHeader)
                .filter(
                    InvoiceHeader.id == invoice_id,
                    InvoiceHeader.company_id == company_id,
                )
                .first()
            )
            if not header:
                raise HTTPException(status_code=404, detail="Invoice not found")

            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, "Fetching entry data...")
            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, "Fetching client and supplier...")
            proveedor_id = compliance.provider_id if compliance else None
            cliente_proveedor = (
                self._obtener_datos_cliente(db, proveedor_id, "Supplier:")
                if proveedor_id
                else ClienteSchema(
                    header="Supplier",
                    nombre="Unassigned",
                    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", "Local Company"),
                direccion="FISCAL ADDRESS",
                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 (Sold To)
            cliente_vendido = cliente_default
            if compliance and compliance.sold_to_id:
                # Force English header for American Invoice
                clean_header = "Sold To:"
                # raw_header = compliance.sold_to_header or "SOLD_TO"
                # clean_header = raw_header.replace("_", " ").title() + ":"
                cliente_vendido = self._obtener_datos_cliente(
                    db, compliance.sold_to_id, clean_header
                )

            # Right Side Logic (Shipped To)
            cliente_enviado = cliente_default
            if compliance and compliance.shipped_to_id:
                # Force English header for American Invoice
                clean_header_shipped = "Shipped To:"
                # raw_header_shipped = compliance.shipped_to_header or "SHIPPED_TO"
                # clean_header_shipped = raw_header_shipped.replace("_", " ").title() + ":"

                # Fetch client data
                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 ""

            # Init values
            placas_val = (logistics.license_plate or "") if logistics else ""  # Plates
            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_int_id is not None:
                    transporter_obj = (
                        db.query(Transporter)
                        .filter(Transporter.transporter_id == logistics.carrier_int_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
                elif 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 (Plates)
                if logistics.transport_id:
                    if logistics.transport_int_id is not None:
                        veh_obj = (
                            db.query(Vehicle)
                            .filter(Vehicle.vehicle_id == logistics.transport_int_id)
                            .first()
                        )
                    else:
                        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
                if logistics.trailer_num:
                    if logistics.trailer_int_id is not None:
                        trl_obj = (
                            db.query(Trailer)
                            .filter(Trailer.trailer_id == logistics.trailer_int_id)
                            .first()
                        )
                    else:
                        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 ""

            # Determine Currency
            moneda_final = getattr(header, "currency", "USD") or "USD"
            if currency_code == "MXN":
                moneda_final = "MXN"
            elif currency_code == "USD":
                moneda_final = "USD"

            factura_schema = FacturaSchema(
                numero=header.invoice_number or "N/A",
                titulo_documento=self._get_document_title(
                    header.invoice_type or "", is_american=True
                ),
                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=moneda_final,
                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, "Processing items...")
            lines = db.query(LineItem).filter(LineItem.invoice_id == header.id).all()
            partidas_list = []

            # Initialize raw totals
            raw_cant = 0.0
            raw_valor = 0.0
            raw_peso_n = 0.0
            raw_peso_b = 0.0
            raw_bultos = 0

            for line in lines:
                qty = (
                    db.query(LineQuantity)
                    .filter(LineQuantity.item_line_id == line.id)
                    .first()
                )
                fin = (
                    db.query(LineFinancial)
                    .filter(LineFinancial.item_line_id == line.id)
                    .first()
                )
                customs = (
                    db.query(LineCustom)
                    .filter(LineCustom.item_line_id == line.id)
                    .first()
                )
                desc_obj = (
                    db.query(LineDescription)
                    .filter(LineDescription.item_line_id == line.id)
                    .first()
                )
                series_objs = (
                    db.query(Serie)
                    .filter(Serie.line_item_id == line.id)
                    .all()
                )
                part_master = (
                    db.query(Part).filter(Part.id == line.part_number_id).first()
                )

                desc_final = "N/D"
                num_parte_final = str(line.part_number_id or "N/A")
                fraccion_raw = ""
                origen_final = "MEX"

                if part_master:
                    # Prefer English description if available, else Spanish
                    desc_final = (
                        part_master.description_english
                        or part_master.description_spanish
                        or "No Desc."
                    )
                    num_parte_final = part_master.part_number
                    # Prefer US Fraction (HTS) if available
                    fraccion_raw = (
                        part_master.us_fraction if part_master.us_fraction else ""
                    )

                    if part_master.fa_data and part_master.fa_data.origin_country:
                        origen_final = part_master.fa_data.origin_country

                # Prioritize description from LineDescription (specific to this invoice)
                if desc_obj:
                    desc_final = desc_obj.description_english or desc_obj.description_spanish or desc_final

                # FRACTION LOGIC: Use US Fraction (us_fraction) if available, otherwise blank
                fraccion_imprimir = ""

                # Check part master US fraction
                if part_master and part_master.us_fraction:
                    fraccion_imprimir = part_master.us_fraction.strip()

                # Preference and Advalorem logic
                preferencia_txt = "General"
                advalorem_txt = "0%"

                # 1. Prioridad: Datos específicos de la partida (LineCustom)
                if customs:
                    if customs.fraction_type:
                        preferencia_txt = str(customs.fraction_type).upper()
                    
                    if customs.advalorem:
                        adv_val = customs.advalorem.strip()
                        if adv_val not in ["0", "0.0", "0.00", ""]:
                            advalorem_txt = adv_val if "%" in adv_val else f"{adv_val}%"
                
                # 2. Marca, Modelo y Series
                marca_val = desc_obj.brand if desc_obj else ""
                modelo_val = desc_obj.model if desc_obj else ""
                series_list = [s.serial_numbers for s in series_objs if s.serial_numbers]

                # Prioritize USD for American Invoice logic if available?
                # Sticking to same logic as Mex for now but could prioritize USD columns.
                # Actually, duplicate logic from mex service for now to ensure consistency.

                v_unitario = 0.0
                v_total = 0.0

                if fin:
                    is_mxn = factura_schema.moneda == "MXN"

                    if is_mxn:
                        v_unitario = float(fin.unit_cost_commercial_mxn or 0.0)
                        v_total = float(fin.value_commercial_mxn or 0.0)
                    else:
                        v_unitario = float(fin.unit_cost_commercial_usd or 0.0)
                        v_total = float(fin.value_commercial_usd or 0.0)

                    if not v_unitario:
                        v_unitario = float(fin.commercial_unit_cost or 0.0)

                    if not v_total:
                        v_total = float(fin.total_commercial_value or 0.0)

                    cantidad = float(qty.quantity) if (qty and qty.quantity) else 0.0

                    if cantidad > 0:
                        if v_unitario > 0 and v_total == 0:
                            v_total = v_unitario * cantidad
                        elif v_total > 0 and v_unitario == 0:
                            v_unitario = v_total / cantidad

                # Accumulate raw totals
                raw_cant += float(qty.quantity) if qty and qty.quantity else 0.0
                raw_valor += v_total
                raw_peso_n += float(qty.net_weight) if qty and qty.net_weight else 0.0
                raw_peso_b += float(qty.gross_weight) if qty and qty.gross_weight else 0.0
                raw_bultos += int(qty.package_quantity) if qty and qty.package_quantity else 0

                # UOM Mapping for English context
                uom_raw = line.unit_of_measure_info.code if line.unit_of_measure_info else "PCS"
                if uom_raw == "PZA":
                    uom_raw = "PCS"

                partidas_list.append(
                    PartidaSchema(
                        numero_parte=num_parte_final,
                        descripcion=desc_final,
                        fraccion=fraccion_imprimir,
                        origen=origen_final,
                        advalorem=advalorem_txt,
                        preferencia=preferencia_txt,
                        marca=marca_val,
                        modelo=modelo_val,
                        series=series_list,
                        cantidad_importacion=self.formatear_numero(
                            qty.quantity if qty else 0
                        ),
                        unidad_medida=uom_raw,
                        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 ""
                        ),
                        peso_neto=self.formatear_numero(qty.net_weight if qty else 0),
                        peso_bruto=self.formatear_numero(
                            qty.gross_weight if qty else 0
                        ),
                        valor_costo_unitario=self.formatear_numero(v_unitario),
                        valor_total=self.formatear_numero(v_total),
                    )
                )

            totales = self.calcular_totales(
                partidas_list, 
                Decimal(factura_schema.tipo_cambio),
                raw_cant=raw_cant,
                raw_valor=raw_valor,
                raw_peso_n=raw_peso_n,
                raw_peso_b=raw_peso_b,
                raw_bultos=raw_bultos
            )

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

        except Exception as e:
            print(f"Error Service A76 USA: {e}")
            raise HTTPException(status_code=500, detail=f"Error: {str(e)}")

    def calcular_totales(
        self, 
        partidas: List[PartidaSchema], 
        tipo_cambio: Decimal,
        raw_cant: float = 0,
        raw_valor: float = 0,
        raw_peso_n: float = 0,
        raw_peso_b: float = 0,
        raw_bultos: int = 0
    ) -> TotalesSchema:
        # Use raw values passed from obtener_datos to avoid TypeError with formatted strings
        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"
        # Don't pluralize strictly in English without logic, kept simple.

        tc = float(tipo_cambio) if tipo_cambio else 1.0
        return TotalesSchema(
            cantidad_total=self.formatear_numero(raw_cant),
            bultos_total=raw_bultos,
            clave_bultos=clave_comun,
            peso_neto_total=self.formatear_numero(raw_peso_n),
            peso_bruto_total=self.formatear_numero(raw_peso_b),
            valor_total_total=self.formatear_numero(raw_valor),
            valor_total_dolares=self.formatear_numero(raw_valor / tc if tc > 0 else 0),
        )

    def generar_factura_completa(
        self,
        db: Session,
        invoice_id: int,
        company_id: int,
        formato: str = "pdf",
        progress_callback: Optional[Callable] = None,
        currency_code: str = "ORIGINAL",
    ) -> Tuple[bytes, str, str]:
        if progress_callback:
            progress_callback(5, "Starting report service...")
        datos = self.obtener_datos(
            db, invoice_id, company_id, progress_callback, currency_code
        )

        if progress_callback:
            progress_callback(80, "Rendering template...")

        # 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}")

        context = {
            "cliente_proveedor": datos.cliente_proveedor.model_dump(),
            "cliente_vendido": datos.cliente_vendido.model_dump(),
            "cliente_enviado": datos.cliente_enviado.model_dump(),
            "factura": datos.factura.model_dump(),
            "partidas": [p.model_dump() for p in datos.partidas],
            "totales": datos.totales.model_dump(),
            "logo_b64": logo_b64,
        }
        html_content = self.template.render(**context)
        nombre = f"Commercial_Invoice_{datos.factura.numero}.{formato}"
        if formato == "html":
            return html_content.encode("utf-8"), nombre, "text/html"

        if progress_callback:
            progress_callback(90, "Generating PDF...")
        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, "Completed")
        return pdf, nombre, "application/pdf"
