"""nikame up — Start infrastructure services locally."""

import secrets
import shutil
import subprocess
import sys
import time
from pathlib import Path

import click
import questionary

from nikame.blueprint.engine import build_blueprint
from nikame.config.schema import NikameConfig
from nikame.utils.logger import console


def _ensure_binary(name: str, install_hint: str) -> None:
    """Check if a binary exists in PATH, else raise helpful error."""
    if not shutil.which(name):
        console.print(f"\n[error]✗ Required tool [bold]'{name}'[/bold] not found in PATH.[/error]")
        console.print(f"[tip]💡 Install Hint:[/tip] {install_hint}")
        console.print("\n[cyan]Alternatives:[/cyan]")
        console.print("If you don't have Kubernetes installed, change [bold]target: local[/bold] in [bold]nikame.yaml[/bold] to use Docker Compose.")
        raise SystemExit(1)


def _ensure_venv(project_dir: Path) -> None:
    """Ensure .venv exists and requirements are installed."""
    venv_dir = project_dir / ".venv"
    app_dir = project_dir / "app"
    
    if not venv_dir.exists():
        console.print("[info]Creating isolated .venv...[/info]")
        try:
            subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
        except subprocess.CalledProcessError as e:
            console.print(f"[error]✗ Failed to create venv: {e}[/error]")
            return

    # Install requirements if they exist
    pip_bin = venv_dir / ("Scripts" if sys.platform == "win32" else "bin") / "pip"
    req_file = app_dir / "requirements.txt"
    
    if req_file.exists():
        with console.status("[info]Installing/Updating requirements...[/info]"):
            try:
                subprocess.run([str(pip_bin), "install", "-r", str(req_file)], check=True, capture_output=True)
            except subprocess.CalledProcessError as e:
                console.print(f"[error]✗ Failed to install requirements: {e.stderr.decode()}[/error]")


def _run_migrations(project_dir: Path, config_name: str, compose_file: Path) -> None:
    """Run database migrations and seed data."""
    app_dir = project_dir / "app"
    if not (app_dir / "alembic").exists():
        return

    console.print("[info]Running database migrations...[/info]")
    cmd = ["docker", "compose", "-p", config_name, "-f", str(compose_file), "run", "--rm", "api", "alembic", "upgrade", "head"]
    try:
        subprocess.run(cmd, check=True, cwd=str(project_dir))
        console.print("[success]✓ Migrations applied successfully.[/success]")
    except subprocess.CalledProcessError:
        console.print("[error]✗ Migration failed.[/error]")

    # Optional: Run seeds
    seed_script = project_dir / "scripts" / "seed_db.py"
    if seed_script.exists():
        console.print("[info]Seeding database...[/info]")
        cmd = ["docker", "compose", "-p", config_name, "-f", str(compose_file), "run", "--rm", "api", "python", "/app/../scripts/seed_db.py"]
        try:
            subprocess.run(cmd, check=True, cwd=str(project_dir))
            console.print("[success]✓ Database seeded.[/success]")
        except subprocess.CalledProcessError:
            console.print("[warning]⚠ Seeding failed.[/warning]")


def _prompt_missing_env_vars(project_dir: Path) -> None:
    """Check .env.example against local env files and prompt for missing vars."""
    env_example = project_dir / ".env.example"
    env_generated = project_dir / ".env.generated"
    env_local = project_dir / ".env"

    if not env_example.exists():
        return

    # Parse what we need from .env.example
    required_vars = {}
    current_desc = ""
    for line in env_example.read_text().splitlines():
        line = line.strip()
        if line.startswith("# "):
            current_desc = line[2:]
        elif line and not line.startswith("#") and "=" in line:
            key = line.split("=", 1)[0]
            required_vars[key] = current_desc
            current_desc = ""

    # Parse what we have
    existing_vars = set()
    for env_file in [env_generated, env_local]:
        if env_file.exists():
            for line in env_file.read_text().splitlines():
                line = line.strip()
                if line and not line.startswith("#") and "=" in line:
                    key, val = line.split("=", 1)
                    if val.strip():  # Only count if it has a truthy value
                        existing_vars.add(key.strip())

    missing_vars = [k for k in required_vars if k not in existing_vars]

    if missing_vars:
        if not sys.stdout.isatty():
            console.print("[warning]⚠ Missing environment variables detected, but terminal is not interactive.[/warning]")
            return

        console.print("\n[bold yellow]🔐 Missing Credentials Detected[/bold yellow]")
        console.print("[dim]Please provide values for the following required environment variables[/dim]")
        console.print("[dim](Leave passwords blank to auto-generate secure values):[/dim]\n")

        new_vars = []
        for key in missing_vars:
            desc = required_vars.get(key, "")
            prompt_text = f"{key} ({desc}):"
            
            if "PASSWORD" in key or "SECRET" in key or "TOKEN" in key:
                val = questionary.password(prompt_text).ask()
                if not val:
                    val = secrets.token_urlsafe(32)
                    console.print(f"  [dim]→ Auto-generated secured value for {key}[/dim]")
            else:
                val = questionary.text(prompt_text).ask()

            if val is not None:
                new_vars.append(f"{key}={val}")

        if new_vars:
            with env_local.open("a") as f:
                f.write("\n# Added by Nikame Interactive Prompt\n")
                f.write("\n".join(new_vars) + "\n")
            console.print("\n[success]✓ Credentials securely saved to .env[/success]\n")


@click.command()
@click.option(
    "--service", "-s",
    multiple=True,
    help="Specific service(s) to start. Omit for all.",
)
@click.option(
    "--build",
    is_flag=True,
    help="Rebuild images before starting.",
)
@click.option(
    "--detach/--no-detach", "-d",
    default=True,
    help="Run in detached mode (default: yes).",
)
@click.option(
    "--project-dir",
    type=click.Path(exists=True, path_type=Path),
    default=Path("."),
    help="Project directory containing infra/.",
)
@click.option(
    "--timeout",
    type=int,
    default=300,
    help="Timeout in seconds for each phase (default: 300).",
)
@click.pass_context
def up(
    ctx: click.Context,
    service: tuple[str, ...],
    build: bool,
    detach: bool,
    project_dir: Path,
    timeout: int,
) -> None:
    """Start NIKAME infrastructure services."""
    from nikame.config.loader import load_config
    config_path = project_dir / "nikame.yaml"

    if not config_path.exists():
        console.print("[error]✗ nikame.yaml not found.[/error]")
        raise SystemExit(1)

    config = load_config(config_path)
    target = config.environment.target

    if target == "local":
        _up_local(project_dir, config, service, build, detach, timeout)
    elif target in ["aws", "gcp", "azure"]:
        _up_cloud(project_dir, target)
    elif target == "kubernetes":
        _up_k8s(project_dir)
    else:
        console.print(f"[error]✗ Target '{target}' not yet supported for 'up'[/error]")

def _up_local(project_dir: Path, config: NikameConfig, service: tuple[str, ...], build: bool, detach: bool, timeout: int) -> None:
    _ensure_binary("docker", "Visit https://docs.docker.com/get-docker/")
    compose_file = project_dir / "infra" / "docker-compose.yml"
    if not compose_file.exists():
        console.print("[error]✗ docker-compose.yml not found. Run 'nikame init' first.[/error]")
        raise SystemExit(1)

    import questionary
    import yaml

    from nikame.utils.docker import find_conflicting_containers, stop_containers

    with open(compose_file) as f:
        try:
            compose_data = yaml.safe_load(f)
            conflicts = find_conflicting_containers(compose_data)
            if conflicts:
                console.print(f"[warning]⚠ Found {len(conflicts)} conflicting containers already running on host ports.[/warning]")
                for c in conflicts:
                    console.print(f"  - [key]{c.name}[/key] (image: {c.image.tags[0] if c.image.tags else 'unknown'})")
                
                if questionary.confirm("Would you like to stop these containers to proceed?").ask():
                    with console.status("[info]Stopping conflicting containers...[/info]"):
                        stop_containers(conflicts)
                    console.print("[success]✓ Conflicting containers stopped successfully.[/success]\n")
                else:
                    console.print("[error]✗ Port conflict detected. Aborting.[/error]")
                    raise SystemExit(1)
        except Exception as exc:
            console.print(f"[warning]⚠ Could not check for port conflicts: {exc}[/warning]")

    console.print("[success]🚀 Starting Local Services (Docker Compose)...[/success]\n")

    # ── Interactive Credential Prompting ──
    _prompt_missing_env_vars(project_dir)

    # ── Venv and Requirements ──
    _ensure_venv(project_dir)

    # ── Pre-flight checks ──
    from nikame.cli.commands.preflight import (
        check_dockerfiles,
        check_env_vars,
        check_python_imports,
    )
    critical_checks = [check_python_imports, check_env_vars, check_dockerfiles]
    preflight_failed = False
    for check_fn in critical_checks:
        try:
            result = check_fn(project_dir)
            if not result.passed and result.severity == "P0":
                console.print(f"  [red]✗ {result.name}:[/red] {result.message}")
                if result.fix_hint:
                    console.print(f"    [dim]Fix: {result.fix_hint}[/dim]")
                preflight_failed = True
            elif not result.passed:
                console.print(f"  [yellow]⚠ {result.name}:[/yellow] {result.message}")
        except Exception:
            pass
    if preflight_failed:
        console.print("\n[bold red]✗ Pre-flight failed. Fix P0 issues first.[/bold red]")
        raise SystemExit(1)

    cmd_base = ["docker", "compose", "-p", config.name, "-f", str(compose_file)]
    env_file = project_dir / ".env.generated"
    local_env_file = project_dir / ".env"
    
    if env_file.exists(): cmd_base.extend(["--env-file", str(env_file)])
    if local_env_file.exists(): cmd_base.extend(["--env-file", str(local_env_file)])

    import asyncio

    from nikame.cli.commands.up_async import execute_up_async
    asyncio.run(execute_up_async(
        project_dir=project_dir,
        config=config,
        service=service,
        build=build,
        detach=detach,
        compose_file=compose_file,
        compose_data=compose_data,
        cmd_base=cmd_base,
        timeout=timeout
    ))
    
    # ── ML-Specific Service URLs
    _print_ml_urls(compose_data)
    
    # ── Display Ngrok Tunnel (if active)
    blueprint = build_blueprint(config)
    if "ngrok" in [m.NAME for m in blueprint.modules]:
        _display_ngrok_tunnel()

def _print_ml_urls(compose_data: dict) -> None:
    """Print ML-specific UI URLs if those services exist."""
    from rich.table import Table
    
    ml_urls = {
        "mlflow": ("MLflow Tracking UI", 5000),
        "langfuse": ("LangFuse Tracing UI", 3000),
        "prefect-server": ("Prefect Orchestration UI", 4200),
        "airflow-webserver": ("Airflow DAG UI", 8080),
        "evidently": ("Evidently AI Dashboard", 8000),
        "grafana": ("Grafana Dashboards", 3001),
    }
    
    services = compose_data.get("services", {})
    found = []
    for svc_name, (label, default_port) in ml_urls.items():
        if svc_name in services:
            # Try to extract actual port from compose spec
            ports = services[svc_name].get("ports", [])
            port = default_port
            if ports:
                port_str = str(ports[0]).split(":")[0]
                try:
                    port = int(port_str)
                except ValueError:
                    pass
            found.append((label, f"http://localhost:{port}"))
    
    if found:
        table = Table(title="ML & Observability Dashboards", show_header=True)
        table.add_column("Service", style="cyan")
        table.add_column("URL", style="bold green")
        for label, url in found:
            table.add_row(label, url)
        console.print("\n")
        console.print(table)


def _display_ngrok_tunnel() -> None:
    """Attempt to fetch and display the public Ngrok url from the local agent."""
    import requests
    
    with console.status("[info]Waiting for ngrok tunnel to establish...[/info]"):
        # Give the tunnel a few seconds to register with the ngrok cloud
        time.sleep(3)
        try:
            response = requests.get("http://localhost:4040/api/tunnels", timeout=2)
            if response.status_code == 200:
                tunnels = response.json().get("tunnels", [])
                for t in tunnels:
                    console.print(f"\\n[success]🌐 Public URL (ngrok):[/success] [bold cyan]{t.get('public_url')}[/bold cyan]")
        except Exception:
            pass

def _up_k8s(project_dir: Path) -> None:
    k8s_dir = project_dir / "infra" / "kubernetes"
    helm_dir = project_dir / "infra" / "helm"

    if helm_dir.exists():
        console.print("[success]🚀 Deploying via Helm...[/success]\n")
        _ensure_binary("helm", "Visit https://helm.sh/docs/intro/install/")
        subprocess.run(["helm", "upgrade", "--install", "app", "."], check=True, cwd=str(helm_dir))
    elif k8s_dir.exists():
        console.print("[success]🚀 Deploying via Kubectl...[/success]\n")
        _ensure_binary("kubectl", "Visit https://kubernetes.io/docs/tasks/tools/")
        subprocess.run(["kubectl", "apply", "-f", "."], check=True, cwd=str(k8s_dir))
    else:
        console.print("[error]✗ No K8s or Helm files found.[/error]")

def _up_cloud(project_dir: Path, target: str) -> None:
    tf_dir = project_dir / "infra" / "terraform"
    if not tf_dir.exists():
        console.print("[error]✗ Terraform files not found.[/error]")
        raise SystemExit(1)

    console.print(f"[success]🚀 Provisioning Cloud Infrastructure ({target})...[/success]\n")
    _ensure_binary("terraform", "Visit https://developer.hashicorp.com/terraform/downloads")
    subprocess.run(["terraform", "init"], check=True, cwd=str(tf_dir))
    subprocess.run(["terraform", "apply", "-auto-approve"], check=True, cwd=str(tf_dir))
