"""``ilum access`` sub-app — port-forward UI and manage ingress."""

from __future__ import annotations

import os
import socket
import subprocess
import threading
import webbrowser

import typer
from kubernetes.client import NetworkingV1Api
from kubernetes.client.exceptions import ApiException as K8sApiException

import ilum.cli.output as output_mod
from ilum.cli.defaults import resolve_access_type, resolve_profile_defaults
from ilum.cli.output import IlumConsole
from ilum.config.manager import ConfigManager
from ilum.config.paths import IlumPaths
from ilum.constants import DEFAULT_CHART_REF, VALID_ACCESS_TYPES
from ilum.core.helm import HelmClient
from ilum.core.kubernetes import KubeClient
from ilum.core.modules import ModuleResolver
from ilum.core.release import ReleaseManager
from ilum.errors import ClusterConnectionError, IlumError
from ilum.wizard.deps import ensure_tools

access_app = typer.Typer(
    help="Access the Ilum UI via port-forward or ingress.", no_args_is_help=True
)
ingress_app = typer.Typer(help="Manage ingress configuration.", no_args_is_help=True)
access_app.add_typer(ingress_app, name="ingress", help="Manage ingress configuration.")


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _build_manager(
    context: str,
    namespace: str,
    timeout: str = "10m",
) -> ReleaseManager:
    paths = IlumPaths.default()
    paths.ensure_dirs()
    return ReleaseManager(
        helm=HelmClient(kubecontext=context, namespace=namespace, timeout=timeout),
        k8s=KubeClient(kubecontext=context),
        resolver=ModuleResolver(),
        config_mgr=ConfigManager(paths),
        paths=paths,
    )


def _is_port_in_use(port: int, address: str = "127.0.0.1") -> bool:
    """Return True if a TCP port is already bound."""
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        try:
            s.bind((address, port))
            return False
        except OSError:
            return True


# ---------------------------------------------------------------------------
# ilum access open — access type dispatchers
# ---------------------------------------------------------------------------


def _open_browser_async(url: str) -> None:
    """Open *url* in a browser on a background thread, suppressing stderr."""

    def _open() -> None:
        import time

        time.sleep(2)
        devnull_fd = os.open(os.devnull, os.O_WRONLY)
        saved_stderr = os.dup(2)
        os.dup2(devnull_fd, 2)
        os.close(devnull_fd)
        try:
            webbrowser.open(url)
        finally:
            os.dup2(saved_stderr, 2)
            os.close(saved_stderr)

    t = threading.Thread(target=_open, daemon=True)
    t.start()


def _open_via_nodeport(
    console: IlumConsole,
    k8s: KubeClient,
    svc_name: str,
    namespace: str,
    no_browser: bool,
) -> None:
    """Open the UI via NodePort — queries the service and node, opens browser."""
    node_port = k8s.get_service_node_port(svc_name, namespace)
    node_ip = k8s.get_node_address()
    url = f"http://{node_ip}:{node_port}"
    console.info(f"Ilum UI available at {url}")
    if not no_browser:
        _open_browser_async(url)
        import time

        time.sleep(3)


def _open_via_port_forward(
    console: IlumConsole,
    svc_name: str,
    namespace: str,
    context: str,
    port: int,
    address: str,
    no_browser: bool,
) -> None:
    """Open the UI via kubectl port-forward (blocking)."""
    ensure_tools(["kubectl"], console)

    if _is_port_in_use(port, address):
        console.error(f"Port {port} is already in use on {address}.")
        console.info(f"Try a different port: ilum access open --port {port + 1}")
        raise typer.Exit(code=1)

    cmd: list[str] = [
        "kubectl",
        "port-forward",
        f"--address={address}",
        f"--namespace={namespace}",
    ]
    if context:
        cmd.extend(["--context", context])
    cmd.extend([f"svc/{svc_name}", f"{port}:9777"])

    url = f"http://{address}:{port}"
    console.info(f"Forwarding {svc_name} to {url}")
    console.info("Press Ctrl+C to stop.")

    proc = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
    )

    if not no_browser:
        _open_browser_async(url)

    try:
        proc.wait()
    except KeyboardInterrupt:
        console.info("Stopping port-forward...")
        proc.terminate()
        try:
            proc.wait(timeout=5)
        except subprocess.TimeoutExpired:
            proc.kill()
        raise typer.Exit(0) from None

    if proc.returncode != 0:
        stderr_text = proc.stderr.read().decode().strip() if proc.stderr else ""
        if stderr_text:
            console.error(f"kubectl port-forward failed: {stderr_text}")
        else:
            console.error("kubectl port-forward failed with no error output.")
        raise typer.Exit(code=1)


def _open_via_ingress(
    console: IlumConsole,
    k8s: KubeClient,
    release: str,
    namespace: str,
    no_browser: bool,
) -> None:
    """Open the UI via its Ingress resource."""
    ingress_name = f"{release}-ui" if release != "ilum" else "ilum-ui"
    try:
        net_api = NetworkingV1Api(k8s._api_client)
        ingress = net_api.read_namespaced_ingress(name=ingress_name, namespace=namespace)
    except K8sApiException as exc:
        raise ClusterConnectionError(
            f"Ingress '{ingress_name}' not found in namespace '{namespace}'.",
            suggestion="Enable ingress first: ilum access ingress enable --host <hostname>",
            error_code="ILUM-010",
        ) from exc

    host = ""
    tls_hosts: set[str] = set()
    for tls_entry in ingress.spec.tls or []:
        for h in tls_entry.hosts or []:
            tls_hosts.add(h)
    for rule in ingress.spec.rules or []:
        if rule.host:
            host = rule.host
            break

    if not host:
        raise ClusterConnectionError(
            f"Ingress '{ingress_name}' has no host configured.",
            suggestion="Check ingress config: ilum access ingress show",
            error_code="ILUM-010",
        )

    scheme = "https" if host in tls_hosts else "http"
    url = f"{scheme}://{host}"
    console.info(f"Ilum UI available at {url}")
    if not no_browser:
        _open_browser_async(url)
        import time

        time.sleep(3)


@access_app.command("open")
def open_ui(
    release: str | None = typer.Option(
        None, "--release", "-r", show_default="from profile", help="Helm release name."
    ),
    namespace: str | None = typer.Option(
        None, "--namespace", "-n", show_default="from profile", help="Kubernetes namespace."
    ),
    context: str | None = typer.Option(
        None, "--context", show_default="from profile", help="Kubernetes context."
    ),
    access: str | None = typer.Option(
        None,
        "--access",
        "-a",
        show_default="from profile",
        help="Access method: nodeport, port-forward, or ingress.",
    ),
    port: int = typer.Option(9777, "--port", "-p", help="Local port to forward to."),
    address: str = typer.Option("127.0.0.1", "--address", help="Local address to bind to."),
    no_browser: bool = typer.Option(
        False, "--no-browser", help="Don't open a browser automatically."
    ),
) -> None:
    """Open the Ilum UI via nodeport, port-forward, or ingress."""
    release, namespace, context = resolve_profile_defaults(release, namespace, context)
    access_type = resolve_access_type(access)
    console = output_mod.console

    if access_type not in VALID_ACCESS_TYPES:
        console.error(
            f"Invalid access type '{access_type}'. Valid options: {', '.join(VALID_ACCESS_TYPES)}"
        )
        raise typer.Exit(code=1)

    try:
        svc_name = f"{release}-ui" if release != "ilum" else "ilum-ui"

        if access_type == "nodeport":
            k8s = KubeClient(kubecontext=context)
            _open_via_nodeport(console, k8s, svc_name, namespace, no_browser)
        elif access_type == "port-forward":
            _open_via_port_forward(console, svc_name, namespace, context, port, address, no_browser)
        elif access_type == "ingress":
            k8s = KubeClient(kubecontext=context)
            _open_via_ingress(console, k8s, release, namespace, no_browser)

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc


# ---------------------------------------------------------------------------
# ilum access ingress enable
# ---------------------------------------------------------------------------


@ingress_app.command("enable")
def ingress_enable(
    host: str = typer.Option(..., "--host", help="Ingress hostname."),
    tls: bool = typer.Option(False, "--tls", help="Enable TLS."),
    tls_secret: str = typer.Option("", "--tls-secret", help="TLS secret name."),
    ingress_class: str = typer.Option("", "--class", help="Ingress class name."),
    release: str | None = typer.Option(
        None, "--release", "-r", show_default="from profile", help="Helm release name."
    ),
    namespace: str | None = typer.Option(
        None, "--namespace", "-n", show_default="from profile", help="Kubernetes namespace."
    ),
    context: str | None = typer.Option(
        None, "--context", show_default="from profile", help="Kubernetes context."
    ),
    set_flags: list[str] | None = typer.Option(  # noqa: B008
        None, "--set", help="Additional Helm --set flag (repeatable)."
    ),
    chart: str = typer.Option(
        "", "--chart", help="Chart reference or local path (default: ilum/ilum)."
    ),
    devel: bool = typer.Option(False, "--devel", help="Include pre-release chart versions."),
    reset_defaults: bool = typer.Option(
        False,
        "--reset-defaults",
        help="Reset to new chart defaults before applying user values."
        " Use when upgrading across chart versions with breaking changes.",
    ),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview without executing."),
    timeout: str = typer.Option("10m", "--timeout", help="Helm timeout."),
) -> None:
    """Enable ingress for the Ilum UI."""
    release, namespace, context = resolve_profile_defaults(release, namespace, context)
    console = output_mod.console

    try:
        ensure_tools(["helm"], console, interactive=not yes)
        mgr = _build_manager(context, namespace, timeout)
        chart_ref = chart or DEFAULT_CHART_REF

        ingress_flags = [
            "ilum-ui.ingress.enabled=true",
            f"ilum-ui.ingress.host={host}",
        ]
        if ingress_class:
            ingress_flags.append(f"ilum-ui.ingress.className={ingress_class}")
        if tls:
            secret_name = tls_secret or f"{host}-tls"
            ingress_flags.extend(
                [
                    f"ilum-ui.ingress.tls[0].secretName={secret_name}",
                    f"ilum-ui.ingress.tls[0].hosts[0]={host}",
                ]
            )

        all_flags = ingress_flags + (set_flags or [])

        plan = mgr.plan_upgrade(
            release=release,
            chart=chart_ref,
            set_flags=all_flags,
            devel=devel,
            reset_defaults=reset_defaults,
        )

        summary_rows = [
            ["Action", "Enable Ingress"],
            ["Release", release],
            ["Namespace", namespace],
            ["Host", host],
        ]
        if ingress_class:
            summary_rows.append(["Ingress Class", ingress_class])
        if tls:
            summary_rows.append(["TLS", "Enabled"])
        console.operation_summary(summary_rows, title="Ingress Summary")

        if plan.drift and plan.drift.has_drift and plan.drift.diff:
            console.warning("External changes detected:")
            console.diff_table(plan.drift.diff, title="External Changes (Drift)")

        if plan.effective_diff and not plan.effective_diff.is_empty:
            console.diff_table(plan.effective_diff)
        else:
            console.info("No values changes needed.")
            return

        console.command_preview(mgr.preview_command(plan))

        if dry_run:
            console.info("Dry-run mode — no changes applied.")
            return

        if not yes and not console.confirm("Proceed?"):
            console.info("Aborted.")
            raise typer.Exit()

        from ilum.cli.progress import execute_with_progress

        execute_with_progress(mgr, plan, console, message="Enabling ingress...")
        console.success(f"Ingress enabled for {host}")

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc


# ---------------------------------------------------------------------------
# ilum access ingress disable
# ---------------------------------------------------------------------------


@ingress_app.command("disable")
def ingress_disable(
    release: str | None = typer.Option(
        None, "--release", "-r", show_default="from profile", help="Helm release name."
    ),
    namespace: str | None = typer.Option(
        None, "--namespace", "-n", show_default="from profile", help="Kubernetes namespace."
    ),
    context: str | None = typer.Option(
        None, "--context", show_default="from profile", help="Kubernetes context."
    ),
    set_flags: list[str] | None = typer.Option(  # noqa: B008
        None, "--set", help="Additional Helm --set flag (repeatable)."
    ),
    chart: str = typer.Option(
        "", "--chart", help="Chart reference or local path (default: ilum/ilum)."
    ),
    devel: bool = typer.Option(False, "--devel", help="Include pre-release chart versions."),
    reset_defaults: bool = typer.Option(
        False,
        "--reset-defaults",
        help="Reset to new chart defaults before applying user values."
        " Use when upgrading across chart versions with breaking changes.",
    ),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview without executing."),
    timeout: str = typer.Option("10m", "--timeout", help="Helm timeout."),
) -> None:
    """Disable ingress for the Ilum UI."""
    release, namespace, context = resolve_profile_defaults(release, namespace, context)
    console = output_mod.console

    try:
        ensure_tools(["helm"], console, interactive=not yes)
        mgr = _build_manager(context, namespace, timeout)
        chart_ref = chart or DEFAULT_CHART_REF

        all_flags = ["ilum-ui.ingress.enabled=false"] + (set_flags or [])

        plan = mgr.plan_upgrade(
            release=release,
            chart=chart_ref,
            set_flags=all_flags,
            devel=devel,
            reset_defaults=reset_defaults,
        )

        summary_rows = [
            ["Action", "Disable Ingress"],
            ["Release", release],
            ["Namespace", namespace],
        ]
        console.operation_summary(summary_rows, title="Ingress Summary")

        if plan.drift and plan.drift.has_drift and plan.drift.diff:
            console.warning("External changes detected:")
            console.diff_table(plan.drift.diff, title="External Changes (Drift)")

        if plan.effective_diff and not plan.effective_diff.is_empty:
            console.diff_table(plan.effective_diff)
        else:
            console.info("No values changes needed.")
            return

        console.command_preview(mgr.preview_command(plan))

        if dry_run:
            console.info("Dry-run mode — no changes applied.")
            return

        if not yes and not console.confirm("Proceed?"):
            console.info("Aborted.")
            raise typer.Exit()

        from ilum.cli.progress import execute_with_progress

        execute_with_progress(mgr, plan, console, message="Disabling ingress...")
        console.success("Ingress disabled.")

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc


# ---------------------------------------------------------------------------
# ilum access ingress show
# ---------------------------------------------------------------------------


@ingress_app.command("show")
def ingress_show(
    release: str | None = typer.Option(
        None, "--release", "-r", show_default="from profile", help="Helm release name."
    ),
    namespace: str | None = typer.Option(
        None, "--namespace", "-n", show_default="from profile", help="Kubernetes namespace."
    ),
    context: str | None = typer.Option(
        None, "--context", show_default="from profile", help="Kubernetes context."
    ),
    timeout: str = typer.Option("10m", "--timeout", help="Helm timeout."),
) -> None:
    """Show current ingress configuration."""
    release, namespace, context = resolve_profile_defaults(release, namespace, context)
    console = output_mod.console

    try:
        ensure_tools(["helm"], console)
        mgr = _build_manager(context, namespace, timeout)
        live = mgr.fetch_computed_values(release)

        ui_values = live.get("ilum-ui") or {}
        ingress = ui_values.get("ingress") or {}

        from ilum.cli.formatters import CommandResult, OutputFormat, ResultFormatter

        fmt = OutputFormat(console.output_format)

        if fmt != OutputFormat.TABLE:
            result = CommandResult(data=ingress, summary="Ingress config")
            ResultFormatter().format(result, fmt, console)
            return

        enabled = ingress.get("enabled", False)
        rows = [
            ["Enabled", str(enabled)],
            ["Host", str(ingress.get("host", ""))],
            ["Class", str(ingress.get("className", ""))],
            ["Path", str(ingress.get("path", ""))],
        ]
        tls = ingress.get("tls") or []
        if tls:
            rows.append(["TLS", "Enabled"])
            for i, entry in enumerate(tls):
                rows.append([f"TLS[{i}] Secret", str(entry.get("secretName", ""))])
                hosts = entry.get("hosts") or []
                rows.append([f"TLS[{i}] Hosts", ", ".join(str(h) for h in hosts)])
        else:
            rows.append(["TLS", "Disabled"])

        console.table(
            f"Ingress Config (release: {release})",
            ["Setting", "Value"],
            rows,
        )

    except IlumError as exc:
        console.handle_error(exc)
        raise typer.Exit(code=1) from exc
