# -*- coding: utf-8 -*-
"""Compara tu archivo con el objetivo de una practica.

    python comparar.py mi_salida.csv clientes_limpios.csv

Si los dos archivos coinciden, dice OK. Si no, te dice EXACTAMENTE que difiere:
que columna, que fila y que valor. No es un corrector con nota: es la misma
comparacion que harias a mano, hecha bien.

Lee los dos archivos de la MISMA manera, usando los tipos que declara el
contrato de la practica (busca contrato.json al lado del objetivo). Eso importa:
si compararas tu DataFrame en memoria contra un CSV, aparecerian diferencias de
tipo que no son errores tuyos.

Opciones:
    --laxo        no compara los tipos, solo los valores. Sirve para saber si tu
                  logica esta bien antes de pelearte con int64 contra float64.
    --filas N     cuantas diferencias mostrar como maximo (por defecto 10)

Necesita pandas.
"""

import json
import sys
from pathlib import Path

try:
    import pandas as pd
except ImportError:
    print("Falta pandas. Instalalo con:  pip install pandas")
    raise SystemExit(2)

MAX_DIFERENCIAS = 10


def salir(mensaje, codigo=1):
    print(mensaje)
    raise SystemExit(codigo)


def comparar_json(mio, esperado, max_diferencias=MAX_DIFERENCIAS):
    """Compara dos estructuras JSON y describe la PRIMERA diferencia real.

    Un .json no se puede comparar por lineas: dos archivos con las claves en
    distinto orden son el mismo dato. Se comparan las estructuras.
    """
    diferencias = []

    def recorrer(a, b, ruta):
        if len(diferencias) >= max_diferencias:
            return
        if type(a) is not type(b) and not (
                isinstance(a, (int, float)) and isinstance(b, (int, float))):
            diferencias.append(
                f"  {ruta}: el tuyo es {type(a).__name__} y se esperaba "
                f"{type(b).__name__}")
            return
        if isinstance(b, dict):
            faltan = [k for k in b if k not in a]
            sobran = [k for k in a if k not in b]
            for k in faltan:
                diferencias.append(f"  {ruta}: te falta la clave '{k}'")
            for k in sobran:
                diferencias.append(f"  {ruta}: te sobra la clave '{k}'")
            for k in b:
                if k in a:
                    recorrer(a[k], b[k], f"{ruta}.{k}" if ruta else k)
        elif isinstance(b, list):
            if len(a) != len(b):
                diferencias.append(
                    f"  {ruta}: tiene {len(a)} elementos y se esperaban "
                    f"{len(b)}")
            for i, (x, y) in enumerate(zip(a, b)):
                recorrer(x, y, f"{ruta}[{i}]")
        elif isinstance(b, float) or isinstance(a, float):
            if abs(float(a) - float(b)) > 1e-9:
                diferencias.append(f"  {ruta}: {a!r} en vez de {b!r}")
        elif a != b:
            diferencias.append(f"  {ruta}: {a!r} en vez de {b!r}")

    recorrer(mio, esperado, "")
    return diferencias


def cargar_contrato(objetivo):
    candidato = objetivo.parent / "contrato.json"
    if not candidato.is_file():
        return None
    try:
        return json.loads(candidato.read_text(encoding="utf-8"))
    except (ValueError, OSError):
        return None


def tipos_declarados(contrato, nombre_archivo):
    """Devuelve {columna: dtype} para leer los dos archivos igual."""
    if not contrato:
        return None
    for salida in contrato.get("salidas", []):
        if salida.get("archivo") != nombre_archivo:
            continue
        mapa = {}
        for columna in salida.get("columnas", []):
            tipo = columna.get("tipo")
            if tipo == "texto":
                mapa[columna["nombre"]] = "str"
            elif tipo == "entero":
                mapa[columna["nombre"]] = "Int64"
            elif tipo == "decimal":
                mapa[columna["nombre"]] = "float64"
        return mapa or None
    return None


def leer(ruta, tipos):
    try:
        if tipos:
            return pd.read_csv(ruta, dtype=tipos, keep_default_na=True)
        return pd.read_csv(ruta)
    except FileNotFoundError:
        salir(f"No encuentro el archivo: {ruta}", 2)
    except Exception as error:
        salir(f"No pude leer {ruta.name}: {type(error).__name__}: {error}", 2)


def comparar_columnas(mio, esperado):
    if list(mio.columns) == list(esperado.columns):
        return True
    faltan = [c for c in esperado.columns if c not in mio.columns]
    sobran = [c for c in mio.columns if c not in esperado.columns]
    print("Las columnas no coinciden.")
    print(f"  esperadas: {list(esperado.columns)}")
    print(f"  las tuyas: {list(mio.columns)}")
    if faltan:
        print(f"  te faltan: {faltan}")
    if sobran:
        print(f"  te sobran: {sobran}")
    if not faltan and not sobran:
        print("  Estan todas, pero en otro orden. El contrato fija el orden.")
    return False


def comparar_filas(mio, esperado):
    if len(mio) == len(esperado):
        return True
    print(f"La cantidad de filas no coincide: tenes {len(mio)}, "
          f"se esperaban {len(esperado)}.")
    clave = esperado.columns[0]
    if clave in mio.columns and esperado[clave].is_unique:
        mios = set(mio[clave].astype(str))
        suyos = set(esperado[clave].astype(str))
        faltan = sorted(suyos - mios)
        sobran = sorted(mios - suyos)
        if faltan:
            print(f"  te faltan {len(faltan)} por {clave}: "
                  f"{', '.join(faltan[:8])}{' ...' if len(faltan) > 8 else ''}")
        if sobran:
            print(f"  te sobran {len(sobran)} por {clave}: "
                  f"{', '.join(sobran[:8])}{' ...' if len(sobran) > 8 else ''}")
        if not faltan and not sobran:
            print(f"  Los {clave} son los mismos: hay filas repetidas.")
    return False


def comparar_tipos(mio, esperado):
    distintos = [(c, str(mio[c].dtype), str(esperado[c].dtype))
                 for c in esperado.columns
                 if str(mio[c].dtype) != str(esperado[c].dtype)]
    if not distintos:
        return True
    print("Los tipos de algunas columnas no coinciden:")
    for columna, tuyo, suyo in distintos:
        print(f"  {columna}: la tuya es {tuyo}, se esperaba {suyo}")
    print("  Si el resto esta bien, proba de nuevo con --laxo para "
          "confirmar que la logica es correcta.")
    return False


def comparar_valores(mio, esperado, maximo):
    diferencias = []
    for columna in esperado.columns:
        a = mio[columna]
        b = esperado[columna]
        distinto = ~((a == b) | (a.isna() & b.isna()))
        for posicion in distinto[distinto].index[: maximo + 1]:
            diferencias.append((posicion, columna, a.loc[posicion], b.loc[posicion]))
    if not diferencias:
        return True
    diferencias.sort(key=lambda d: (d[0], d[1]))
    print(f"Hay {len(diferencias)} celda(s) con valores distintos"
          f"{' (muestro las primeras)' if len(diferencias) > maximo else ''}:")
    for posicion, columna, tuyo, suyo in diferencias[:maximo]:
        print(f"  fila {posicion} (la {posicion + 2} del archivo), columna "
              f"{columna}: tenes {mostrar(tuyo)}, se esperaba {mostrar(suyo)}")
    return False


def mostrar(valor):
    """repr limpio: los escalares de numpy se imprimen como np.int64(99)."""
    if valor is None or (isinstance(valor, float) and valor != valor):
        return "(vacio)"
    if hasattr(valor, "item"):
        try:
            valor = valor.item()
        except (AttributeError, ValueError):
            pass
    return repr(valor)


def comparar_archivos_json(mio_ruta, objetivo_ruta, maximo):
    """Compara dos archivos JSON estructura contra estructura."""
    def cargar(ruta):
        try:
            return json.loads(ruta.read_text(encoding="utf-8"))
        except FileNotFoundError:
            salir(f"No encuentro el archivo: {ruta}", 2)
        except ValueError as error:
            salir(f"{ruta.name} no es JSON valido: {error}", 2)

    mio = cargar(mio_ruta)
    esperado = cargar(objetivo_ruta)

    diferencias = comparar_json(mio, esperado, maximo)
    if diferencias:
        print(f"{mio_ruta.name} difiere de {objetivo_ruta.name}:")
        for linea in diferencias:
            print(linea)
        return 1

    cuantos = len(esperado) if isinstance(esperado, list) else 1
    print(f"OK: {mio_ruta.name} coincide con {objetivo_ruta.name} "
          f"({cuantos} registros).")
    return 0


def main(argumentos):
    laxo = "--laxo" in argumentos
    maximo = MAX_DIFERENCIAS
    if "--filas" in argumentos:
        i = argumentos.index("--filas")
        try:
            maximo = int(argumentos[i + 1])
            del argumentos[i:i + 2]
        except (IndexError, ValueError):
            salir("--filas necesita un numero. Ejemplo: --filas 20", 2)
    rutas = [a for a in argumentos if not a.startswith("--")]
    if len(rutas) != 2:
        salir(__doc__.strip(), 2)

    mio_ruta, objetivo_ruta = Path(rutas[0]), Path(rutas[1])

    if objetivo_ruta.suffix == ".json":
        return comparar_archivos_json(mio_ruta, objetivo_ruta, maximo)

    contrato = cargar_contrato(objetivo_ruta)
    tipos = None if laxo else tipos_declarados(contrato, objetivo_ruta.name)

    mio = leer(mio_ruta, tipos)
    esperado = leer(objetivo_ruta, tipos)

    if not comparar_columnas(mio, esperado):
        return 1
    if not comparar_filas(mio, esperado):
        return 1
    if not laxo and not comparar_tipos(mio, esperado):
        return 1
    if not comparar_valores(mio, esperado, maximo):
        return 1

    print(f"OK: {mio_ruta.name} coincide con {objetivo_ruta.name} "
          f"({len(esperado)} filas, {len(esperado.columns)} columnas)"
          f"{' - sin comparar tipos, por --laxo' if laxo else ''}.")
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
