from server_backsemot.models import GuiaDespacho, ItemsGD, UnidadGD, CamionGD, ClienteGD, PreciosGD, DetalleGuia, PesajeGD
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from server_backsemot.api.dte import generar_anulacion_guia_despacho
from server_backsemot.personal.personal import validaciones_basicas
from server_backsemot.api.dte import generar_guia_despacho
from server_backsemot.api.dte import enviar_datos
from rest_framework.decorators import api_view
from urllib.parse import urlparse, urlunparse
from django.db.models.query import QuerySet
from binascii import Error as BinasciiError
from django.db.models import Q, Prefetch
from base64 import b64encode, b64decode
from server_backsemot.utiles import *
from datetime import datetime, time
import xml.etree.ElementTree as ET
from django.db import transaction
from typing import Optional
import traceback
import json




@api_view(["GET"])
def consultar_itemsgd(request):
    try:
        # Validación de permisos y autenticación
        validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
        if validacion != "ok":
            return Utiles.responder_500(validacion)
        
        items = ItemsGD.objects.all()
        datos = [formatear_item(i) for i in items]
        return Utiles.responder_200(datos)
    except:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



@api_view(["GET"])
def consultar_unidadesgd(request):
    try:
        # Validación de permisos y autenticación
        validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
        if validacion != "ok":
            return Utiles.responder_500(validacion)
        
        unidades = UnidadGD.objects.all()
        datos = [formatear_unidad(u) for u in unidades]
        return Utiles.responder_200(datos)
    except:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



@api_view(["POST"])
def crear_guia_despacho(request):
    try:
        with transaction.atomic():
            validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
            if validacion != "ok":
                return Utiles.responder_500(validacion)

            campos_requeridos = ["cliente", "camion", "fecha", "glosa"]
            validacion_campos = validar_campos_requeridos(request, campos_requeridos)
            if validacion_campos != "ok":
                return Utiles.responder_500(validacion_campos)

            cliente_id = request.POST.get("cliente")
            camion_id = request.POST.get("camion")
            fecha = request.POST.get("fecha")
            es_venta = request.POST.get("es_venta")
            glosa_raw = request.POST.get("glosa", "[]")

            try:
                es_venta = int(es_venta)
                if es_venta != 1 and es_venta != 2:
                    return Utiles.responder_500("El valor de es venta debe ser 1 o 2")
            except ValueError:
                return Utiles.responder_500("El valor de es vnta debe ser numérico")
            if es_venta == 2:
                es_venta = 6

            try:
                glosa = json.loads(glosa_raw)
            except Exception:
                return Utiles.responder_500("El campo glosa debe venir en formato JSON válido")

            if not isinstance(glosa, list) or len(glosa) == 0:
                return Utiles.responder_500("El campo glosa debe ser un arreglo con al menos un detalle")

            if not Utiles.validar_fecha_formato(fecha):
                return Utiles.responder_500("El formato de la fecha es incorrecto")

            cliente = ClienteGD.objects.get(id=cliente_id)
            camion = CamionGD.objects.get(id=camion_id)
            
            if camion.activo == 0 or cliente.activo == 0:
                return Utiles.responder_500("Tanto el cliente como el camión deben estar activos")
            
            if cliente.id != camion.cliente.id:
                return Utiles.responder_500("El camión seleccionado no pertenece a ese cliente")
            
            pesaje = PesajeGD.objects.filter(camion=camion).order_by("-fecha").first()
            
            neto = 0
            usa_pesaje = None
            detalles_a_crear = []

            for index, detalle in enumerate(glosa, start=1):
                if not isinstance(detalle, dict):
                    return Utiles.responder_500(f"Cada elemento de glosa debe ser un objeto. Error en posición {index}")

                item_id = detalle.get("item")
                unidad_id = detalle.get("unidad")
                cantidad = detalle.get("cantidad")

                if not item_id:
                    return Utiles.responder_500(f"El item en la posición {index} es obligatorio")

                if not unidad_id:
                    return Utiles.responder_500(f"La unidad en la posición {index} es obligatoria")

                try:
                    item_id = int(item_id)
                    unidad_id = int(unidad_id)
                except (TypeError, ValueError):
                    return Utiles.responder_500(f"Item y unidad deben ser IDs numéricos en la posición {index}")

                if unidad_id == 4:
                    if cantidad is None or str(cantidad).strip() == "":
                        return Utiles.responder_500(f"La cantidad en la posición {index} es obligatoria para la unidad {unidad_id}")

                    try:
                        cantidad = float(cantidad)
                    except (TypeError, ValueError):
                        return Utiles.responder_500(f"La cantidad en la posición {index} debe ser numérica" )

                    if cantidad <= 0:
                        return Utiles.responder_500(f"La cantidad en la posición {index} debe ser mayor a 0" )

                item = ItemsGD.objects.get(id=item_id)
                unidad = UnidadGD.objects.get(id=unidad_id)

                precio = PreciosGD.objects.filter(
                    unidad=unidad,
                    item=item
                ).first()

                if not precio:
                    return Utiles.responder_500(f"No existe precio configurado para el item {item_id} con la unidad {unidad_id} en la posición {index}")

                precio_unitario = float(precio.precio)

                if unidad_id == 1:
                    if camion.unidad == "m3":
                        return Utiles.responder_500("El camión está registrado por m3, pero la guía se está generando por peso")
                    if not pesaje:
                        return Utiles.responder_500("No existe pesaje para el camión indicado")
                    if camion.peso_tara <= 0:
                        return Utiles.responder_500("El camion debe realizar el pesaje inicial")

                    cantidad = (pesaje.peso - camion.peso_tara) * 1000
                    if cantidad <= 0:
                        return Utiles.responder_500("La cantidad calculada en kilogramos debe ser mayor a 0")

                    subtotal = cantidad * precio_unitario
                    usa_pesaje = pesaje

                elif unidad_id == 2:
                    if camion.unidad == "m3":
                        return Utiles.responder_500("El camión está registrado por m3, pero la guía se está generando por peso")
                    if not pesaje:
                        return Utiles.responder_500("No existe pesaje para el camión indicado")
                    if camion.peso_tara <= 0:
                        return Utiles.responder_500("El camion debe realizar el pesaje inicial")

                    cantidad = (pesaje.peso - camion.peso_tara)
                    if cantidad <= 0:
                        return Utiles.responder_500("La cantidad calculada en toneladas debe ser mayor a 0")

                    subtotal = cantidad * precio_unitario
                    usa_pesaje = pesaje

                elif unidad_id == 3:
                    if camion.unidad == "toneladas":
                        return Utiles.responder_500("El camión está registrado por peso, pero la guía se está generando por m3")

                    cantidad = camion.peso_tara
                    if cantidad is None or cantidad <= 0:
                        return Utiles.responder_500("La cubicación del camión debe ser mayor a 0")

                    subtotal = cantidad * precio_unitario

                else:
                    if cantidad is None or str(cantidad).strip() == "":
                        return Utiles.responder_500(f"La cantidad en la posición {index} es obligatoria para la unidad {unidad_id}")

                    try:
                        cantidad = float(cantidad)
                    except (TypeError, ValueError):
                        return Utiles.responder_500(f"La cantidad en la posición {index} debe ser numérica")

                    if cantidad <= 0:
                        return Utiles.responder_500(f"La cantidad en la posición {index} debe ser mayor a 0")

                    subtotal = cantidad * precio_unitario
                neto += subtotal

                detalles_a_crear.append({
                    "camion": camion,
                    "item": item,
                    "unidad": unidad,
                    "cantidad": cantidad,
                    "precio_unitario": precio_unitario,
                })

            iva = round(neto * 0.19)
            total = neto + iva

            data_guia = {
                "fecha_creacion": datetime.now(),
                "fecha_guia": fecha,
                "neto": int(round(neto)),
                "iva": int(round(iva)),
                "total": int(round(total)),
                "es_venta": es_venta,
            }

            if usa_pesaje:
                data_guia["pesaje"] = usa_pesaje

            guia = GuiaDespacho.objects.create(**data_guia)

            detalles_modelo = [
                DetalleGuia(
                    camion=detalle["camion"],
                    guia=guia,
                    item=detalle["item"],
                    unidad=detalle["unidad"],
                    cantidad=detalle["cantidad"],
                    precio_unitario=detalle["precio_unitario"],
                )
                for detalle in detalles_a_crear
            ]

            DetalleGuia.objects.bulk_create(detalles_modelo)

            resp, data = generar_guia_facturacion(guia)
            if not resp:
                return Utiles.responder_200("Se registraron los datos, pero ocurrió un error con facturacion.cl al generar el documento en el SII")
            else:
                return Utiles.responder_200("Se ha creado la el documento correctamente", data)

    except ClienteGD.DoesNotExist:
        return Utiles.responder_500("No existe cliente con el id indicado")
    except CamionGD.DoesNotExist:
        return Utiles.responder_500("No existe camión con el id indicado")
    except ItemsGD.DoesNotExist:
        return Utiles.responder_500("No existe item con el id indicado")
    except UnidadGD.DoesNotExist:
        return Utiles.responder_500("No existe unidad con el id indicado")
    except Exception:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



@api_view(["GET"])
def buscar_guias_despacho(request):
    try:
        validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
        if validacion != "ok":
            return Utiles.responder_500(validacion)

        guias = GuiaDespacho.objects.all()

        fecha_inicio = request.GET.get("fecha_inicio")
        fecha_final = request.GET.get("fecha_final")

        if fecha_inicio or fecha_final:
            if not fecha_inicio or not fecha_final:
                return Utiles.responder_500("Debe enviar fecha_inicio y fecha_final")

            if not Utiles.validar_fecha_formato(fecha_inicio):
                return Utiles.responder_500("La fecha de inicio no tiene el formato correcto")

            if not Utiles.validar_fecha_formato(fecha_final):
                return Utiles.responder_500("La fecha de término no tiene el formato correcto")

            try:
                fecha_inicio_dt = datetime.strptime(fecha_inicio, "%Y-%m-%d")
                fecha_final_dt = datetime.strptime(fecha_final, "%Y-%m-%d")

                fecha_inicio_dt = datetime.combine(fecha_inicio_dt.date(), time.min)
                fecha_final_dt = datetime.combine(fecha_final_dt.date(), time.max)
            except ValueError:
                return Utiles.responder_500("Las fechas no tienen el formato correcto")

            guias = guias.filter(
                fecha_guia__range=(fecha_inicio_dt, fecha_final_dt)
            )

        guias = guias.order_by("-fecha_guia", "-id")

        page = request.GET.get("page", 1)
        paginator = Paginator(guias, 20)

        try:
            guias_paginadas = paginator.page(page)
        except PageNotAnInteger:
            guias_paginadas = paginator.page(1)
        except EmptyPage:
            guias_paginadas = paginator.page(paginator.num_pages)

        datos = formatear_guiadespacho_full(guias_paginadas)

        respuesta = {
            "mensaje": "Listado de guías de despacho",
            "datos": datos,
            "paginacion": {
                "pagina_actual": guias_paginadas.number,
                "total_paginas": paginator.num_pages,
                "total_elementos": paginator.count,
                "elementos_pagina_actual": f"{guias_paginadas.start_index()} al {guias_paginadas.end_index()}"
            }
        }

        return Utiles.responder_200("Guías listadas correctamente", respuesta)

    except Exception:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



@api_view(["GET"])
def buscar_guias_despacho_filtro(request):
    try:
        validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
        if validacion != "ok":
            return Utiles.responder_500(validacion)

        filtro = request.GET.get("filtro")
        if not filtro:
            return Utiles.responder_500("Falta el filtro")

        guias = GuiaDespacho.objects.filter(
            Q(pesaje__camion__cliente__rut__icontains=filtro) |
            Q(pesaje__camion__cliente__nombre__icontains=filtro) |
            Q(pesaje__camion__patente__icontains=filtro) |
            Q(detalleguia__camion__cliente__rut__icontains=filtro) |
            Q(detalleguia__camion__cliente__nombre__icontains=filtro) |
            Q(detalleguia__camion__patente__icontains=filtro)
        ).distinct().order_by("-fecha_guia", "-id")

        if not guias.exists():
            return Utiles.responder_500("No se encontraron guías con ese filtro")

        datos = formatear_guiadespacho_full(guias)

        return Utiles.responder_200("Guías encontradas correctamente", datos)

    except Exception:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



@api_view(["POST"])
def anular_guia_despacho(request):
    try:
        validacion = validaciones_basicas(
            validaciones=[1, 2, 3, 4],
            perfiles=[1, 2],
            request=request
        )
        if validacion != "ok":
            return Utiles.responder_500(validacion)

        guia_despacho_id = request.POST.get("guia_despacho_id")

        if not guia_despacho_id:
            return Utiles.responder_500("Debe indicar el id de la guía.")

        try:
            guia_despacho_id = int(guia_despacho_id)
        except (TypeError, ValueError):
            return Utiles.responder_500("El id de la guía es inválido.")

        guia = GuiaDespacho.objects.filter(pk=guia_despacho_id).first()
        if not guia:
            return Utiles.responder_500("La guía no existe.")

        ok, params, guia = generar_anulacion_guia_despacho(guia_despacho_id)
        if not ok:
            return Utiles.responder_500(params)

        ok_envio, respuesta = enviar_datos(params, 3)

        if not ok_envio:
            return Utiles.responder_500(respuesta)

        if not respuesta:
            return Utiles.responder_500("No se pudo anular la guía en facturacion.cl")

        with transaction.atomic():
            guia_actual = GuiaDespacho.objects.select_for_update().get(pk=guia_despacho_id)
            guia_actual.facturado = -1
            guia_actual.link = None
            guia_actual.save(update_fields=["facturado"])

        return Utiles.responder_200(
            "Guía anulada correctamente",
            {
                "id": guia_despacho_id,
                "facturado": -1,
                "respuesta": respuesta
            }
        )

    except:
        return Utiles.responder_500("Error inesperado", sys.exc_info())



def formatear_guiadespacho_full(guias):
    if isinstance(guias, GuiaDespacho):
        qs = GuiaDespacho.objects.filter(pk=guias.pk)
    elif isinstance(guias, QuerySet):
        qs = guias
    else:
        ids = []
        for g in guias or []:
            ids.append(g.pk if hasattr(g, "pk") else int(g))
        qs = GuiaDespacho.objects.filter(pk__in=ids)

    ordered_ids = list(qs.values_list("id", flat=True))

    qs = (
        GuiaDespacho.objects
        .filter(id__in=ordered_ids)
        .order_by("-fecha_guia", "-id")
        .select_related(
            "pesaje",
            "pesaje__camion",
            "pesaje__camion__cliente",
            "pesaje__camion__cliente__comuna",
        )
        .prefetch_related(
            Prefetch(
                "detalleguia_set",
                queryset=DetalleGuia.objects.select_related(
                    "camion",
                    "camion__cliente",
                    "camion__cliente__comuna",
                    "item",
                    "unidad",
                ).order_by("id")
            )
        )
    )

    resultado = []

    for guia in qs:
        detalles = list(guia.detalleguia_set.all())

        camion = None
        cliente = None

        if guia.pesaje and guia.pesaje.camion:
            camion = guia.pesaje.camion
            cliente = camion.cliente
        elif detalles:
            camion = detalles[0].camion
            cliente = camion.cliente if camion else None

        detalles_formateados = []
        for detalle in detalles:
            detalles_formateados.append({
                "id": detalle.id,
                "camion_id": detalle.camion.id if detalle.camion else None,
                "patente": detalle.camion.patente if detalle.camion else None,
                "item_id": detalle.item.id if detalle.item else None,
                "item_codigo": detalle.item.codigo if detalle.item else None,
                "item_descripcion": detalle.item.descripcion if detalle.item else None,
                "unidad_id": detalle.unidad.id if detalle.unidad else None,
                "unidad": detalle.unidad.nombre if detalle.unidad else None,
                "cantidad": detalle.cantidad,
                "precio_unitario": detalle.precio_unitario,
                "subtotal": int(round(detalle.cantidad * detalle.precio_unitario)) if detalle.cantidad is not None and detalle.precio_unitario is not None else 0
            })

        resultado.append({
            "guia": {
                "id": guia.id,
                "fecha_creacion": guia.fecha_creacion,
                "fecha_guia": guia.fecha_guia,
                "neto": guia.neto,
                "iva": guia.iva,
                "total": guia.total,
                "facturado": guia.facturado,
                "folio": guia.folio,
                "link": guia.link,
            },
            "cliente": {
                "id": cliente.id if cliente else None,
                "rut": cliente.rut if cliente else None,
                "nombre": cliente.nombre if cliente else None,
                "direccion": cliente.direccion if cliente else None,
                "correo": cliente.correo if cliente else None,
                "telefono": cliente.telefono if cliente else None,
                "giro": cliente.giro if cliente else None,
            },
            "comuna": {
                "id": cliente.comuna.id if cliente and cliente.comuna else None,
                "nombre": cliente.comuna.nombreComuna if cliente and cliente.comuna else None,
            },
            "camion": {
                "id": camion.id if camion else None,
                "patente": camion.patente if camion else None,
                "unidad": camion.unidad if camion else None,
                "peso_tara": camion.peso_tara if camion else None,
                "pesar": camion.pesar if camion else None,
            },
            "pesaje": {
                "id": guia.pesaje.id if guia.pesaje else None,
                "peso": guia.pesaje.peso if guia.pesaje else None,
                "fecha": guia.pesaje.fecha if guia.pesaje else None,
                "personal_id": guia.pesaje.personal.id if guia.pesaje and guia.pesaje.personal else None,
            },
            "detalles": detalles_formateados
        })

    return resultado



def formatear_guiadespacho(g: GuiaDespacho):
    camion = None
    cliente = None
    detalle_principal = None

    if g.pesaje and g.pesaje.camion:
        camion = g.pesaje.camion
        cliente = camion.cliente
    else:
        detalle_principal = DetalleGuia.objects.select_related(
            "camion",
            "camion__cliente"
        ).filter(guia=g).order_by("id").first()

        if detalle_principal:
            camion = detalle_principal.camion
            cliente = camion.cliente if camion else None

    return {
        "id": g.id,
        "fecha_creacion": g.fecha_creacion,
        "fecha_guia": g.fecha_guia,
        "neto": g.neto,
        "iva": g.iva,
        "total": g.total,
        "facturado": g.facturado,
        "folio": g.folio,
        "link": g.link,
        "pesaje_id": g.pesaje.id if g.pesaje else None,
        "camion_id": camion.id if camion else None,
        "patente": camion.patente if camion else None,
        "cliente_id": cliente.id if cliente else None,
        "cliente_rut": cliente.rut if cliente else None,
        "cliente_nombre": cliente.nombre if cliente else None,
    }



def _decode_b64_text(value: Optional[str]) -> Optional[str]:
    if not value:
        return None

    try:
        return b64decode(value).decode("utf-8")
    except (BinasciiError, UnicodeDecodeError, ValueError):
        return None


def generar_guia_facturacion(guia: GuiaDespacho):
    try:
        resp, data, params = generar_guia_despacho(guia.id)

        if not resp:
            return False, data

        params["file"] = b64encode(data.encode("utf-8")).decode("utf-8")
        params["formato"] = "2"  # 2 = XML

        try:
            enviar, etexto = enviar_datos(params, 1)
        except Exception:
            traceback.print_exc()
            return False, "Error enviando datos a facturacion.cl"

        if not enviar:
            return False, etexto

        try:
            root = ET.fromstring(etexto)
        except ET.ParseError:
            return False, "Respuesta XML inválida desde facturacion.cl"

        resultado = root.findtext("./Resultado")
        mensaje = root.findtext("./Mensaje")
        folio = root.findtext("./Detalle/Documento/Folio")

        # Primero urlCedible, si no existe entonces urlOriginal
        url_b64 = (
            root.findtext("./Detalle/Documento/urlCedible")
            or root.findtext("./Detalle/Documento/urlOriginal")
        )
        url_documento = _decode_b64_text(url_b64)
        url_documento = _ensure_https(url_documento)

        guia.facturacioncl = etexto
        guia.link = url_documento
        guia.folio = folio
        guia.xml = data
        guia.facturado = 1
        guia.save(update_fields=["facturacioncl", "link", "folio", "xml", "facturado"])

        return True, {
            "resultado": resultado,
            "mensaje": mensaje,
            "url_cedible": url_documento,
        }

    except GuiaDespacho.DoesNotExist:
        return False, "No existe documento con el id indicado"
    except Exception:
        traceback.print_exc()
        return False, "Error interno."



@api_view(["GET"])
def obtener_precios_por_item(request):
    try:
        validacion = validaciones_basicas(validaciones=[1, 2, 3, 4], perfiles=[1, 2, 3], request=request)
        if validacion != "ok":
            return Utiles.responder_500(validacion)
        
        precios = PreciosGD.objects.all()
        datos = [formatear_precio(p) for p in precios]

        return Utiles.responder_200(datos)
    except Exception:
        traceback.print_exc()
        return Utiles.responder_500("Error interno.")



def _ensure_https(url: str) -> str:
    """
    Fuerza que una URL use HTTPS sin alterar el resto de sus componentes.
    """
    if not url:
        return url

    try:
        parsed = urlparse(url)

        # Si no tiene esquema, opcionalmente podrías decidir qué hacer
        if not parsed.scheme:
            # fallback conservador
            return f"https://{url.lstrip('/')}"
        
        if parsed.scheme == "https":
            return url

        return urlunparse(parsed._replace(scheme="https"))

    except Exception:
        # fallback defensivo (no romper flujo por una URL malformada)
        return url.replace("http://", "https://", 1)



def formatear_item(i: ItemsGD):
    return {
        "id": i.id,
        "codigo": i.codigo,
        "descripcion": i.descripcion
    }



def formatear_unidad(u: UnidadGD):
    return {
        "id": u.id,
        "unidad": u.nombre
    }



def formatear_precio(p: PreciosGD):
    return {
        "id": p.id,
        "precio": p.precio,
        "unidad_id": p.unidad.id,
        "unidad": p.unidad.nombre,
        "item_id": p.item.id,
        "item": p.item.descripcion
    }



def validar_campos_requeridos(request, campos):
    # Valida que los campos requeridos estén presentes en la solicitud
    for campo in campos:
        if campo not in request.POST:
            return f"Falta {campo}"
    return "ok"