"""
Sincronización de capas geográficas en ArcGIS Enterprise
=========================================================
Compara una capa origen (fuente de datos nueva) con una capa destino
(publicada en Enterprise) e inserta únicamente los registros nuevos.

Dependencias:
    pip install arcgis pandas

Uso:
    python actualizar_capa_arcgis.py
"""

import pandas as pd
from arcgis.gis import GIS
from arcgis.features import FeatureLayer
from arcgis.geometry import Geometry
import logging
from datetime import datetime

# ─── Configuración de logging ────────────────────────────────────────────────
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    handlers=[
        logging.StreamHandler(),
        logging.FileHandler("sync_arcgis.log"),
    ],
)
log = logging.getLogger(__name__)


# ─── Parámetros ─────────────────────────────────────────────────────────────

ENTERPRISE_URL = "https://tu-empresa.maps.arcgis.com"  # ← Cambia esto
USERNAME       = "tu_usuario"                           # ← Cambia esto
PASSWORD       = "tu_contraseña"                        # ← Cambia esto

# URL del Feature Service destino (capa publicada en Enterprise)
URL_CAPA_DESTINO = "https://tu-empresa.maps.arcgis.com/arcgis/rest/services/.../FeatureServer/0"

# URL del Feature Service origen (fuente con datos nuevos)
URL_CAPA_ORIGEN  = "https://tu-empresa.maps.arcgis.com/arcgis/rest/services/.../FeatureServer/0"

# Campo que se usa como identificador único en ambas capas
CAMPO_CLAVE = "id_unico"   # ← Ajusta al nombre real del campo


# ─── Conexión ────────────────────────────────────────────────────────────────

def conectar_enterprise() -> GIS:
    """Establece la conexión con ArcGIS Enterprise."""
    log.info("Conectando a ArcGIS Enterprise: %s", ENTERPRISE_URL)
    gis = GIS(ENTERPRISE_URL, USERNAME, PASSWORD, verify_cert=True)
    log.info("Conectado como: %s", gis.properties.user.username)
    return gis


# ─── Carga de datos ──────────────────────────────────────────────────────────

def cargar_features(url: str, nombre: str) -> tuple[FeatureLayer, pd.DataFrame]:
    """
    Carga todos los features de una capa como DataFrame.

    Retorna:
        layer   → objeto FeatureLayer (para poder hacer edits después)
        df      → DataFrame con atributos + geometría serializada
    """
    log.info("Cargando capa '%s' desde: %s", nombre, url)
    layer = FeatureLayer(url)
    fset  = layer.query(where="1=1", out_fields="*", return_geometry=True)

    if not fset.features:
        log.warning("La capa '%s' no tiene features.", nombre)
        return layer, pd.DataFrame()

    # Convierte a DataFrame conservando la geometría como columna extra
    df = fset.sdf  # Spatial DataFrame
    log.info("  → %d features cargados.", len(df))
    return layer, df


# ─── Comparación ─────────────────────────────────────────────────────────────

def encontrar_nuevos(
    df_origen: pd.DataFrame,
    df_destino: pd.DataFrame,
    campo_clave: str,
) -> pd.DataFrame:
    """
    Retorna los registros de `df_origen` que NO existen en `df_destino`,
    comparando por `campo_clave`.
    """
    if df_destino.empty:
        log.info("La capa destino está vacía → todos los registros son nuevos.")
        return df_origen

    ids_destino = set(df_destino[campo_clave].dropna().astype(str))
    mascara_nuevo = ~df_origen[campo_clave].astype(str).isin(ids_destino)
    nuevos = df_origen[mascara_nuevo].copy()

    log.info(
        "Comparación: %d origen | %d destino | %d nuevos detectados",
        len(df_origen), len(df_destino), len(nuevos),
    )
    return nuevos


def resumen_diferencias(
    df_origen: pd.DataFrame,
    df_destino: pd.DataFrame,
    campo_clave: str,
) -> dict:
    """
    Genera un resumen con estadísticas de la comparación.
    Útil para revisar antes de hacer cambios.
    """
    ids_origen   = set(df_origen[campo_clave].astype(str))
    ids_destino  = set(df_destino[campo_clave].astype(str)) if not df_destino.empty else set()

    nuevos        = ids_origen - ids_destino
    solo_destino  = ids_destino - ids_origen   # existen en destino pero no en origen
    comunes       = ids_origen & ids_destino

    resumen = {
        "total_origen":       len(ids_origen),
        "total_destino":      len(ids_destino),
        "registros_nuevos":   len(nuevos),
        "solo_en_destino":    len(solo_destino),
        "registros_comunes":  len(comunes),
        "ids_nuevos":         sorted(nuevos)[:20],  # muestra hasta 20 IDs de ejemplo
    }

    log.info("─── Resumen de diferencias ───────────────────────")
    for k, v in resumen.items():
        log.info("  %-25s %s", k + ":", v)
    log.info("──────────────────────────────────────────────────")
    return resumen


# ─── Inserción de features nuevos ────────────────────────────────────────────

def df_a_features(df: pd.DataFrame) -> list[dict]:
    """
    Convierte un SpatialDataFrame (o DataFrame con columna SHAPE) a una lista
    de diccionarios de Feature compatibles con edit_features().
    """
    features = []
    for _, fila in df.iterrows():
        atributos = fila.drop(labels=["SHAPE", "OBJECTID"], errors="ignore").to_dict()

        # Limpia NaN para que la API de ArcGIS no los rechace
        atributos = {
            k: (None if pd.isna(v) else v)
            for k, v in atributos.items()
        }

        geometria = fila.get("SHAPE")
        feature   = {"attributes": atributos}

        if geometria is not None and not (isinstance(geometria, float)):
            try:
                feature["geometry"] = dict(geometria)
            except Exception:
                pass  # Si no se puede convertir, se omite

        features.append(feature)
    return features


def agregar_nuevos(layer: FeatureLayer, df_nuevos: pd.DataFrame) -> dict:
    """
    Inserta los features nuevos en la capa destino de ArcGIS Enterprise.
    Procesa en lotes de 200 para evitar límites del servidor.
    """
    if df_nuevos.empty:
        log.info("No hay features nuevos que agregar.")
        return {"agregados": 0, "errores": 0}

    features   = df_a_features(df_nuevos)
    tam_lote   = 200
    total      = len(features)
    agregados  = 0
    errores    = 0

    log.info("Insertando %d features en lotes de %d...", total, tam_lote)

    for inicio in range(0, total, tam_lote):
        lote = features[inicio: inicio + tam_lote]
        try:
            resultado = layer.edit_features(adds=lote)
            exitos = sum(1 for r in resultado.get("addResults", []) if r.get("success"))
            fallos = len(lote) - exitos
            agregados += exitos
            errores   += fallos

            if fallos:
                log.warning(
                    "  Lote %d-%d: %d ok, %d con error.",
                    inicio + 1, inicio + len(lote), exitos, fallos,
                )
                for r in resultado.get("addResults", []):
                    if not r.get("success"):
                        log.warning("    Error: %s", r.get("error", {}))
            else:
                log.info("  Lote %d-%d: %d insertados.", inicio + 1, inicio + len(lote), exitos)

        except Exception as exc:
            log.error("  Error en lote %d-%d: %s", inicio + 1, inicio + len(lote), exc)
            errores += len(lote)

    log.info("Inserción completada: %d ok | %d errores.", agregados, errores)
    return {"agregados": agregados, "errores": errores}


# ─── Orquestador principal ───────────────────────────────────────────────────

def sincronizar(
    solo_comparar: bool = False,
    confirmar: bool = True,
) -> dict:
    """
    Flujo completo de sincronización:
        1. Conecta con Enterprise
        2. Carga capas origen y destino
        3. Compara y muestra diferencias
        4. (Opcional) Inserta registros nuevos

    Args:
        solo_comparar : Si True, solo analiza sin insertar nada (modo seguro).
        confirmar     : Si True, pide confirmación antes de insertar.
    """
    inicio_ts = datetime.now()
    log.info("═══ Inicio de sincronización: %s ═══", inicio_ts.isoformat())

    # 1. Conexión
    gis = conectar_enterprise()

    # 2. Cargar capas
    capa_destino, df_destino = cargar_features(URL_CAPA_DESTINO, "destino")
    _,            df_origen  = cargar_features(URL_CAPA_ORIGEN,  "origen")

    # 3. Comparar
    resumen = resumen_diferencias(df_origen, df_destino, CAMPO_CLAVE)
    df_nuevos = encontrar_nuevos(df_origen, df_destino, CAMPO_CLAVE)

    resultado = {**resumen, "agregados": 0, "errores": 0}

    if solo_comparar:
        log.info("Modo solo-comparar activo. No se realizaron cambios.")
        return resultado

    if df_nuevos.empty:
        log.info("Capa destino ya está actualizada. Nada que insertar.")
        return resultado

    # 4. Confirmación (interactiva)
    if confirmar:
        respuesta = input(
            f"\n¿Deseas insertar {len(df_nuevos)} features nuevos en la capa destino? [s/N]: "
        )
        if respuesta.strip().lower() not in ("s", "si", "sí", "y", "yes"):
            log.info("Operación cancelada por el usuario.")
            return resultado

    # 5. Insertar
    stats = agregar_nuevos(capa_destino, df_nuevos)
    resultado.update(stats)

    duracion = (datetime.now() - inicio_ts).total_seconds()
    log.info("═══ Sincronización finalizada en %.1f s ═══", duracion)
    return resultado


# ─── Punto de entrada ────────────────────────────────────────────────────────

if __name__ == "__main__":
    # Para solo ver diferencias sin modificar nada:
    #   sincronizar(solo_comparar=True)
    #
    # Para insertar sin pedir confirmación (modo automático/scheduler):
    #   sincronizar(confirmar=False)

    reporte = sincronizar(solo_comparar=False, confirmar=True)
    print("\n=== Reporte final ===")
    for k, v in reporte.items():
        print(f"  {k:<25} {v}")
