#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
from shutil import which

PROJECT_ROOT = os.path.abspath(os.getcwd())

# === Archivos Compose (entornos) ===
BASE_COMPOSE = os.path.join(PROJECT_ROOT, "compose.yaml")
DEV_COMPOSE  = os.path.join(PROJECT_ROOT, "compose.dev.yaml")
PROD_COMPOSE = os.path.join(PROJECT_ROOT, "compose.prod.yaml")

# === Archivos Compose (globales) ===
NPM_COMPOSE     = os.path.join(PROJECT_ROOT, "compose.npm.yaml")
DBEAVER_COMPOSE = os.path.join(PROJECT_ROOT, "compose.dbeaver.yaml")

# Archivos .env
ENV_FILES = {
    "development": ".env.development",
    "production":  ".env.production",
}

# Nombres de proyecto para permitir DEV y PROD simultáneos
def _base_project():
    return os.path.basename(PROJECT_ROOT).lower() or "composeproj"

PROJECT_NAMES = {
    "development": f"{_base_project()}_dev",
    "production":  f"{_base_project()}_prod",
}

# Nombre de proyecto global (ambos yaml globales usan name: suitecoffee)
GLOBAL_PROJECT_NAME = "suitecoffee"

# ---------- Utilidades ----------
def check_prereqs():
    if which("docker") is None:
        fail("No se encontró 'docker' en el PATH.")
    try:
        run(["docker", "compose", "version"], check=True, capture_output=True)
    except Exception:
        fail("No se pudo ejecutar 'docker compose'. Asegúrate de tener Docker Compose v2.")

def run(cmd, check=False, capture_output=False):
    return subprocess.run(cmd, check=check, capture_output=capture_output, text=True)

def compose_files_args(env_key):
    """
    Devuelve los -f correctos según el entorno (dev/prod) + base.
    """
    if not os.path.exists(BASE_COMPOSE):
        fail("No se encontró compose.yaml en la raíz del proyecto.")

    args = ["-f", BASE_COMPOSE]

    if env_key == "development":
        if not os.path.exists(DEV_COMPOSE):
            fail("No se encontró compose.dev.yaml.")
        args += ["-f", DEV_COMPOSE]
    elif env_key == "production":
        if not os.path.exists(PROD_COMPOSE):
            fail("No se encontró compose.prod.yaml.")
        args += ["-f", PROD_COMPOSE]
    else:
        fail(f"Entorno desconocido: {env_key}")
    return args

def compose_files_args_global(kind):
    """
    Devuelve los -f correctos para servicios globales (npm/dbeaver).
    """
    if kind == "npm":
        if not os.path.exists(NPM_COMPOSE):
            fail("No se encontró compose.npm.yaml.")
        return ["-f", NPM_COMPOSE]
    elif kind == "dbeaver":
        if not os.path.exists(DBEAVER_COMPOSE):
            fail("No se encontró compose.dbeaver.yaml.")
        return ["-f", DBEAVER_COMPOSE]
    else:
        fail(f"Servicio global desconocido: {kind}")

def env_file_path(env_key):
    fname = ENV_FILES.get(env_key)
    if not fname:
        return None
    path = os.path.join(PROJECT_ROOT, fname)
    return path if os.path.exists(path) else None

def compose_cmd(base_args, env_key, env_file=None, project_name=None):
    """
    Construye: docker compose -f base -f env --env-file ... -p <name> <COMANDO> [OPCIONES]
    (importante: --env-file y -p son opciones globales y van antes del subcomando)
    """
    cmd = ["docker", "compose"]
    cmd += compose_files_args(env_key)
    if env_file:
        cmd += ["--env-file", env_file]
    if project_name:
        cmd += ["-p", project_name]
    cmd += base_args  # ["up","-d","--force-recreate"] o ["ps","--status","running","-q"]
    return cmd

def compose_cmd_global(base_args, kind, project_name=GLOBAL_PROJECT_NAME):
    """
    Comandos para servicios globales (npm/dbeaver):
    docker compose -f compose.<kind>.yaml -p suitecoffee <COMANDO> ...
    """
    cmd = ["docker", "compose"]
    cmd += compose_files_args_global(kind)
    if project_name:
        cmd += ["-p", project_name]
    cmd += base_args
    return cmd

def yes_no(prompt, default="n"):
    default = default.lower()
    hint = "[Y/n]" if default == "y" else "[y/N]"
    while True:
        resp = input(f"{prompt} {hint} ").strip().lower()
        if not resp:
            return default == "y"
        if resp in ("y", "yes", "s", "si", "sí"):
            return True
        if resp in ("n", "no"):
            return False
        print("Respuesta no reconocida. Por favor, responde con 'y' o 'n'.")

def print_header(title):
    print("\n" + "=" * 60)
    print(title)
    print("=" * 60 + "\n")

def info(msg): print(f"• {msg}")
def ok(msg): print(f"✓ {msg}")
def warn(msg): print(f"! {msg}")
def fail(msg):
    print(f"✗ {msg}")
    sys.exit(1)

# ---------- Helpers globales (servicios por archivo y ps por labels) ----------
def list_services_from_compose_file(compose_path):
    """Obtiene la lista de servicios definidos en un archivo compose específico."""
    cmd = ["docker", "compose", "-f", compose_path, "config", "--services"]
    proc = run(cmd, capture_output=True)
    if proc.returncode != 0:
        return []
    return [s.strip() for s in proc.stdout.splitlines() if s.strip()]

def list_services_global(kind):
    compose_path = NPM_COMPOSE if kind == "npm" else DBEAVER_COMPOSE
    return list_services_from_compose_file(compose_path)

def docker_ps_by_labels(project, service=None, running_only=True):
    """Lista contenedores por labels de compose (project/service)."""
    cmd = ["docker", "ps"]
    if running_only:
        # por defecto docker ps ya lista solo running; se deja explícito por claridad
        pass
    cmd += ["--filter", f"label=com.docker.compose.project={project}"]
    if service:
        cmd += ["--filter", f"label=com.docker.compose.service={service}"]
    cmd += ["-q"]
    proc = run(cmd, capture_output=True)
    if proc.returncode != 0:
        return []
    return [l.strip() for l in proc.stdout.splitlines() if l.strip()]

# ---------- Acciones (entornos) ----------
def bring_up(env_key, force_recreate=False):
    env_path = env_file_path(env_key)
    if not env_path:
        warn(f"No se encontró archivo de entorno para '{env_key}'. Continuando sin --env-file.")
    pname = PROJECT_NAMES.get(env_key)

    base_args = ["up", "-d"]
    if force_recreate:
        base_args.append("--force-recreate")

    cmd = compose_cmd(base_args, env_key=env_key, env_file=env_path, project_name=pname)
    info("Ejecutando: " + " ".join(cmd))
    proc = run(cmd)
    if proc.returncode == 0:
        ok(f"Entorno '{env_key}' levantado correctamente.")
    else:
        fail(f"Fallo al levantar entorno '{env_key}'. Código: {proc.returncode}")

def bring_down(env_key):
    env_path = env_file_path(env_key)
    pname = PROJECT_NAMES.get(env_key)
    cmd = compose_cmd(["down"], env_key=env_key, env_file=env_path, project_name=pname)
    info("Ejecutando: " + " ".join(cmd))
    proc = run(cmd)
    if proc.returncode == 0:
        ok(f"Entorno '{env_key}' detenido correctamente.")
    else:
        fail(f"Fallo al detener entorno '{env_key}'. Código: {proc.returncode}")

def running_ids(env_key):
    env_path = env_file_path(env_key)
    pname = PROJECT_NAMES.get(env_key)
    cmd = compose_cmd(["ps", "--status", "running", "-q"], env_key=env_key, env_file=env_path, project_name=pname)
    proc = run(cmd, capture_output=True)
    if proc.returncode != 0:
        return []
    return [l.strip() for l in proc.stdout.splitlines() if l.strip()]

# ---------- Acciones (globales) ----------
def bring_up_global(kind, force_recreate=False):
    base_args = ["up", "-d"]
    if force_recreate:
        base_args.append("--force-recreate")
    cmd = compose_cmd_global(base_args, kind=kind)
    info("Ejecutando: " + " ".join(cmd))
    proc = run(cmd)
    if proc.returncode == 0:
        ok(f"Servicio global '{kind}' levantado correctamente.")
    else:
        fail(f"Fallo al levantar servicio global '{kind}'. Código: {proc.returncode}")

def bring_down_global(kind, remove=False):
    """
    Apaga SOLO los servicios definidos en el compose global indicado.
    - Primero 'stop <servicios>'.
    - Opcionalmente, 'rm -f <servicios>' si remove=True.
    """
    services = list_services_global(kind)
    if not services:
        warn(f"No se encontraron servicios en compose.{kind}.yaml.")
        return
    # stop específico
    cmd = compose_cmd_global(["stop"] + services, kind=kind)
    info("Ejecutando: " + " ".join(cmd))
    proc = run(cmd)
    if proc.returncode != 0:
        fail(f"Fallo al detener servicios de '{kind}'. Código: {proc.returncode}")

    if remove:
        cmd_rm = compose_cmd_global(["rm", "-f"] + services, kind=kind)
        info("Ejecutando: " + " ".join(cmd_rm))
        proc_rm = run(cmd_rm)
        if proc_rm.returncode != 0:
            fail(f"Fallo al remover servicios de '{kind}'. Código: {proc_rm.returncode}")

    ok(f"Servicio(s) global(es) '{kind}' detenido(s).")

def running_ids_global(kind):
    """
    Detecta si el compose global está corriendo revisando contenedores por servicio,
    filtrando por labels (project+service).
    """
    services = list_services_global(kind)
    ids = []
    for svc in services:
        ids += docker_ps_by_labels(GLOBAL_PROJECT_NAME, service=svc, running_only=True)
    # eliminar duplicados
    return list(dict.fromkeys(ids))

# ---------- Estado y flujo ----------
def detect_status_summary():
    dev_running = running_ids("development")
    prod_running = running_ids("production")
    npm_running = running_ids_global("npm")
    dbeaver_running = running_ids_global("dbeaver")

    print_header("Estado actual")
    info(f"DESARROLLO: {len(dev_running)} contenedor(es) en ejecución.")
    info(f"PRODUCCIÓN: {len(prod_running)} contenedor(es) en ejecución.")
    info(f"NPM (global): {len(npm_running)} contenedor(es) en ejecución.")
    info(f"DBEAVER (global): {len(dbeaver_running)} contenedor(es) en ejecución.\n")

    return bool(dev_running), bool(prod_running), bool(npm_running), bool(dbeaver_running)

def detect_and_optionally_shutdown():
    """Muestra estado y ofrece (opcional) apagar dev/prod.
       Los servicios globales se gestionan desde el menú principal (levantar/apagar).
    """
    dev_on, prod_on, _npm_on, _dbeaver_on = detect_status_summary()

    options = []
    if dev_on:
        options.append(("1", "Apagar entorno de DESARROLLO", "development"))
    if prod_on:
        options.append(("2", "Apagar entorno de PRODUCCIÓN", "production"))
    options.append(("3", "Continuar sin detener nada", None))

    if len(options) == 1:
        return

    print("Selecciona una opción:")
    for opt in options:
        key, label = opt[0], opt[1]
        print(f"  {key}) {label}")

    while True:
        choice = input("> ").strip()
        selected = next((opt for opt in options if opt[0] == choice), None)
        if not selected:
            print("Opción inválida.")
            continue
        if choice == "3" or selected[2] is None:
            ok("Continuamos sin detener nada.")
            return
        env_key = selected[2]
        bring_down(env_key)
        return

def main_menu():
    # Consultar estado de globales para decidir si mostrar opciones de "Levantar" o "Apagar"
    _dev_on, _prod_on, npm_on, dbeaver_on = detect_status_summary()

    print_header("Gestor de entornos Docker Compose")
    print("Selecciona una opción:")
    print("  1) Levantar entorno de DESARROLLO")
    print("  2) Levantar entorno de PRODUCCIÓN")

    dynamic_keys = {}
    next_key = 3

    # NPM: opción según estado
    if not npm_on:
        print(f"  {next_key}) Levantar NPM (compose.npm.yaml)")
        dynamic_keys[str(next_key)] = ("global_up", "npm")
    else:
        print(f"  {next_key}) Apagar NPM (compose.npm.yaml)")
        dynamic_keys[str(next_key)] = ("global_down", "npm")
    next_key += 1

    # DBEAVER: opción según estado
    if not dbeaver_on:
        print(f"  {next_key}) Levantar DBEAVER (compose.dbeaver.yaml)")
        dynamic_keys[str(next_key)] = ("global_up", "dbeaver")
    else:
        print(f"  {next_key}) Apagar DBEAVER (compose.dbeaver.yaml)")
        dynamic_keys[str(next_key)] = ("global_down", "dbeaver")
    next_key += 1

    # Salir
    print(f"  {next_key}) Salir")
    exit_key = str(next_key)

    while True:
        choice = input("> ").strip()
        if choice == "1":
            force = yes_no("¿Usar --force-recreate para DESARROLLO?", default="n")
            bring_up("development", force_recreate=force)
            return
        elif choice == "2":
            force = yes_no("¿Usar --force-recreate para PRODUCCIÓN?", default="n")
            bring_up("production", force_recreate=force)
            return
        elif choice in dynamic_keys:
            action, kind = dynamic_keys[choice]
            if action == "global_up":
                force = yes_no(f"¿Usar --force-recreate para {kind.upper()}?", default="n")
                bring_up_global(kind, force_recreate=force)
                return
            elif action == "global_down":
                remove = yes_no(f"¿También remover contenedores de {kind.upper()}? (rm -f)", default="n")
                bring_down_global(kind, remove=remove)
                return
        elif choice == exit_key:
            ok("Saliendo.")
            sys.exit(0)
        else:
            print("Opción inválida. Elige una de las opciones listadas.")

def main():
    try:
        check_prereqs()
        # Mostrar estado y permitir opcionalmente apagar dev/prod
        detect_and_optionally_shutdown()
        # Menú de gestión (incluye globales: levantar o apagar según estado)
        main_menu()
    except KeyboardInterrupt:
        print("\n")
        ok("Interrumpido por el usuario (Ctrl+C). Saliendo.")
        sys.exit(0)
    except Exception as e:
        fail(f"Ocurrió un error inesperado: {e}")

if __name__ == "__main__":
    main()
