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_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_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 (bright, noticeable)
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

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 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"),
        ("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 == "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)
