"""Nexus TLS recipe: nginx reverse-proxy with self-signed, CA, or ACME certs."""
from __future__ import annotations

from dataclasses import dataclass

from k4s.core.executor import Executor, ExecutorError
from k4s.core.products import Step
from k4s.recipes.common.linux import apt_install, ensure_apt_updated
from k4s.recipes.common.run import check, q, run
from k4s.ui.ui import Ui


@dataclass(frozen=True)
class NexusTlsPlan:
    """Plan inputs for enabling TLS on Nexus via an nginx reverse proxy."""

    http_port: int
    issuer: str
    domain: str | None = None
    email: str | None = None
    cert_key_path: str | None = None
    cert_pem_path: str | None = None
    docker_port: int | None = None
    docker_domain: str | None = None
    docker_connector_port: int = 8081
    https_port: int = 443

    @property
    def cert_dir(self) -> str:
        return "/etc/nginx/ssl/nexus"

    @property
    def cert_key_remote(self) -> str:
        return f"{self.cert_dir}/cert.key"

    @property
    def cert_pem_remote(self) -> str:
        return f"{self.cert_dir}/cert.pem"


def build_tls_steps(ui: Ui, ex: Executor, plan: NexusTlsPlan) -> list[Step]:
    """Build ordered steps to enable TLS on Nexus via nginx reverse proxy."""
    issuer = plan.issuer.lower().strip()
    runtime: dict[str, object] = {"nginx_was_active": False}

    # -- helpers --------------------------------------------------------------

    def _acme_domains() -> list[str]:
        """Return all domains to include in the ACME certificate."""
        domains = [plan.domain]
        if plan.docker_domain and plan.docker_domain != plan.domain:
            domains.append(plan.docker_domain)
        return domains

    # -- step functions -------------------------------------------------------

    def _validate():
        ui.log("Validating issuer, domain, and Nexus reachability.")
        if issuer not in {"self-signed", "ca", "acme"}:
            raise ValueError(f"Unsupported issuer: {plan.issuer}")
        if issuer in {"self-signed", "acme"} and not plan.domain:
            raise ValueError(f"Missing --domain for {issuer} issuer.")
        if issuer == "acme" and not plan.email:
            raise ValueError("Missing --email for acme issuer.")
        if issuer == "ca":
            if not plan.cert_key_path or not plan.cert_pem_path:
                raise ValueError("CA issuer requires --cert-key-path and --cert-pem-path.")
            if not plan.domain:
                raise ValueError("Missing --domain for ca issuer (used in nginx server_name).")
        if plan.docker_port and not plan.docker_domain:
            raise ValueError("--docker-port requires --docker-domain.")
        # Verify Nexus is actually listening on the expected HTTP port.
        rc, _, _ = run(ex, f"curl -fsS -m 5 http://127.0.0.1:{plan.http_port}/ >/dev/null 2>&1 || true")
        if rc != 0:
            ui.warning(f"Nexus does not appear to be responding on http://127.0.0.1:{plan.http_port}/ (best-effort check).")

    def _install_nginx():
        ui.log("Installing nginx via apt.")
        ensure_apt_updated(ex)
        apt_install(ex, ["nginx"])

    def _prepare_cert_dir():
        ui.log(f"Creating certificate directory {plan.cert_dir}.")
        check(ex, f"sudo -n mkdir -p {q(plan.cert_dir)}")

    def _self_signed():
        ui.log(f"Generating self-signed certificate for {plan.domain}.")

        # Build SAN list: domain(s) + auto-detected server IPs.
        san_entries = [f"DNS:{plan.domain}"]
        if plan.docker_domain and plan.docker_domain != plan.domain:
            san_entries.append(f"DNS:{plan.docker_domain}")

        # Detect server IPs (private + public) for IP-based access.
        _, priv_ips, _ = run(ex, "hostname -I 2>/dev/null || true")
        for ip in (priv_ips or "").split():
            ip = ip.strip()
            if ip:
                san_entries.append(f"IP:{ip}")
        _, pub_ip, _ = run(ex, "curl -s --max-time 3 https://api.ipify.org 2>/dev/null || true")
        pub_ip = (pub_ip or "").strip()
        if pub_ip and f"IP:{pub_ip}" not in san_entries:
            san_entries.append(f"IP:{pub_ip}")

        san_string = ",".join(san_entries)
        ui.log(f"SAN entries: {san_string}")

        cmd = (
            "sudo -n openssl req -x509 -nodes -days 365 -newkey rsa:2048 "
            f"-keyout {q(plan.cert_key_remote)} -out {q(plan.cert_pem_remote)} "
            f"-subj {q('/CN=' + plan.domain)} "
            f"-addext {q('subjectAltName=' + san_string)}"
        )
        check(ex, cmd)
        check(ex, f"sudo -n chmod 600 {q(plan.cert_key_remote)}")

    def _ca_upload():
        ui.log("Uploading CA-issued certificate and key.")
        ex.upload_file(plan.cert_key_path, plan.cert_key_remote, permissions="600", use_sudo=True)
        ex.upload_file(plan.cert_pem_path, plan.cert_pem_remote, permissions="644", use_sudo=True)

    def _acme_prepare():
        ui.log("Installing certbot and checking port 80 availability.")
        apt_install(ex, ["certbot"])

        # Check DNS for all domains that will be included in the certificate.
        for domain in _acme_domains():
            _, dns_out, _ = run(ex, f"getent ahostsv4 {q(domain)} 2>/dev/null || true")
            if dns_out.strip():
                ui.info(f"DNS for {domain} (from target):\n{dns_out.strip()}")
            else:
                ui.warning(f"DNS resolution for {domain} returned no records. ACME validation may fail.")

        # Record nginx state so we can restore it later.
        _, out, _ = run(ex, "systemctl is-active nginx 2>/dev/null || true")
        runtime["nginx_was_active"] = out.strip() == "active"

        # Stop nginx to free port 80 for certbot standalone.
        if runtime["nginx_was_active"]:
            ui.log("Temporarily stopping nginx to free port 80 for ACME standalone.")
            run(ex, "sudo -n systemctl stop nginx")

        # ss always prints a header line; skip it with tail -n +2 so only
        # actual listener rows remain.
        _, port80, _ = run(ex, "sudo -n ss -ltnp 'sport = :80' 2>/dev/null | tail -n +2 || true")
        if port80.strip():
            raise ExecutorError(
                "Port 80 is still in use after stopping nginx.\n"
                "ACME HTTP-01 standalone requires free port 80. Free it manually and retry."
            )

    def _acme_issue():
        ui.log(f"Issuing ACME certificate for {plan.domain}.")
        domains = " ".join(f"-d {q(d)}" for d in _acme_domains())

        # --expand allows adding new domains to an existing cert (e.g.
        # adding --docker-domain on a second run).  --keep-until-expiring
        # avoids unnecessary renewal when domains haven't changed.
        cmd = (
            f"sudo -n certbot certonly --standalone {domains} "
            "--agree-tos --non-interactive "
            f"-m {q(plan.email)} "
            "--preferred-challenges http "
            "--keep-until-expiring --expand"
        )
        check(ex, cmd)

        live_dir = f"/etc/letsencrypt/live/{plan.domain}"
        check(ex, f"sudo -n cp {q(live_dir)}/fullchain.pem {q(plan.cert_pem_remote)}")
        check(ex, f"sudo -n cp {q(live_dir)}/privkey.pem {q(plan.cert_key_remote)}")
        check(ex, f"sudo -n chmod 600 {q(plan.cert_key_remote)}")

    def _acme_restore_nginx():
        # nginx will be (re)started by _reload_nginx anyway,
        # but if certbot failed and we got here via error handling, restore state.
        pass

    def _configure_nginx():
        ui.log("Writing nginx reverse-proxy configuration for Nexus.")
        conf = _build_nginx_config(plan)
        # Write via tee
        check(
            ex,
            f"cat <<'K4S_NGINX_EOF' | sudo -n tee /etc/nginx/sites-available/nexus >/dev/null\n{conf}K4S_NGINX_EOF",
        )
        # Enable the site
        check(ex, "sudo -n ln -sf /etc/nginx/sites-available/nexus /etc/nginx/sites-enabled/nexus")
        # Remove default site if it exists (avoids port 80/443 conflicts)
        run(ex, "sudo -n rm -f /etc/nginx/sites-enabled/default")

    def _test_and_reload_nginx():
        ui.log("Testing nginx configuration and reloading.")
        check(ex, "sudo -n nginx -t", error_hint="nginx configuration test failed. Check /etc/nginx/sites-available/nexus.")
        check(ex, "sudo -n systemctl reload-or-restart nginx")
        check(ex, "sudo -n systemctl enable nginx")

    def _verify():
        ui.log(f"Verifying HTTPS reachability on port {plan.https_port}.")
        # curl -k to accept self-signed / untrusted certs during verification
        rc, _, _ = run(ex, f"curl -kfsS -m 5 https://127.0.0.1:{plan.https_port}/ >/dev/null 2>&1 || true")
        if rc == 0:
            ui.info(f"Nexus web UI: https://{plan.domain}:{plan.https_port}/")
        else:
            ui.warning(f"HTTPS verification on 127.0.0.1:{plan.https_port} returned non-zero (Nexus may still be starting).")

        if plan.docker_port and plan.docker_domain:
            # Check if Nexus Docker connector is actually listening.
            rc, _, _ = run(
                ex,
                f"curl -kfsS -m 5 http://127.0.0.1:{plan.docker_connector_port}/v2/ >/dev/null 2>&1 || true",
            )
            if rc == 0:
                ui.info(f"Docker registry: https://{plan.docker_domain}:{plan.docker_port}/")
            else:
                ui.warning(
                    f"Nexus Docker upstream on port {plan.docker_connector_port} is not responding.\n"
                    "  If you are using legacy port connectors, create a Docker hosted repository\n"
                    "  with an HTTP connector on this port.\n"
                    "  If you are using path-based routing, set --docker-connector-port to the Nexus\n"
                    "  HTTP port (usually 8081) and enable 'Path-based routing' on the Docker repo.\n"
                    f"  Nexus UI: https://{plan.domain}:{plan.https_port}/ → Repositories → docker (hosted)."
                )
                ui.info(f"Docker registry (after Nexus config): https://{plan.docker_domain}:{plan.docker_port}/")

    # -- assemble steps -------------------------------------------------------

    steps: list[Step] = [
        Step(title="Validate TLS inputs", run=_validate),
        Step(title="Install nginx", run=_install_nginx),
        Step(title="Prepare certificate directory", run=_prepare_cert_dir),
    ]

    if issuer == "self-signed":
        steps.append(Step(title="Generate self-signed certificate", run=_self_signed))
    elif issuer == "ca":
        steps.append(Step(title="Upload CA-issued certificate", run=_ca_upload))
    elif issuer == "acme":
        steps.extend([
            Step(title="Prepare ACME (certbot, free port 80)", run=_acme_prepare),
            Step(title="Issue ACME certificate", run=_acme_issue),
        ])

    steps.extend([
        Step(title="Configure nginx reverse proxy", run=_configure_nginx),
        Step(title="Test and reload nginx", run=_test_and_reload_nginx),
        Step(title="Verify HTTPS", run=_verify),
    ])

    return steps


def _build_nginx_config(plan: NexusTlsPlan) -> str:
    """Generate nginx site configuration for Nexus reverse proxy."""

    # Collect all server_names for the HTTP redirect block.
    redirect_names = plan.domain
    if plan.docker_domain and plan.docker_domain != plan.domain:
        redirect_names += f" {plan.docker_domain}"

    blocks = [
        f"""# Managed by k4s - Nexus TLS reverse proxy
server {{
    listen 80;
    server_name {redirect_names};
    return 301 https://$host$request_uri;
}}

server {{
    listen {plan.https_port} ssl;
    server_name {plan.domain};

    ssl_certificate     {plan.cert_pem_remote};
    ssl_certificate_key {plan.cert_key_remote};
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    client_max_body_size 0;
    proxy_read_timeout 600;
    proxy_send_timeout 600;

    location / {{
        proxy_pass http://127.0.0.1:{plan.http_port};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }}
}}"""
    ]

    # Optional Docker registry block with Docker-specific settings.
    if plan.docker_port and plan.docker_domain:
        blocks.append(
            f"""
server {{
    listen {plan.docker_port} ssl;
    server_name {plan.docker_domain};

    ssl_certificate     {plan.cert_pem_remote};
    ssl_certificate_key {plan.cert_key_remote};
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # Docker registry requires large layer uploads and no buffering.
    client_max_body_size 0;
    chunked_transfer_encoding on;
    proxy_buffering off;
    proxy_request_buffering off;
    proxy_read_timeout 900;
    proxy_send_timeout 900;

    location / {{
        proxy_pass http://127.0.0.1:{plan.docker_connector_port};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Docker-Distribution-Api-Version registry/2.0;
    }}
}}"""
        )

    return "\n".join(blocks) + "\n"
