"""
Redis y rutas de archivos para imports CSV (compartido por módulos layouts_csv).
Cada módulo usa un job_type (ej. "part", "cls", "bom") para prefijos y nombres de archivo.
"""
import os
import base64
import json
import logging
from typing import Optional, Set, List

from core.paths import layout_path

logger = logging.getLogger(__name__)

IMPORT_REDIS_TTL = 3600


class ImportStoreError(Exception):
    """Fallo al guardar CSV en Redis/MinIO."""

    pass


def _get_redis():
    import redis
    url = os.getenv("VALKEY_URL", os.getenv("REDIS_URL", "redis://valkey:6379/0"))
    return redis.Redis.from_url(url, decode_responses=False)


def upload_dir() -> str:
    """Directorio temporal para CSV en el worker."""
    return layout_path("imports", "temp")


def error_dir() -> str:
    """Directorio de archivos JSONL de errores."""
    return layout_path("imports", "errors")


def storage_keys(job_type: str, job_id: str) -> tuple:
    """Prefijos Redis para un job_type y job_id. Devuelve (file_key, meta_key, error_lines_key)."""
    # Facturas usa prefijo "import_" sin tipo (compatibilidad con rutas existentes)
    if job_type == "" or job_type == "invoice":
        prefix = "import_"
    else:
        prefix = f"{job_type}_import_"
    return (
        f"{prefix}file:{job_id}",
        f"{prefix}meta:{job_id}",
        f"{prefix}error_lines:{job_id}",
    )


def file_path_for_job(job_type: str, job_id: str) -> str:
    """Ruta local del archivo CSV para un job."""
    if job_type == "" or job_type == "invoice":
        return os.path.join(upload_dir(), f"{job_id}.csv")
    return os.path.join(upload_dir(), f"{job_type}_{job_id}.csv")


def error_path_for_job(job_type: str, job_id: str) -> str:
    """Ruta del archivo JSONL de errores para un job."""
    os.makedirs(error_dir(), exist_ok=True)
    if job_type == "" or job_type == "invoice":
        return os.path.join(error_dir(), f"{job_id}.jsonl")
    return os.path.join(error_dir(), f"{job_type}_{job_id}.jsonl")


def store_import_file(
    job_type: str,
    job_id: str,
    raw_bytes: bytes,
    meta_dict: dict,
    tenant_id,
    company_id: int,
    ttl: int = IMPORT_REDIS_TTL,
    log_label: str = "",
) -> None:
    """
    Guarda CSV y meta en Redis. Si CSV_IMPORT_STORAGE=minio, sube el CSV a S3 y en Redis
    guarda JSON {"v":2,"s3_key":...}; si no, base64 en Redis (comportamiento anterior).
    En modo redis opcionalmente escribe copia local bajo layouts/imports/temp (debug).
    """
    from core.config import settings

    file_key, meta_key, _ = storage_keys(job_type, job_id)
    r = _get_redis()
    meta_bytes = json.dumps(meta_dict).encode("utf-8")

    if settings.CSV_IMPORT_STORAGE == "minio":
        from core.storage_s3 import put_csv_object, s3_key_for_csv_import

        key = s3_key_for_csv_import(tenant_id, company_id, job_type, job_id)
        try:
            put_csv_object(key, raw_bytes)
        except Exception as e:
            logger.exception("%s MinIO put failed: %s", log_label or job_type, e)
            raise ImportStoreError(str(e)) from e
        payload = json.dumps({"v": 2, "s3_key": key}).encode("utf-8")
        r.set(file_key, payload, ex=ttl)
    else:
        r.set(file_key, base64.b64encode(raw_bytes), ex=ttl)

    r.set(meta_key, meta_bytes, ex=ttl)

    if settings.CSV_IMPORT_STORAGE != "minio":
        try:
            path = file_path_for_job(job_type, job_id)
            os.makedirs(upload_dir(), exist_ok=True)
            with open(path, "wb") as f:
                f.write(raw_bytes)
            meta_path = path.replace(".csv", ".meta.json")
            with open(meta_path, "w", encoding="utf-8") as f:
                json.dump(meta_dict, f)
        except Exception as e:
            logger.warning("%s local file save failed: %s", log_label or job_type, e)


def ensure_file_from_redis(job_type: str, job_id: str, log_prefix: str = "") -> Optional[str]:
    """
    Obtiene el CSV desde Redis (referencia MinIO o base64 legacy) y lo escribe en disco.
    Devuelve la ruta del archivo o None si no hay datos o falla.
    """
    from core.config import settings

    file_key, _, _ = storage_keys(job_type, job_id)
    r = _get_redis()
    data = r.get(file_key)
    if not data:
        return None

    path = file_path_for_job(job_type, job_id)
    os.makedirs(upload_dir(), exist_ok=True)

    if data.startswith(b"{"):
        try:
            obj = json.loads(data.decode("utf-8"))
            if isinstance(obj, dict) and obj.get("v") == 2 and obj.get("s3_key"):
                if settings.CSV_IMPORT_STORAGE != "minio":
                    logger.warning(
                        "%s Redis has MinIO ref but CSV_IMPORT_STORAGE=%s",
                        log_prefix or job_type,
                        settings.CSV_IMPORT_STORAGE,
                    )
                from core.storage_s3 import get_object_bytes

                try:
                    raw = get_object_bytes(obj["s3_key"])
                except Exception as e:
                    logger.warning("%s MinIO get failed: %s", log_prefix or job_type, e)
                    return None
                with open(path, "wb") as f:
                    f.write(raw)
                return path
        except json.JSONDecodeError:
            pass
        except Exception as e:
            logger.warning("%s failed to parse MinIO ref from Redis: %s", log_prefix or job_type, e)
            return None

    try:
        raw = base64.b64decode(data)
    except Exception as e:
        logger.warning("%s failed to decode file from Redis: %s", log_prefix or job_type, e)
        return None
    with open(path, "wb") as f:
        f.write(raw)
    return path


def ensure_meta_from_redis(job_type: str, job_id: str, file_path: str, log_prefix: str = "") -> bool:
    """Descarga meta desde Redis y la escribe en .meta.json. Devuelve True si hubo datos."""
    _, meta_key, _ = storage_keys(job_type, job_id)
    r = _get_redis()
    data = r.get(meta_key)
    if not data:
        return False
    try:
        meta = json.loads(data.decode("utf-8"))
    except Exception as e:
        logger.warning("%s failed to decode meta from Redis: %s", log_prefix or job_type, e)
        return False
    meta_path = file_path.replace(".csv", ".meta.json")
    with open(meta_path, "w", encoding="utf-8") as f:
        json.dump(meta, f)
    return True


def store_error_lines(job_type: str, job_id: str, line_numbers: List[int]) -> None:
    """Guarda la lista de números de línea con error en Redis."""
    _, _, error_key = storage_keys(job_type, job_id)
    try:
        r = _get_redis()
        r.set(error_key, json.dumps(line_numbers).encode("utf-8"), ex=IMPORT_REDIS_TTL)
    except Exception as e:
        logger.warning("Failed to store error lines in Redis: %s", e)


def get_error_lines(job_type: str, job_id: str, error_path: str) -> Set[int]:
    """
    Obtiene el conjunto de líneas con error: primero desde Redis, si está vacío desde el JSONL.
    """
    _, _, error_key = storage_keys(job_type, job_id)
    lines = set()
    try:
        r = _get_redis()
        raw = r.get(error_key)
        if raw:
            lines = set(json.loads(raw.decode("utf-8")))
    except Exception as e:
        logger.debug("Could not load error lines from Redis: %s", e)
    if not lines and os.path.exists(error_path):
        with open(error_path, "r", encoding="utf-8") as f:
            for line in f:
                try:
                    err = json.loads(line)
                    if "line" in err:
                        lines.add(err["line"])
                except Exception:
                    pass
    return lines


def delete_import_from_redis(job_type: str, job_id: str) -> None:
    """Borra claves Redis del import (file, meta, error_lines)."""
    file_key, meta_key, error_key = storage_keys(job_type, job_id)
    try:
        r = _get_redis()
        r.delete(file_key, meta_key, error_key)
    except Exception as e:
        logger.warning("Failed to delete import keys from Redis: %s", e)


def cleanup_import_job(
    job_type: str,
    job_id: str,
    file_path: Optional[str] = None,
    error_path: Optional[str] = None,
    meta_path: Optional[str] = None,
) -> None:
    """Elimina archivos locales, objeto MinIO si aplica, y claves Redis del job."""
    from core.config import settings

    if settings.CSV_IMPORT_STORAGE == "minio":
        from core.storage_s3 import delete_object_if_exists
        from core.s3_keys import legacy_csv_import_key

        file_key, _, _ = storage_keys(job_type, job_id)
        try:
            r = _get_redis()
            data = r.get(file_key)
            deleted = False
            if data and data.startswith(b"{"):
                try:
                    obj = json.loads(data.decode("utf-8"))
                    if isinstance(obj, dict) and obj.get("v") == 2 and obj.get("s3_key"):
                        delete_object_if_exists(obj["s3_key"])
                        deleted = True
                except Exception as e:
                    logger.warning("Cleanup: parse Redis file ref: %s", e)
            if not deleted:
                delete_object_if_exists(legacy_csv_import_key(job_type, job_id))
        except Exception as e:
            logger.warning("Cleanup: MinIO delete failed: %s", e)

    if file_path and os.path.exists(file_path):
        try:
            os.remove(file_path)
        except Exception as e:
            logger.warning("Cleanup: failed to remove file %s: %s", file_path, e)
    if error_path and os.path.exists(error_path):
        try:
            os.remove(error_path)
        except Exception as e:
            logger.warning("Cleanup: failed to remove error file %s: %s", error_path, e)
    if meta_path and os.path.exists(meta_path):
        try:
            os.remove(meta_path)
        except Exception as e:
            logger.warning("Cleanup: failed to remove meta %s: %s", meta_path, e)
    delete_import_from_redis(job_type, job_id)
