import re
import time
import hashlib
import logging
import httpx
from app.services.timezone import now as tznow
from lxml import etree

from app.config import settings

logger = logging.getLogger(__name__)

# Cache en memoria (por instancia Alfresco) del resultado de verificar si el
# modelo de datos "indexExpedient" existe. Evita consultar el diccionario en
# cada index.xml generado durante un sync recursivo.
_type_exists_cache: dict[str, tuple[float, bool]] = {}
_TYPE_EXISTS_TTL_SECONDS = 300

# Traducción del tipo de evento del histórico de expedientes al nombre del item XML
_HISTORIAL_TIPO_ES = {"close": "CierreExpediente", "reopen": "ReaperturaExpediente"}


def extract_tipologia(folder_name: str, folder_path: str) -> str:
    """Extrae la tipologia documental del nombre de la Serie, Subserie o Expediente segun ruta.
    Busca el elemento con formato numerado que contenga punto (X-XXXX-XX.X o X-XXXX-XX-XX.X).
    La estructura siempre es [numeracion con punto] [espacio] [nombre].
    Si no la hay, usa el elemento con formato numerado sin punto.
    Si no, usa el nombre del elemento padre inmediato."""
    if not folder_path:
        return folder_name or ""

    parts = [p for p in folder_path.strip("/").split("/") if p]

    # Buscar de abajo hacia arriba: elemento con formato que incluya punto
    # El formato es: inicio de la cadena, numeros y guiones, un punto, mas numeros, luego espacio
    for part in reversed(parts):
        if re.match(r'^[\d]+(?:-[\d]+)*\.[\d]+\s', part):
            return part

    # Si no hay con punto, buscar con formato numerado sin punto (2+ segmentos: X-XXXX o X-XXXX-XX)
    for part in reversed(parts):
        if re.match(r'^\d+-\d+', part):
            return part

    # Si no hay numeracion, usar el elemento padre inmediato
    if len(parts) >= 1:
        return parts[-1]

    return folder_name or ""


def _normalize_xml_for_comparison(xml_str: str) -> str:
    """Elimina Fecha_actualizacion_indice para comparar contenido real sin falsos positivos por timestamp."""
    if not xml_str:
        return ""
    return re.sub(r"<Fecha_actualizacion_indice>.*?</Fecha_actualizacion_indice>", "", xml_str).strip()


def build_index_xml_content(
    files: list[dict],
    folder_name: str,
    folder_path: str,
    tipologia_keys: list[str] | None = None,
    expediente_info: dict | None = None,
    historial: list[dict] | None = None,
) -> str:
    """Genera el contenido del index.xml en formato colombiano (TipoDocumentoFoliado).
    Si se pasan tipologia_keys, busca en las propiedades de cada archivo la primera
    key que coincida y usa su valor como Tipologia_Documental. Si no, queda vacío.
    Si se pasa historial (no vacío), se agrega el item historial_expediente dentro de
    ExpedienteIndizado con el historial de cierres/reaperturas del expediente."""

    root = etree.Element("TipoDocumentoFoliado")
    root.set(
        "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation",
        "http://www.w3.org/2001/XMLSchema-instance",
    )

    if expediente_info:
        exp = etree.SubElement(root, "ExpedienteIndizado")
        etree.SubElement(exp, "Identificador").text = str(expediente_info.get("identificador") or "")
        etree.SubElement(exp, "Fecha_creacion_indice").text = str(expediente_info.get("fecha_creacion_indice") or "")
        etree.SubElement(exp, "Fecha_actualizacion_indice").text = str(expediente_info.get("fecha_actualizacion_indice") or "")
        etree.SubElement(exp, "Nombre_de_expediente").text = str(expediente_info.get("nombre_de_expediente") or "")

        serie = expediente_info.get("serie_del_expediente")
        sub_serie = etree.SubElement(exp, "Serie_del_expediente")
        if serie:
            sub_serie.text = str(serie)

        subserie = expediente_info.get("subserie_del_expediente")
        sub_subserie = etree.SubElement(exp, "Subserie_del_expediente")
        if subserie:
            sub_subserie.text = str(subserie)

        etree.SubElement(exp, "Estado_expediente").text = str(expediente_info.get("estado_expediente") or "Abierto")
        etree.SubElement(exp, "Control_de_acceso").text = str(expediente_info.get("control_de_acceso") or "Publico")
        etree.SubElement(exp, "NombreEntidadContacto").text = str(expediente_info.get("nombre_entidad_contacto") or "")

        if historial:
            hist_root = etree.SubElement(exp, "HistorialExpediente")
            for h in historial:
                tipo = str(h.get("type") or "")
                tag = _HISTORIAL_TIPO_ES.get(tipo, "CierreExpediente")
                reg = etree.SubElement(hist_root, tag)
                etree.SubElement(reg, "Fecha").text = str(h.get("date") or "")
                etree.SubElement(reg, "Usuario").text = str(h.get("user") or "")
                etree.SubElement(reg, "Comentario").text = str(h.get("comment") or "")

    cumulative_page = 1
    for idx, f in enumerate(files, 1):
        doc = etree.SubElement(root, "DocumentoIndizado")

        file_tipologia = ""
        if tipologia_keys:
            props = f.get("properties", {}) or {}
            for key in tipologia_keys:
                if key in props:
                    val = props[key]
                    if val is not None and val != "":
                        file_tipologia = str(val)
                        break

        etree.SubElement(doc, "Id").text = f.get("id", "")
        etree.SubElement(doc, "Nombre_Documento").text = f.get("name", "")
        etree.SubElement(doc, "Tipologia_Documental").text = file_tipologia

        created = f.get("alfresco_created_at", "") or ""
        if created:
            created = created[:10]
        etree.SubElement(doc, "Fecha_Creacion_Documento").text = created
        etree.SubElement(doc, "Fecha_Incorporacion_Expediente").text = tznow().strftime("%Y-%m-%d")

        etree.SubElement(doc, "Autor").text = f.get("created_by_user", "") or ""
        etree.SubElement(doc, "Version").text = f.get("version", "") or ""

        etree.SubElement(doc, "Valor_Huella").text = f.get("hash_sha256", "")
        etree.SubElement(doc, "Funcion_Resumen").text = "SHA256"

        etree.SubElement(doc, "Orden_Documento_Expediente").text = str(idx)

        folios = f.get("folios", 1) or 1
        etree.SubElement(doc, "Pagina_Inicio").text = str(cumulative_page)
        etree.SubElement(doc, "Pagina_Fin").text = str(cumulative_page + folios - 1)
        cumulative_page += folios

        etree.SubElement(doc, "Formato").text = f.get("mimetype", "")

        size = f.get("size_bytes", 0) or 0
        if size > 1048576:
            size_str = f"{size / 1048576:.1f} MB"
        elif size > 1024:
            size_str = f"{size / 1024:.1f} KB"
        else:
            size_str = f"{size} bytes"
        etree.SubElement(doc, "Tamano").text = size_str

    declaration = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n'
    xml_str = declaration + etree.tostring(root, encoding="unicode", pretty_print=True)
    return xml_str


def compute_sha256_from_content(content: bytes) -> str:
    """Calcula SHA256 del contenido del archivo."""
    return hashlib.sha256(content).hexdigest()


def count_pages(content: bytes, mimetype: str) -> int:
    """Cuenta el número de páginas solo para PDFs. Todo lo demás retorna 1."""
    try:
        if not mimetype:
            return 1
        if "pdf" not in mimetype.lower():
            return 1
        try:
            from PyPDF2 import PdfReader
            from io import BytesIO
            reader = PdfReader(BytesIO(content))
            pages = len(reader.pages)
            return max(pages, 1)
        except Exception as e:
            logger.warning(f"Count pages PDF: {e}")
            return 1
    except Exception as e:
        logger.warning(f"Count pages: error con mimetype={mimetype}: {e}")
        return 1


def compute_and_update_hash(db, file_record, conn_base_url, auth, file_node_id):
    """Descarga el contenido del archivo desde Alfresco, calcula SHA256 y paginas, actualiza DB."""
    from app.models.alfresco_file import AlfrescoFile
    try:
        url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{file_node_id}/content"
        resp = httpx.get(url, auth=auth, timeout=120, verify=False)
        if resp.status_code == 200:
            content = resp.content
            file_hash = compute_sha256_from_content(content)
            pages = count_pages(content, file_record.mimetype or "")
            if file_record:
                file_record.hash_sha256 = file_hash
                file_record.folios = pages
                db.commit()
            return file_hash
        else:
            logger.warning(f"Hash: HTTP {resp.status_code} al descargar contenido de {file_node_id}")

    except Exception as e:
        logger.error(f"Hash: error al descargar {file_node_id}: {e}")
    return ""


def _ensure_versionable(conn_base_url, auth, node_id):
    """Asegura que el nodo tenga el aspect cm:versionable para mantener historial de versiones."""
    try:
        aspects_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{node_id}"
        resp = httpx.get(aspects_url, auth=auth, timeout=15, verify=False)
        if resp.status_code == 200:
            entry = resp.json().get("entry", {})
            aspect_names = entry.get("aspectNames", [])
            if "cm:versionable" not in aspect_names:
                aspect_add_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{node_id}"
                aspect_data = {"aspectNames": aspect_names + ["cm:versionable"]}
                httpx.put(aspect_add_url, auth=auth, json=aspect_data, timeout=15, verify=False)
    except Exception as e:
        logger.warning(f"Ensure versionable: error en {node_id}: {e}")


def _find_index_xml_node(conn_base_url, auth, folder_id):
    """Busca el node id del index.xml existente en la carpeta.
    Primero resuelve por relativePath (algunas instalaciones de Alfresco omiten
    index.xml del listado de hijos) y luego pagina los hijos como fallback."""
    node_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{folder_id}?relativePath=index.xml"
    try:
        resp = httpx.get(node_url, auth=auth, timeout=30, verify=False)
        if resp.status_code == 200:
            entry = resp.json().get("entry", {})
            if entry.get("isFile") and entry.get("name") == "index.xml":
                return entry.get("id"), None
    except Exception as e:
        logger.warning(f"Error buscando index.xml por relativePath en {folder_id}: {e}")

    base_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{folder_id}/children"
    skip_count = 0
    while True:
        list_url = f"{base_url}?maxItems=500&skipCount={skip_count}"
        resp = httpx.get(list_url, auth=auth, timeout=30, verify=False)
        if resp.status_code == 404:
            return None, f"folder {folder_id} not found in Alfresco (404)"
        if resp.status_code in (401, 403):
            return None, f"Auth error listing folder {folder_id} (HTTP {resp.status_code})"
        if resp.status_code != 200:
            return None, f"List folder {folder_id} failed HTTP {resp.status_code}"
        data = resp.json().get("list", {})
        entries = data.get("entries", [])
        count = len(entries)
        pagination = data.get("pagination", {})
        has_more = pagination.get("hasMoreItems", False)
        for e in entries:
            entry = e.get("entry", {})
            if entry.get("name") == "index.xml" and entry.get("isFile"):
                return entry.get("id"), None
        if not has_more and count < 500:
            return None, None
        skip_count += count


def _alfresco_type_exists(conn_base_url: str, auth, type_name: str) -> bool:
    """Verifica en el diccionario de Alfresco si existe el tipo/modelo `type_name`.
    Si el modelo no existe (o el endpoint no está disponible) retorna False para
    omitir la verificación y usar el tipo por defecto."""
    key = conn_base_url.rstrip("/")
    cached = _type_exists_cache.get(key)
    if cached and time.time() - cached[0] < _TYPE_EXISTS_TTL_SECONDS:
        return cached[1]

    exists = False
    try:
        short_name = type_name.replace(":", "_")
        url = f"{conn_base_url}/alfresco/service/api/classes/{short_name}"
        resp = httpx.get(url, auth=auth, timeout=15, verify=False)
        if resp.status_code == 200:
            try:
                data = resp.json()
            except Exception:
                data = None
            if isinstance(data, list):
                exists = len(data) > 0
            elif isinstance(data, dict):
                exists = bool(data.get("name") or data.get("className"))
    except Exception as e:
        logger.warning(f"Verificar tipo {type_name}: error consultando diccionario: {e}")
        exists = False

    _type_exists_cache[key] = (time.time(), exists)
    return exists


def upload_index_xml_to_alfresco(conn_base_url, auth, folder_id, xml_content):
    """Sube o actualiza el index.xml en la Serie, Subserie o Expediente de Alfresco, manteniendo historial de versiones."""
    try:
        existing_id, err = _find_index_xml_node(conn_base_url, auth, folder_id)
        if err:
            return False, None, err
        if existing_id:
            # Comparar contenido actual
            try:
                content_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{existing_id}/content"
                content_resp = httpx.get(content_url, auth=auth, verify=False, timeout=30)
                if content_resp.status_code == 200:
                    if _normalize_xml_for_comparison(content_resp.text) == _normalize_xml_for_comparison(xml_content):
                        logger.info("El contenido de index.xml es identico al existente, se omite actualizacion.")
                        return True, existing_id, None
            except Exception as e:
                logger.warning(f"Error comparando index.xml existente: {e}")

            _ensure_versionable(conn_base_url, auth, existing_id)
            update_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{existing_id}/content?majorVersion=false&comment=Actualizacion+automatica+Metafoliador+v{tznow().strftime('%Y%m%d%H%M%S')}"
            resp = httpx.put(
                update_url,
                auth=auth,
                content=xml_content.encode("utf-8"),
                headers={"Content-Type": "application/xml"},
                timeout=30,
                verify=False,
            )
            if resp.status_code in (200, 201, 202, 204):
                return True, existing_id, None
            return False, existing_id, f"Update failed HTTP {resp.status_code}: {resp.text[:200]}"
        else:
            create_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{folder_id}/children"
            node_type = settings.index_xml_node_type
            if _alfresco_type_exists(conn_base_url, auth, settings.index_expedient_type):
                node_type = settings.index_expedient_type
                logger.info(f"Usando modelo de datos {node_type} para index.xml")
            files = {
                "filedata": ("index.xml", xml_content.encode("utf-8"), "text/xml"),
            }
            form_data = {
                "name": "index.xml",
                "nodeType": node_type,
                "aspectNames": "cm:versionable",
                "autoRename": "false",
            }
            resp = httpx.post(
                create_url,
                auth=auth,
                files=files,
                data=form_data,
                timeout=30,
                verify=False,
            )
            if resp.status_code in (200, 201):
                new_node = resp.json().get("entry", {})
                new_id = new_node.get("id", "")
                if new_id:
                    return True, new_id, None
                return False, None, "No node id in create response"
            if resp.status_code == 409:
                logger.warning(f"index.xml already exists in folder {folder_id}, trying to find and update...")
                existing_id, err = _find_index_xml_node(conn_base_url, auth, folder_id)
                if err:
                    return False, None, f"index.xml already exists, unable to update ({err})"
                if existing_id:
                    _ensure_versionable(conn_base_url, auth, existing_id)
                    update_url = f"{conn_base_url}/alfresco/api/-default-/public/alfresco/versions/1/nodes/{existing_id}/content?majorVersion=false&comment=Actualizacion+automatica+Metafoliador+v{tznow().strftime('%Y%m%d%H%M%S')}"
                    resp3 = httpx.put(update_url, auth=auth, content=xml_content.encode("utf-8"), headers={"Content-Type": "application/xml"}, timeout=30, verify=False)
                    if resp3.status_code in (200, 201, 202, 204):
                        return True, existing_id, None
                    return False, existing_id, f"Update after conflict failed HTTP {resp3.status_code}"
                return False, None, "index.xml already exists, unable to update"
            return False, None, f"Create node failed HTTP {resp.status_code}: {resp.text[:200]}"
    except Exception as e:
        return False, None, f"Exception: {str(e)[:300]}"
