"""Services CLI commands — expose, ports, logs, exec."""

from __future__ import annotations

import subprocess
import threading
import webbrowser

import click

from kuberoku.cli.context import resolve_app
from kuberoku.cli.output import info, render_detail, success
from kuberoku.models import LogLine


class _ColonGroup(click.Group):
    """Resolve colon-separated subcommands (expose:on, ports:add, etc.)."""

    def resolve_command(
        self,
        ctx: click.Context,
        args: list[str],
    ) -> tuple[str | None, click.Command | None, list[str]]:
        cmd_name = args[0] if args else None
        if cmd_name and ":" in cmd_name:
            parts = cmd_name.split(":", 1)
            args = [parts[0], parts[1]] + args[1:]
        return super().resolve_command(ctx, args)


@click.group("services", cls=_ColonGroup, invoke_without_command=True)
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def services(ctx: click.Context, app_flag: str | None) -> None:
    """Manage process type Services (expose, ports)."""
    ctx.ensure_object(dict)
    ctx.obj["_services_app"] = app_flag

    if ctx.invoked_subcommand is not None:
        return

    # Default: list ports
    app_name = resolve_app(app_flag)
    factory = ctx.obj["factory"]
    port_map = factory.services.list_ports(app_name)

    if not port_map:
        info("No ports declared.")
        return

    for ptype in sorted(port_map):
        click.echo(f"=== {ptype}")
        port_strs = [f"{pm.port}/{pm.protocol.lower()}" for pm in port_map[ptype]]
        click.echo("   ".join(port_strs))


# ── expose subgroup ──────────────────────────────────────────────


@services.group("expose", cls=_ColonGroup, invoke_without_command=True)
@click.pass_context
def expose(ctx: click.Context) -> None:
    """Expose or unexpose a process type."""
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())


@expose.command("on")
@click.argument("name")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.option(
    "--method",
    type=click.Choice(["gateway", "loadbalancer", "nodeport"]),
    default=None,
    help="Expose method. If omitted, prompts interactively.",
)
@click.option("--port", type=int, default=None, help="Override port.")
@click.option("--wait/--no-wait", default=True, help="Wait for external IP.")
@click.pass_context
def expose_on(
    ctx: click.Context,
    name: str,
    app_flag: str | None,
    method: str | None,
    port: int | None,
    wait: bool,
) -> None:
    """Expose a process type externally."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    if method is None:
        use_random = click.confirm(
            "Can you connect using a random port? (recommended)",
            default=True,
        )
        method = "gateway" if use_random else "loadbalancer"

    endpoint = factory.services.expose_on(
        app_name,
        name,
        method=method,
        port=port,
        wait=wait,
        on_step=lambda msg: info(msg),
    )

    if endpoint.gateway_port is not None:
        success(f"Exposed process '{name}' via gateway port {endpoint.gateway_port}.")
    else:
        success(f"Exposed process '{name}' via {endpoint.service_type}.")

    fields: dict[str, str] = {
        "Name": endpoint.name,
        "Kind": endpoint.kind,
        "Method": endpoint.method,
    }
    if endpoint.gateway_port is not None:
        fields["Gateway Port"] = str(endpoint.gateway_port)
    if endpoint.external_ip:
        fields["External IP"] = endpoint.external_ip
    if endpoint.hostname:
        fields["Hostname"] = endpoint.hostname
    if endpoint.ports:
        fields["Ports"] = ", ".join(str(p) for p in endpoint.ports)
    render_detail(f"{app_name} expose", fields)


@expose.command("off")
@click.argument("name")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def expose_off(
    ctx: click.Context,
    name: str,
    app_flag: str | None,
) -> None:
    """Revert an exposed process to internal-only."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    endpoint = factory.services.expose_off(
        app_name,
        name,
        on_step=lambda msg: info(msg),
    )

    success(f"Unexposed process '{name}' (now ClusterIP).")
    fields: dict[str, str] = {
        "Name": endpoint.name,
        "Kind": endpoint.kind,
        "Type": endpoint.service_type,
    }
    if endpoint.ports:
        fields["Ports"] = ", ".join(str(p) for p in endpoint.ports)
    render_detail(f"{app_name} unexpose", fields)


# ── open ─────────────────────────────────────────────────────────


@services.command("open")
@click.argument("name", required=False, default="web")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def open_cmd(
    ctx: click.Context,
    name: str,
    app_flag: str | None,
) -> None:
    """Open HTTP process in browser."""

    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    url = factory.services.open_url(app_name, name)
    info(f"Opening {url}...")
    webbrowser.open(url)


# ── connect ──────────────────────────────────────────────────────


@services.command("connect")
@click.argument("name")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.option("--port", "local_port", type=int, default=None, help="Local port override.")
@click.pass_context
def connect(
    ctx: click.Context,
    name: str,
    app_flag: str | None,
    local_port: int | None,
) -> None:
    """Open a local port-forward to a process type."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    target = factory.services.resolve_connect(app_name, name)

    # Build kubectl port-forward command
    fwd_args = ["kubectl", "port-forward", "-n", target.namespace, f"svc/{target.service_name}"]
    for pm in target.ports:
        lp = local_port if local_port is not None else pm.port
        fwd_args.append(f"{lp}:{pm.port}")
        info(f"Forwarding localhost:{lp} -> {name}:{pm.port}/{pm.protocol.lower()}")
        # Only use the explicit local_port for the first port
        local_port = None

    info(f"Connecting to process '{name}' on app '{app_name}'...")
    info("Press Ctrl+C to stop.")

    proc = subprocess.Popen(fwd_args)
    try:
        proc.wait()
    except KeyboardInterrupt:
        proc.terminate()
        proc.wait()
        info("Disconnected.")


# ── ports subgroup ───────────────────────────────────────────────


@services.group("ports", cls=_ColonGroup, invoke_without_command=True)
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def ports(ctx: click.Context, app_flag: str | None) -> None:
    """List or manage ports for process types."""
    ctx.ensure_object(dict)
    ctx.obj["_ports_app"] = app_flag

    if ctx.invoked_subcommand is not None:
        return

    # Default: list ports
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]
    port_map = factory.services.list_ports(app_name)

    if not port_map:
        info("No ports declared.")
        return

    for ptype in sorted(port_map):
        click.echo(f"=== {ptype}")
        port_strs = [f"{pm.port}/{pm.protocol.lower()}" for pm in port_map[ptype]]
        click.echo("   ".join(port_strs))


@ports.command("add")
@click.argument("port_spec")
@click.option("--type", "process_type", default=None, help="Process type to add port to.")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def ports_add(
    ctx: click.Context,
    port_spec: str,
    process_type: str | None,
    app_flag: str | None,
) -> None:
    """Add a port to a process type."""
    parent_app = ctx.obj.get("_services_app")
    app_flag = app_flag or ctx.obj.get("_ports_app") or parent_app
    app_name = resolve_app(app_flag)
    factory = ctx.obj["factory"]

    factory.services.add_port(
        app_name,
        port_spec,
        process_type=process_type,
        on_step=lambda msg: info(msg),
    )
    success(f"Added {port_spec}. Rolling restart initiated.")


@ports.command("remove")
@click.argument("port_spec")
@click.option("--type", "process_type", default=None, help="Process type to remove port from.")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def ports_remove(
    ctx: click.Context,
    port_spec: str,
    process_type: str | None,
    app_flag: str | None,
) -> None:
    """Remove a port from a process type."""
    parent_app = ctx.obj.get("_services_app")
    app_flag = app_flag or ctx.obj.get("_ports_app") or parent_app
    app_name = resolve_app(app_flag)
    factory = ctx.obj["factory"]

    factory.services.remove_port(
        app_name,
        port_spec,
        process_type=process_type,
        on_step=lambda msg: info(msg),
    )
    success(f"Removed {port_spec}. Rolling restart initiated.")


# ── logs ──────────────────────────────────────────────────────────


@services.command("logs")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.option("--tail", "-t", "follow", is_flag=True, default=False, help="Stream in real time.")
@click.option("--num", "-n", "num", type=int, default=100, help="Lines to show (default: 100).")
@click.option("--type", "process_type", default=None, help="Filter by process type.")
@click.option("--dyno", default=None, help="Filter by dyno (e.g., web.1).")
@click.option("--since", default=None, help="Duration filter (5m, 1h, 30s).")
@click.option("--timestamps", is_flag=True, default=False, help="Prefix with K8s timestamps.")
@click.option("--previous", is_flag=True, default=False, help="Show crashed container logs.")
@click.pass_context
def logs(
    ctx: click.Context,
    app_flag: str | None,
    follow: bool,
    num: int,
    process_type: str | None,
    dyno: str | None,
    since: str | None,
    timestamps: bool,
    previous: bool,
) -> None:
    """View recent process logs."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    if follow:
        stop = threading.Event()

        def on_line(line: LogLine) -> None:
            ts_prefix = f"{line.timestamp.isoformat()} " if timestamps else ""
            click.echo(f"{ts_prefix}{line.dyno} | {line.message}")

        info(f"Streaming logs for app '{app_name}'... (Ctrl+C to stop)")
        try:
            factory.services.stream_logs(
                app_name,
                process_type=process_type,
                dyno=dyno,
                num=num,
                since=since,
                previous=previous,
                on_line=on_line,
                stop_event=stop,
            )
        except KeyboardInterrupt:
            stop.set()
    else:
        log_lines = factory.services.get_logs(
            app_name,
            process_type=process_type,
            dyno=dyno,
            num=num,
            since=since,
            timestamps=timestamps,
            previous=previous,
        )
        for line in log_lines:
            ts_prefix = f"{line.timestamp.isoformat()} " if timestamps else ""
            click.echo(f"{ts_prefix}{line.dyno} | {line.message}")


# ── exec ──────────────────────────────────────────────────────────


@services.command("exec")
@click.argument("command_parts", nargs=-1, required=True)
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.option("--type", "process_type", default=None, help="Process type (default: web).")
@click.option("--no-tty", is_flag=True, default=False, help="Don't allocate TTY.")
@click.option("--detach", is_flag=True, default=False, help="Run as background Job.")
@click.option("--env", "env_vars", multiple=True, help="Extra env vars (KEY=VAL, repeatable).")
@click.option("--timeout", "timeout", type=int, default=3600, help="Max seconds (default: 3600).")
@click.option("--dyno", default=None, help="Specific dyno (e.g., web.1).")
@click.pass_context
def exec_cmd(
    ctx: click.Context,
    command_parts: tuple[str, ...],
    app_flag: str | None,
    process_type: str | None,
    no_tty: bool,
    detach: bool,
    env_vars: tuple[str, ...],
    timeout: int,
    dyno: str | None,
) -> None:
    """Exec into a process pod."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]

    command = list(command_parts)

    if detach:
        extra_env: dict[str, str] | None = None
        if env_vars:
            extra_env = {}
            for ev in env_vars:
                if "=" not in ev:
                    raise click.BadParameter(f"Invalid env var format: '{ev}'. Use KEY=VAL.")
                key, val = ev.split("=", 1)
                extra_env[key] = val

        result = factory.services.exec_detached(
            app_name,
            command,
            process_type=process_type,
            extra_env=extra_env,
            timeout=timeout,
        )
        success(f"Started detached job: {result.job_name}")
        render_detail(
            f"{app_name} exec (detached)",
            {
                "Job": result.job_name,
                "Command": " ".join(result.command),
            },
        )
    else:
        result = factory.services.exec_interactive(
            app_name,
            command,
            process_type=process_type,
            dyno=dyno,
            tty=not no_tty,
        )
        if result.output:
            click.echo(result.output, nl=False)


# ── maintenance subgroup ─────────────────────────────────────────


@services.group("maintenance", cls=_ColonGroup, invoke_without_command=True)
@click.pass_context
def maintenance(ctx: click.Context) -> None:
    """Toggle maintenance mode for a single process type."""
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())


@maintenance.command("on")
@click.argument("process_type")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def maintenance_on(ctx: click.Context, process_type: str, app_flag: str | None) -> None:
    """Put a single process type into maintenance mode."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]
    factory.services.maintenance_on(app_name, process_type, on_step=lambda msg: info(msg))
    success(f"Maintenance enabled for {process_type} on {app_name}.")


@maintenance.command("off")
@click.argument("process_type")
@click.option("--app", "-a", "app_flag", default=None, help="Target app.")
@click.pass_context
def maintenance_off(ctx: click.Context, process_type: str, app_flag: str | None) -> None:
    """Restore a single process type from maintenance mode."""
    parent_app = ctx.obj.get("_services_app")
    app_name = resolve_app(app_flag or parent_app)
    factory = ctx.obj["factory"]
    factory.services.maintenance_off(app_name, process_type, on_step=lambda msg: info(msg))
    success(f"Maintenance disabled for {process_type} on {app_name}.")
