
import shutil
import base64
import pdfkit
from pathlib import Path
from typing import Tuple, List, Callable, Optional, Dict, Any
from jinja2 import Environment, FileSystemLoader, select_autoescape
from fastapi import HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from decimal import Decimal
from datetime import datetime


# --- MODELOS (Imported from system for Header info) ---
from api.v1.modules.a76.invoices.models import InvoiceHeader, InvoiceComplianceMx
from api.v1.modules.a76.general_catalogs.company.models import Company
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
from api.v1.modules.a76.parts.models import Part
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
from api.v1.modules.a76.pedmientos.models.pedimento_dates import PedimentoDates
from api.v1.modules.core.tenants.models import Tenant
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
from sqlalchemy.orm import joinedload, load_only

# --- FIFO SERVICE (Internalized) ---
from api.v1.modules.a76.items.series.models import Serie

class FIFOAssignmentService:
    """
    Service for calculating FIFO (PEPS) assignments in real-time.
    Does not persist to database, returns calculated discharge objects.
    """

    def calculate_fifo(self, db: Session, invoice_id: int) -> List[Dict[str, Any]]:
        """
        Calculates the FIFO trail for all lines in an export invoice.
        Returns a list of calculated discharges.
        """
        # 1. Get Export Lines
        export_lines = db.query(LineItem).join(LineItem).filter(
            LineItem.invoice_id == invoice_id
        ).options(
            joinedload(LineItem.quantity),
            joinedload(LineItem.description),
            joinedload(LineItem.customs),
            joinedload(LineItem.unit_of_measure_info),
            joinedload(LineItem.part_info)
        ).all()

        results = []
        self._log(f"Starting FIFO for Invoice {invoice_id}. Export Lines: {len(export_lines)}")

        for exp_line in export_lines:
            qty_needed = float(exp_line.quantity.quantity) if exp_line.quantity and exp_line.quantity.quantity is not None else 0.0
            if qty_needed <= 0:
                continue

            part_number = exp_line.part_number_id
            if not part_number:
                self._log(f"Skipping line {exp_line.id}, no part number")
                continue
            
            self._log(f"Processing Exp Line {exp_line.id}, Part: {part_number}, Qty Needed: {qty_needed}")

            # --- SERIES EXPO LOOKUP ---
            series_desc = ""
            series_count = db.query(Serie).filter(Serie.line_item_id == exp_line.id).count()
            if series_count > 0:
                series_list = db.query(Serie).filter(Serie.line_item_id == exp_line.id).all()
                parts_str = []
                for idx, s in enumerate(series_list, 1):
                    line_parts = [f"{idx}) Serie: {s.serial_numbers or ''}", f"Modelo: {s.model or ''}", f"Parte: {exp_line.part_info.part_number if exp_line.part_info else ''}", f"Num ID Expo: {s.number_id or ''}", f"SubModelo: {s.sub_model or ''}"]
                    parts_str.append(", ".join([p for p in line_parts if p]))
                if parts_str:
                    series_desc = "\nSeries:\n" + "\n".join(parts_str)

            # 2. Find Import Candidates (FIFO order by payment date)
            # Use outerjoin for pedimento dates to avoid filtering out candidates with missing dates
            candidates = db.query(LineItem).join(InvoiceHeader)\
                .join(InvoiceComplianceMx).join(InvoiceComplianceMx.pedimento).outerjoin(Pedimentos.pedimento_dates)\
                .filter(
                    LineItem.part_number_id == part_number,
                    InvoiceHeader.operation_type == 'imp', # Assuming 'imp' is the value for Import based on Enum
                ).order_by(
                    PedimentoDates.payment_date.asc()
                ).options(
                    joinedload(LineItem.quantity),
                    joinedload(LineItem.customs),
                    joinedload(LineItem.financial),
                    joinedload(LineItem.item).joinedload(LineItem.invoice).joinedload(InvoiceHeader.compliance_mx).joinedload(InvoiceComplianceMx.pedimento).joinedload(Pedimentos.pedimento_dates)
                ).all()
            
            self._log(f"Found {len(candidates)} candidates for {part_number}")

            for imp_line in candidates:
                if qty_needed <= 0:
                    break

                imp_qty_total = float(imp_line.quantity.quantity) if imp_line.quantity and imp_line.quantity.quantity is not None else 0.0
                if imp_qty_total <= 0:
                    continue

                take = min(qty_needed, imp_qty_total)
                ratio = take / imp_qty_total if imp_qty_total > 0 else 0

                # Calculate Proportions
                imp_weight = float(imp_line.quantity.net_weight) if imp_line.quantity and imp_line.quantity.net_weight is not None else 0.0
                # imp_val_mn = float(imp_line.customs.customs_value) if imp_line.customs and imp_line.customs.customs_value is not None else 0.0
                imp_val_me = float(imp_line.financial.customs_value_usd) if imp_line.financial and imp_line.financial.customs_value_usd is not None else 0.0
                imp_igi = float(imp_line.customs.igi_amount) if imp_line.customs and imp_line.customs.igi_amount is not None else 0.0

                imp_inv = imp_line.item.invoice
                ped = imp_inv.compliance_mx.pedimento if imp_inv and imp_inv.compliance_mx else None
                
                # --- EXCHANGE RATE VALIDATION ---
                payment_date = ped.pedimento_dates.payment_date if ped and ped.pedimento_dates else None
                exchange_rate_val = 1.0
                validation_error = None
                
                if payment_date:
                    er_obj = db.query(ExchangeRate).filter(ExchangeRate.date == payment_date).first()
                    if er_obj:
                         exchange_rate_val = float(er_obj.value)
                    else:
                         # Try previous day if strict match fails (mimicking SisGen:UtilizarFechaPagoPedDeUnDiaAnterior logic broadly or just flagging)
                         # For now, flag it.
                         validation_error = f"Tipo de Cambio no encontrado para fecha {payment_date}"
                
                # Calculate Valor MN based on Clarion logic: (CantDesc * ValorImpoME / CantImpo) * TC
                # which simplifies to: Ratio * ValorImpoME * TC
                val_mn_calc = (imp_val_me * ratio) * exchange_rate_val

                discharge = {
                    "export_line_id": exp_line.id,
                    "import_line_id": imp_line.id,
                    "quantity": take,
                    "net_weight": imp_weight * ratio,
                    "value_mxn": val_mn_calc,
                    "value_usd": imp_val_me * ratio,
                    "igi_amount": imp_igi * ratio,
                    "import_invoice": imp_inv.invoice_number if imp_inv else "N/A",
                    "pedimento": ped.pedimento_number if ped else "N/A",
                    "pedimento_clave": ped.pedimento_code if ped else "", 
                    "pedimento_date": payment_date.isoformat() if payment_date else None,
                    "series_desc": series_desc,
                    "validation_error": validation_error
                }
                
                self._log(f"MATCH: Taking {take} from Imp Line {imp_line.id}")
                results.append(discharge)
                qty_needed -= take

        return results

    def _log(self, msg):
        try:
            with open("/tmp/fifo_debug.log", "a") as f:
                f.write(f"{datetime.now()}: {msg}\n")
        except: pass


# --- SCHEMAS FOR TEMPLATE CONTEXT ---

class DischargeItemSchema(BaseModel):
    # Column 1: Pedimento Info
    pedimento_numero: str
    pedimento_clave: str
    pedimento_fecha_pago: str
    
    # Column 2: Import Invoice
    factura_impo: str
    
    # Column 3: Part Info
    numero_parte: str
    descripcion: str
    fraccion: str
    origen_pref_sector: str # e.g. "CHN-GENERAL"
    
    # Metrics
    cantidad: str
    unidad_medida: str
    peso_neto: str
    
    # Values
    valor_mn: str
    valor_me: str
    valor_igi: str
    
    # Flags
    se_pago: str # "0.0" or "Yes"? Image says "0.0" in column "Se Pago"? No, "Se Pago" might be a flag, image key implies payment. 
                 # Image: "Se Pago" column has "0.0"? No, look closer. 
                 # "Value/Monto IGI USD/Dolares" has "0.0". 
                 # "Se Pago" column seems empty or has '1'?
                 # Wait, looking at image: 
                 # Col: "Se Pago", Row: "0.0"? No that's IGI.
                 # Let's assume Se Pago is a boolean/string. 
                 # Last col: "Linea Expo".
    
    linea_expo: str
    
    # Errors
    error_msg: Optional[str] = None

    # Helper for Jinja (if methods not allowed in pydantic models in template)
    def __init__(self, **data):
        super().__init__(**data)

class DischargeContext(BaseModel):
    items: List[DischargeItemSchema]
    invoice_number: str
    company_name: str
    company_address: str
    company_rfc: str
    company_immex: str
    
    # Totals
    total_cantidad: str
    total_peso: str
    total_valor_mn: str
    total_valor_me: str
    total_igi: str

class DescargaReportService:
    def formatear_numero(self, valor, decimales: int = 2):
        if valor is None: return "0.00"
        try:
            return "{:,.{}f}".format(float(valor), decimales)
        except: return "0.00"

    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('descarga.html')

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

    def obtener_datos(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> DischargeContext:
        try:
            if progress_callback: progress_callback(10, "Buscando factura...")
            
            # --- 1. Obtener Cabeceras (Igual que antes) ---
            header = db.query(InvoiceHeader).filter(InvoiceHeader.id == invoice_id).first()
            if not header:
                raise HTTPException(status_code=404, detail="Factura no encontrada")

            tenant = db.query(Tenant).filter(Tenant.id == header.tenant_id).first()
            company = db.query(Company).filter(Company.id == header.company_id).first()
            
            # --- 2. Obtener Líneas de Exportación (Lo que necesitamos cubrir) ---
            if progress_callback: progress_callback(20, "Obteniendo items a exportar...")
            
            export_lines = db.query(LineItem).filter(                
                LineItem.invoice_id == invoice_id
            ).options(
                joinedload(LineItem.quantity),
                joinedload(LineItem.customs),
                joinedload(LineItem.description),
                joinedload(LineItem.unit_of_measure_info),
                joinedload(LineItem.part_info)
            ).join(LineItem).all()

            items_reporte = []

            # --- 3. EL ALGORITMO PEPS EN VIVO ---
            if progress_callback: progress_callback(40, "Calculando PEPS en tiempo real...")

            fifo_service = FIFOAssignmentService()
            discharges = fifo_service.calculate_fifo(db, invoice_id)

            print(f"DEBUG: Calculated {len(discharges)} discharges.") # FORCE PRINT
            
            # Create map for faster/safer lookup
            exp_map = {l.id: l for l in export_lines}

            for d in discharges:
                exp_id = d["export_line_id"]
                exp_line = exp_map.get(exp_id)
                
                if not exp_line:
                    print(f"DEBUG: Skipping discharge, Exp Line {exp_id} not found in map keys: {list(exp_map.keys())}")
                    continue
                
                print(f"DEBUG: Adding item to report: Imp {d['import_line_id']} -> Exp {exp_id}")
                
                desc_final = exp_line.description.description_spanish if exp_line.description else "S/D"
                items_reporte.append(DischargeItemSchema(
                    pedimento_numero=d["pedimento"],
                    pedimento_clave=d["pedimento_clave"], 
                    pedimento_fecha_pago=d["pedimento_date"].split("T")[0] if d["pedimento_date"] else "",
                    
                    factura_impo=d["import_invoice"],
                    
                    numero_parte=exp_line.part_info.part_number if exp_line.part_info else "",
                    descripcion=desc_final,
                    fraccion=exp_line.customs.fraction if exp_line.customs else "",
                    origen_pref_sector=f"{exp_line.customs.origin_country or ''} - {exp_line.customs.sector or ''}" if exp_line.customs else "",
                    
                    cantidad=self.formatear_numero(d["quantity"], 3),
                    unidad_medida=exp_line.unit_of_measure_info.code if exp_line.unit_of_measure_info else "PZA",
                    
                    peso_neto=self.formatear_numero(d["net_weight"], 3),
                    valor_mn=self.formatear_numero(d["value_mxn"]),
                    valor_me=self.formatear_numero(d["value_usd"]),
                    valor_igi=self.formatear_numero(d["igi_amount"]),
                    
                    se_pago="",
                    linea_expo=str(exp_line.line_number),
                    error_msg=d.get("validation_error")
                ))
            
            print(f"DEBUG: Final Report Items Count: {len(items_reporte)}")
            
            # --- 4. Totales y Finalización (Igual que antes) ---
            addr_str = "DIRECCION NO REGISTRADA"
            immex_val = ""
            
            if company:
                # Address Logic
                if company.addresses:
                    # Prefer 'main' address, otherwise take the first one
                    main_addr = next((a for a in company.addresses if a.address_type == 'main'), company.addresses[0])
                    
                    parts = []
                    if main_addr.street: parts.append(main_addr.street)
                    if main_addr.exterior_number: parts.append(f"No. {main_addr.exterior_number}")
                    if main_addr.neighborhood: parts.append(main_addr.neighborhood)
                    if main_addr.city: parts.append(main_addr.city)
                    if main_addr.state: parts.append(main_addr.state)
                    if main_addr.postal_code: parts.append(f"CP {main_addr.postal_code}")
                    
                    if parts:
                        addr_str = ", ".join(parts)

                # IMMEX Logic
                if company.program and "IMMEX" in company.program and company.program_number:
                    immex_val = company.program_number

            # Recalcular totales basados en la lista generada
            t_cant = sum(float(i.cantidad.replace(",","")) for i in items_reporte)
            t_peso = sum(float(i.peso_neto.replace(",","")) for i in items_reporte)
            t_mn = sum(float(i.valor_mn.replace(",","")) for i in items_reporte)
            t_me = sum(float(i.valor_me.replace(",","")) for i in items_reporte)
            t_igi = sum(float(i.valor_igi.replace(",","")) for i in items_reporte)

            return DischargeContext(
                items=items_reporte,
                invoice_number=header.invoice_number or "SIN FOLIO",
                company_name=company.name if company else (tenant.name if tenant else "EMPRESA DESCONOCIDA"),
                company_address=addr_str,
                company_rfc=company.rfc if company else "",
                company_immex=immex_val,
                total_cantidad=self.formatear_numero(t_cant, 3),
                total_peso=self.formatear_numero(t_peso, 3),
                total_valor_mn=self.formatear_numero(t_mn),
                total_valor_me=self.formatear_numero(t_me),
                total_igi=self.formatear_numero(t_igi)
            )

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

    def generar_pdf(self, db: Session, invoice_id: int, company_id: int, progress_callback: Optional[Callable] = None) -> Tuple[bytes, str, str]:
        if progress_callback: progress_callback(5, "Iniciando servicio de reporte...")
        
        datos = self.obtener_datos(db, invoice_id, company_id, progress_callback)
        
        if progress_callback: progress_callback(80, "Renderizando plantilla...")
        
        context = datos.model_dump()
        html_content = self.template.render(**context)
        nombre = f"Descarga_{datos.invoice_number}.pdf"
        
        if progress_callback: progress_callback(90, "Generando PDF final...")
        
        options = {
            'page-size': 'Letter', 
            'orientation': 'Landscape', # Correct argument for wkhtmltopdf
            'margin-top': '0.5in', 
            'margin-right': '0.5in', 
            'margin-bottom': '0.5in', 
            'margin-left': '0.5in', 
            'encoding': "UTF-8"
        }
        
        pdf = pdfkit.from_string(html_content, False, options=options, configuration=self._get_wkhtmltopdf_config())
        
        if progress_callback: progress_callback(100, "Completado")
        return pdf, nombre, "application/pdf"
