import argparse
import logging
import os
import shutil
import subprocess
import sys
from pathlib import Path

import getpass
import urllib3
from dotenv import load_dotenv

from eda_integration.clients.eda.client import EDAClient
from eda_integration.clients.eda.saas_client import EDASaasClient
from eda_integration.services.eda_integrator import EDAIntegrator
from eda_integration.services.eda_exporter import run_export
from eda_integration.services.eda_remover import EDARemover
from eda_integration.utils.exceptions import (
    ClabIntegratorError,
    ConfigError,
    EDAConnectionError,
)
from eda_integration.utils.logging_config import setup_logging
from eda_integration.utils.constants import HTTP_OK
from xls_parser.xls_to_eda_yaml import run_convert_xls_to_eda
from stage_processing import (
    get_stage_topology_path,
    run_destroy_clab,
    run_inspect_clab,
    run_preprocess,
)
from config import get_all_config, get_cluster_model, get_edasaas_org_name, get_export_dir, get_log_dir, get_log_level, get_stage_dir, set_config_path
from state import get_state, format_state_box, get_action_prompt
from utils.boxes import format_box, format_sep

_ROOT = Path(__file__).resolve().parent
if str(_ROOT) not in sys.path:
    sys.path.insert(1, str(_ROOT))
urllib3.disable_warnings()
logger = logging.getLogger(__name__)

SEP = format_sep()

# ANSI colors for step labels and export output
ANSI_RESET = "\033[0m"
ANSI_STEP1 = "\033[1;36m"   # bold cyan
ANSI_STEP2 = "\033[1;33m"   # bold yellow
ANSI_STEP3 = "\033[1;32m"   # bold green
ANSI_SUCCESS = "\033[1;32m"  # bold green
ANSI_ERROR = "\033[1;31m"   # bold red

CONFIG_FILENAME = "config.yaml"
ENV_FILENAME = ".env"


def _parse_args():
    """Parse CLI flags for config, env, and optional xls paths."""
    p = argparse.ArgumentParser(
        description="clab-integrator: parse Excel topology, preprocess, and upload to EDA.",
    )
    p.add_argument(
        "-c", "--config",
        type=Path,
        default=None,
        metavar="PATH",
        help=f"Path to config.yaml (required; default: {CONFIG_FILENAME} in current directory)",
    )
    p.add_argument(
        "-e", "--env",
        type=Path,
        default=None,
        metavar="PATH",
        help=f"Path to credentials file (required; default: {ENV_FILENAME} in current directory)",
    )
    p.add_argument(
        "-x", "--xls",
        type=Path,
        default=None,
        metavar="PATH",
        help="Path to Excel topology file (optional; if omitted, prompted at start/parse)",
    )
    return p.parse_args()


def _resolve_config_and_env(args) -> tuple[Path, Path]:
    """Resolve config.yaml and .env paths from -c/-e or cwd. Both files are required (no prompting)."""
    cwd = Path.cwd()
    default_config = cwd / CONFIG_FILENAME
    default_env = cwd / ENV_FILENAME

    config_path = Path(args.config).resolve() if args.config is not None else default_config
    if not config_path.is_file():
        raise SystemExit(
            f"Config file not found: {config_path}\n"
            f"Create config.yaml in the current directory or pass -c /path/to/config.yaml"
        )

    env_path = Path(args.env).resolve() if args.env is not None else default_env
    if not env_path.is_file():
        raise SystemExit(
            f"Credentials file not found: {env_path}\n"
            f"Create .env in the current directory or pass -e /path/to/.env"
        )

    return config_path, env_path

def _norm(x: str | None) -> str | None:
    """Return stripped non-empty string or None. Used so empty env/entry means 'don't use that flow'."""
    if x is None:
        return None
    s = (x or "").strip()
    return s if s else None


def create_eda_client(**kwargs) -> EDAClient:
    """Create EDA client from common parameters. Only pass kc_secret / kc_user / kc_password when non-empty."""
    kc_secret = _norm(kwargs.get("kc_secret"))
    kc_user = _norm(kwargs.get("kc_user") or os.environ.get("KC_USER")) or "admin"
    kc_password = _norm(kwargs.get("kc_password") or os.environ.get("KC_PASSWORD")) or "admin"
    return EDAClient(
        hostname=kwargs["eda_url"],
        eda_user=kwargs.get("eda_user", "admin"),
        eda_password=kwargs.get("eda_password", "admin"),
        kc_secret=kc_secret,
        kc_user=kc_user,
        kc_password=kc_password,
        verify=kwargs.get("verify", False),
    )

def prompt(prompt_text: str, default: str | None = None, hidden: bool = False) -> str:
    if hidden:
        if default:
            return getpass.getpass(f"{prompt_text} [{default}]: ") or default
        return getpass.getpass(f"{prompt_text}: ")
    if default:
        return input(f"{prompt_text} [{default}]: ") or default
    return input(f"{prompt_text}: ")

def inspect(stage_dir: Path) -> None:
    """Run containerlab inspect for the stage topology, then prompt for an IP to ping."""
    run_inspect_clab(stage_dir)
    ip = prompt("EDA IP to ping (e.g. bsvr ip, toolbox ip)").strip()
    if not ip:
        print("No IP given, skipping ping.")
        return
    print("Pinging for 5 seconds, then exiting...")
    subprocess.run(["ping", "-w", "5", ip])

def run_destroy(eda_client: EDAClient, namespace: str, stage_dir: Path) -> None:
    """Destroy containerlab, clear stage, then remove namespace from EDA."""
    # Remove namespace from EDA
    try:
        eda_client.login()
        remover = EDARemover(eda_client)
        remover.run(namespace=namespace)
        print("Namespace removed from EDA.")
    except Exception as e:
        print(f"Error removing namespace from EDA: {e}")

    if not run_destroy_clab(stage_dir):
        print("Aborting stage clear (containerlab destroy did not succeed). Please clears containerlab manually.")
        return
    # Clear stage
    if stage_dir.exists():
        for child in stage_dir.iterdir():
            shutil.rmtree(child) if child.is_dir() else child.unlink()
        print("Stage cleared.")
    else:
        print("Stage directory not found; creating new one.")
        stage_dir.mkdir(parents=True, exist_ok=True)
        print("Stage directory created.")

def _prompt_select_organization(namespaces: dict[str, str]) -> str:
    """Let user select an organization by number or name; return the chosen org id."""
    names_list = list(namespaces.keys())
    if not names_list:
        raise ValueError("No organizations available to select")
    for i, name in enumerate(names_list, 1):
        print(f"  {i}. {name}")
    choice = prompt("Select organization (enter number or name)").strip()
    if not choice:
        raise ValueError("No organization selected")
    if choice.isdigit():
        idx = int(choice)
        if 1 <= idx <= len(names_list):
            selected_name = names_list[idx - 1]
        else:
            raise ValueError(f"Invalid number: {choice}. Choose 1–{len(names_list)}.")
    else:
        selected_name = choice
    if selected_name not in namespaces:
        raise ValueError(f"Unknown organization: {selected_name}")
    return namespaces[selected_name]


def run_create_resources(integrator: EDAIntegrator, namespace: str, stage_dir: Path):
    try:
        is_dedicated = integrator.cluster_model.strip().lower() == "dedicated"
        if not is_dedicated:
            print(
                "WARNING: Superadmin credentials are required to create mgmt resources for shared/MSP."
            )
            namespaces = integrator.get_namespaces()
            org_id = _prompt_select_organization(namespaces)
            subnet = prompt("Subnet for MSP (e.g. 10.150.150.0/24)", "10.150.150.0/24").strip()
            if subnet:
                integrator.run_create_shared_mgmt_resources(org_id, subnet)
            else:
                print("Skipping MSP subnet creation...")
        integrator.run(namespace=namespace, stage_dir=stage_dir)
        logger.info("Resource creation finished")
    except (ConfigError, ValueError) as e:
        logger.error("Config error: %s", e)
        print(f"Config error: {e}")
        raise
    except EDAConnectionError as e:
        logger.error("EDA connection error: %s", e)
        print(f"EDA connection error: {e}")
        raise
    except ClabIntegratorError as e:
        logger.error("EDA Integrator error: %s", e)
        print(f"EDA Integrator error: {e}")
        raise
    except Exception as e:
        logger.error("Error running EDA Integrator: %s", e)
        print(f"Error: {e}")
        raise

# Auth type labels for logging (one of three possible authentication kinds)
AUTH_TYPE_EDA_SAAS = "EDA SaaS"
AUTH_TYPE_KC_SECRET = "Keycloak with client secret"
AUTH_TYPE_KC_ADMIN = "Keycloak admin flow"


def enter_credentials() -> dict | None:
    """
    Load credentials from env only. Raise ConfigError if required creds are missing.
    Cluster type is inferred: if EDA_SAAS_ORG_NAME is set, the script treats the cluster as
    shared/MSP and uses EDA SaaS auth; if not set, the script assumes dedicated and uses
    Keycloak (KC_SECRET or KC_USER+KC_PASSWORD).
    """
    creds: dict = {}
    creds["eda_url"] = _norm(os.environ.get("EDA_URL"))
    creds["eda_user"] = _norm(os.environ.get("EDA_USER"))
    creds["eda_password"] = _norm(os.environ.get("EDA_PASSWORD"))
    creds["namespace"] = _norm(os.environ.get("DEFAULT_NAMESPACE"))
    creds["kc_secret"] = _norm(os.environ.get("KC_SECRET"))
    creds["kc_user"] = _norm(os.environ.get("KC_USER"))
    creds["kc_password"] = _norm(os.environ.get("KC_PASSWORD"))
    creds["stage_dir"] = get_stage_dir()
    edasaas_org_name = _norm(get_edasaas_org_name())
    if edasaas_org_name:
        creds["edasaas_org_name"] = edasaas_org_name

    # Required credentials: must be set in .env
    missing = []
    if not creds["eda_url"]:
        missing.append("EDA_URL")
    if not creds["eda_user"]:
        missing.append("EDA_USER")
    if not creds["eda_password"]:
        missing.append("EDA_PASSWORD")
    if not creds["namespace"]:
        missing.append("DEFAULT_NAMESPACE")
    if missing:
        raise ConfigError(
            f"Missing required credential(s): {', '.join(missing)}. Set them in .env or environment."
        )

    # At least one auth method: EDA SaaS (org name set), or Keycloak client secret, or Keycloak admin
    if edasaas_org_name:
        pass  # will use EDA SaaS (cluster inferred as shared/MSP)
    elif creds["kc_secret"]:
        pass  # will use Keycloak with client secret (cluster inferred as dedicated)
    elif creds["kc_user"] and creds["kc_password"]:
        pass  # will use Keycloak admin flow (cluster inferred as dedicated)
    else:
        raise ConfigError(
            "No authentication method available: set EDA_SAAS_ORG_NAME (shared/MSP), "
            "or KC_SECRET, or KC_USER and KC_PASSWORD in .env."
        )

    if edasaas_org_name:
        try:
            EDASaasClient(
                base_url=creds["eda_url"],
                username=creds["eda_user"],
                password=creds["eda_password"],
                org_name=edasaas_org_name,
                verify=True,
            ).login()
        except Exception as e:
            logger.error("EDA SaaS login failed: %s", e)
            return None
        logger.info("Authentication finished: %s", AUTH_TYPE_EDA_SAAS)
        return creds
    if creds["kc_secret"]:
        eda_client = create_eda_client(
            eda_url=creds["eda_url"],
            eda_user=creds["eda_user"],
            eda_password=creds["eda_password"],
            kc_secret=creds["kc_secret"],
            kc_user=creds.get("kc_user"),
            kc_password=creds.get("kc_password"),
            verify=True,
        )
        eda_client.login()
        resp = eda_client.get("apps/core.eda.nokia.com")
        if resp.status != HTTP_OK:
            logger.error("Failed to connect to EDA: %s %s", resp.status, resp.data.decode())
            return None
        logger.info("Authentication finished: %s", AUTH_TYPE_KC_SECRET)
        return creds
    if creds["kc_user"] and creds["kc_password"]:
        eda_client = create_eda_client(
            eda_url=creds["eda_url"],
            eda_user=creds["eda_user"],
            eda_password=creds["eda_password"],
            kc_secret=None,
            kc_user=creds["kc_user"],
            kc_password=creds["kc_password"],
            verify=True,
        )
        eda_client.login()
        resp = eda_client.get("apps/core.eda.nokia.com")
        if resp.status != HTTP_OK:
            logger.error("Failed to connect to EDA: %s %s", resp.status, resp.data.decode())
            return None
        logger.info("Authentication finished: %s", AUTH_TYPE_KC_ADMIN)
        return creds
    return None

def _censor(key: str, value: str | None) -> str:
    """Return value or censored placeholder for password/secret-like keys."""
    if value is None or (isinstance(value, str) and not value.strip()):
        return "N/A"
    s = str(value).strip()
    if not s:
        return "N/A"
    lower = key.lower()
    if "password" in lower or "secret" in lower:
        return "*" * 8
    return s


def print_settings(creds: dict, config_path: Path, env_path: Path) -> None:
    """Print config and credentials in one box; censor passwords and secrets."""
    lines = ["Settings (config + credentials)", ""]
    lines.append(f"config: {config_path}")
    lines.append(f"env: {env_path}")
    lines.append("")
    cfg = get_all_config()
    for k, v in sorted(cfg.items()):
        lines.append(f"  {k}: {v}")
    lines.append("")
    lines.append("Credentials (from .env):")
    for key in ("eda_url", "eda_user", "eda_password", "namespace", "kc_secret", "kc_user", "kc_password", "edasaas_org_name", "stage_dir"):
        val = creds.get(key)
        lines.append(f"  {key}: {_censor(key, val)}")
    print("\n" + format_box(lines))

def run_start(
    creds: dict,
    stage_dir: Path,
    eda_client: EDAClient,
    integrator: EDAIntegrator,
    xls_path: str | Path | None = None,
) -> None:
    """Run full pipeline: parse → preprocess → upload. Prints intro and runs steps in order.
    If xls_path is not provided, prompts for the Excel topology file path."""
    print(SEP)
    print(
        "Run 'debug print-state' in the main menu to debug issues with script flow."
    )
    print()
    # 1. parse
    if not xls_path:
        xls_path = (prompt("Path to Excel topology file (required)")).strip()
    else:
        xls_path = str(xls_path)
    if not xls_path:
        print("No path given, skipping parse.")
        return
    xls_file = Path(xls_path).resolve()
    if not xls_file.is_file():
        logger.error("Input file not found: %s. Quitting without creating EDA resources.", xls_file)
        print(f"Input file not found: {xls_file}. Quitting without creating EDA resources.")
        return

    print()
    print(SEP)
    print(f"{ANSI_STEP1}[STEP 1]{ANSI_RESET} Parsing Excel topology file and outputting to {stage_dir}...")
    print(SEP)
    parse_result = run_convert_xls_to_eda(
        xls_path,
        creds.get("namespace"),
        output_dir=stage_dir,
    )
    if parse_result != 0:
        print("Parse step failed; not proceeding to preprocess or upload.")
        return
    print(SEP)
    print(f"{ANSI_STEP2}[STEP 2]{ANSI_RESET} Deploying Clab topology, updating toponode addresses, and cloning index pool data to {stage_dir}...")
    print(SEP)
    # 2. preprocess
    run_preprocess(stage_dir)
    print(SEP)
    print(f"{ANSI_STEP3}[STEP 3]{ANSI_RESET} Generating EDA resources from {stage_dir}...")
    print(SEP)
    # 3. upload
    run_create_resources(integrator, creds.get("namespace"), stage_dir)


def print_state_help(stage_dir: Path, eda_client: EDAClient, namespace: str) -> None:
    """Show state box plus explanation of start steps and that they can be run individually."""
    state = get_state(stage_dir, eda_client, namespace)
    print(SEP)
    print(format_state_box(state, stage_dir, namespace))
    print(
        "Start runs these steps in order: run-parse (Excel → YAML), run-preprocess (clab + toponode + index pools), run-upload (EDA resources). They can be run individually: "
    )
    print()
    print(SEP)



def run_export_command(namespace: str, export_dir: str) -> None:
    """Export EDA CRs from the cluster to YAML via kubectl. Prints each result at runtime with colored output."""

    def on_result(kind: str, message: str, path: Path | None) -> None:
        if kind == "ok":
            print(f"{ANSI_SUCCESS}{message}{ANSI_RESET}")
        else:
            print(f"{ANSI_ERROR}{message}{ANSI_RESET}")

    print(SEP)
    print(
        "This command requires kubectl credentials (kubeconfig or in-cluster config). "
        "No EDA URL or Keycloak credentials are used for export."
    )
    print(SEP)
    result = run_export(
        namespace=namespace,
        out_dir=export_dir,
        set_namespace=namespace,
        on_result=on_result,
    )
    if result["written"]:
        print(f"\nExported {len(result['written'])} file(s) to {Path(export_dir).resolve()}")
    if result["errors"] and not result["written"]:
        print("No resources were exported.")


def print_help():
    commands = [
        ("help", "Show this list of commands"),
        ("debug", "Show debug subcommands (print-state, print-settings, set-log-level, run-parse, run-preprocess, run-upload)"),
        ("start", "Parse XLS, preprocess (clab + toponode + index pools), and upload to EDA"),
        ("export", "Export EDA CRs from cluster to YAML (requires kubectl credentials)"),
        ("inspect", "Inspect Clab topology, then ping EDA for connectivity"),
        ("reset", "Remove namespace from EDA, destroy Clab topology, and clear stage"),
        ("exit", "Exit the program"),
    ]
    lines = ["Available commands:", ""] + [f"{comm}: {desc}" for comm, desc in commands]
    print("\n" + format_box(lines))


def run_set_log_level() -> None:
    """Prompt for a log level and set root logger and handlers to it."""
    root = logging.getLogger()
    level_to_name = {logging.DEBUG: "DEBUG", logging.INFO: "INFO", logging.WARNING: "WARNING", logging.ERROR: "ERROR"}
    current = level_to_name.get(root.level, logging.getLevelName(root.level))
    choice = prompt("Log level (DEBUG, INFO, WARNING, ERROR)", current or "INFO").strip().upper()
    if not choice:
        choice = "INFO"
    level_map = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARNING": logging.WARNING, "ERROR": logging.ERROR}
    if choice not in level_map:
        print(f"Invalid level '{choice}'. Use one of: DEBUG, INFO, WARNING, ERROR.")
        return
    level = level_map[choice]
    root.setLevel(level)
    for h in root.handlers:
        h.setLevel(level)
    if choice == "INFO":
        logging.getLogger("eda_integration.clients").setLevel(logging.WARNING)
    else:
        logging.getLogger("eda_integration.clients").setLevel(level)
    logger.info("Log level set to %s", choice)


def print_debug():
    """Show debug menu: subcommands that can be run as 'debug <subcommand>'."""
    commands = [
        ("debug print-settings", "Print config and credentials (passwords/secrets censored)"),
        ("debug print-state", "Show current state of script and where it left off"),
        ("debug set-log-level", "Set log level at runtime (DEBUG, INFO, WARNING, ERROR)"),
        ("debug run-parse", "Run Step 1 only: parse Excel topology to YAML"),
        ("debug run-preprocess", "Run Step 2 only: deploy Clab, toponode addresses, index pools"),
        ("debug run-upload", "Run Step 3 only: push resources to EDA"),
    ]
    lines = ["Debug commands (run as 'debug <subcommand>'):", ""] + [f"  {comm}: {desc}" for comm, desc in commands]
    print("\n" + format_box(lines))


def main():
    args = _parse_args()
    config_path, env_path = _resolve_config_and_env(args)
    set_config_path(config_path)
    load_dotenv(env_path)

    log_level = get_log_level()
    log_dir = get_log_dir()
    setup_logging(log_level, log_dir)
    logger.info(f"LOG_LEVEL: {log_level}")
    logger.debug(f"LOG_FILE: {log_dir}")

    # Optional xls path from --xls flag; if None, prompted at start/parse
    cli_xls_path: Path | None = Path(args.xls).resolve() if args.xls else None
    if cli_xls_path is not None and not cli_xls_path.is_file():
        logger.warning("XLS file from --xls not found: %s; will prompt at start/parse", cli_xls_path)
        cli_xls_path = None

    logger.info("Starting EDA integrator CLI")

    try:
        creds = enter_credentials()
    except ConfigError as e:
        logger.error("Config error: %s", e)
        raise SystemExit(f"Config error: {e}") from e
    while creds is None:
        logger.error("Invalid credentials, please try again")
        try:
            creds = enter_credentials()
        except ConfigError as e:
            logger.error("Config error: %s", e)
            raise SystemExit(f"Config error: {e}") from e
    stage_dir = Path(creds["stage_dir"]).resolve()

    print("\nWelcome to the clab-integrator CLI!")
    print_settings(creds, config_path, env_path)

    # Cluster type inferred from env: EDA_SAAS_ORG_NAME set → shared/MSP (SaaS auth); not set → dedicated (Keycloak).
    edasaas_org_name = _norm(creds.get("edasaas_org_name") or get_edasaas_org_name())
    kc_secret = _norm(creds.get("kc_secret") or os.environ.get("KC_SECRET"))
    kc_user = _norm(creds.get("kc_user") or os.environ.get("KC_USER"))
    kc_password = _norm(creds.get("kc_password") or os.environ.get("KC_PASSWORD"))

    eda_client = create_eda_client(
        eda_url=creds["eda_url"],
        eda_user=creds["eda_user"],
        eda_password=creds["eda_password"],
        kc_secret=kc_secret,
        kc_user=creds.get("kc_user"),
        kc_password=creds.get("kc_password"),
        verify=True,
    )
    namespace = creds["namespace"]
    cluster_model = get_cluster_model()
    eda_saas_client = None

    if edasaas_org_name:
        eda_saas_client = EDASaasClient(
            base_url=creds["eda_url"],
            username=creds["eda_user"],
            password=creds["eda_password"],
            org_name=edasaas_org_name,
            verify=True,
        )
        eda_saas_client.login()
    elif kc_secret:
        eda_client.login()
    elif kc_user and kc_password:
        eda_client.login()

    eda_integrator = EDAIntegrator(eda_client, cluster_model, eda_saas_client)

    while True:
        state = get_state(stage_dir, eda_client, namespace)
        print(f"\nType {get_action_prompt(state)} (type 'help' or 'debug' for more options, 'exit' to quit)")
        cmd = prompt("clab-integrator").strip().lower()
        if cmd in ("exit", "quit"):
            print("Goodbye")
            break
        if cmd == "deploy" or cmd == "start":
            run_start(creds, stage_dir, eda_client, eda_integrator, xls_path=cli_xls_path)
        elif cmd == "help":
            print_help()
        elif cmd == "debug":
            print_debug()
        elif cmd.startswith("debug "):
            sub = cmd[6:].strip()
            if sub == "print-settings":
                print_settings(creds, config_path, env_path)
            elif sub == "print-state":
                print_state_help(stage_dir, eda_client, namespace)
            elif sub == "set-log-level":
                run_set_log_level()
            elif sub == "run-parse":
                xls_path = (
                    (str(cli_xls_path) if cli_xls_path is not None else None)
                    or os.environ.get("XLS_PATH")
                    or prompt("Path to Excel topology file")
                ).strip()
                if not xls_path:
                    print("No path given, skipping.")
                    continue
                xls_file = Path(xls_path).resolve()
                if not xls_file.is_file():
                    logger.error("Input file not found: %s", xls_file)
                    print(f"Input file not found: {xls_file}")
                    continue
                run_convert_xls_to_eda(
                    xls_path,
                    creds.get("namespace"),
                    output_dir=stage_dir,
                )
            elif sub == "run-preprocess":
                run_preprocess(stage_dir)
            elif sub == "run-upload":
                run_create_resources(eda_integrator, namespace, stage_dir)
            else:
                print("Unknown debug subcommand. Type 'debug' to see available debug commands.")
        elif cmd == "export":
            export_ns = prompt("Namespace to export", default=namespace).strip()
            if not export_ns:
                print("No namespace given, skipping export.")
            else:
                run_export_command(export_ns, get_export_dir())
        elif cmd == "inspect":
            inspect(stage_dir)
        elif cmd == "destroy" or cmd == "reset":
            run_destroy(eda_client, namespace, stage_dir)
        else:
            print("Unknown command.")


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print("\nInterrupted, exiting")
        sys.exit(0)
