from __future__ import annotations

from dataclasses import dataclass
from pathlib import PurePosixPath

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.ini import set_ini_value
from k4s.recipes.common.run import check, q, run
from k4s.ui.ui import Ui


@dataclass(frozen=True)
class DataikuTlsPlan:
    os_user: str
    os_group: str
    data_dir: str
    issuer: str
    domain: str | None = None
    email: str | None = None  # required for ACME
    stop_nginx: bool = False
    cert_key_path: str | None = None  # local path for CA issuer
    cert_pem_path: str | None = None  # local path for CA issuer

    @property
    def ssl_dir(self) -> str:
        return str(PurePosixPath(self.data_dir) / "config" / "ssl")

    @property
    def cert_key_remote(self) -> str:
        return str(PurePosixPath(self.ssl_dir) / "cert.key")

    @property
    def cert_pem_remote(self) -> str:
        return str(PurePosixPath(self.ssl_dir) / "cert.pem")

    @property
    def install_ini(self) -> str:
        return str(PurePosixPath(self.data_dir) / "install.ini")


def build_tls_steps(ui: Ui, ex: Executor, plan: DataikuTlsPlan) -> list[Step]:
    issuer = plan.issuer.lower().strip()
    runtime: dict[str, object] = {"nginx_was_active": False}

    def _validate():
        ui.log("Validating issuer selection and checking install.ini exists.")
        if issuer not in {"self-signed", "ca", "acme"}:
            raise ValueError(f"Unsupported issuer: {plan.issuer}")
        rc, _, _ = run(ex, f"test -f {q(plan.install_ini)}")
        if rc != 0:
            raise ExecutorError(f"Missing install.ini at {plan.install_ini}. Run Dataiku install first.")
        if issuer == "self-signed" and not plan.domain:
            raise ValueError("Missing --domain for self-signed issuer.")
        if issuer == "acme":
            if not plan.domain:
                raise ValueError("Missing --domain for acme issuer.")
            if not plan.email:
                raise ValueError("Missing --email for acme issuer.")
            ui.log(
                "ACME (HTTP-01) requirements:\n"
                "- DNS A/AAAA for the domain must resolve to this server's public IP\n"
                "- Inbound TCP port 80 must be reachable from the internet\n"
                "- Another service using port 80 will block standalone validation\n"
                "\n"
                "Notes:\n"
                "- HTTP-01 is defined for port 80; using an arbitrary custom public port is not supported.\n"
                "- If you want k4s to temporarily stop nginx to free port 80, pass --stop-nginx."
            )
        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 (local paths).")

    def _stop_dss():
        ui.log("Stopping DSS before changing TLS configuration.")
        cmd = f"sudo -n -u {q(plan.os_user)} {q(plan.data_dir)}/bin/dss stop"
        run(ex, cmd)  # ignore errors

    def _ensure_ssl_dir():
        ui.log(f"Creating and setting ownership on {plan.ssl_dir}.")
        check(ex, f"sudo -n mkdir -p {q(plan.ssl_dir)}")
        check(ex, f"sudo -n chown -R {q(plan.os_user)}:{q(plan.os_group)} {q(plan.ssl_dir)}")

    def _self_signed():
        ui.log("Generating self-signed cert.pem/cert.key under DATA_DIR/config/ssl.")

        # Build SAN list: domain + auto-detected server IPs.
        san_entries = [f"DNS:{plan.domain}"]
        _, 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 chown -R {q(plan.os_user)}:{q(plan.os_group)} {q(plan.ssl_dir)}")
        check(ex, f"sudo -n chmod 600 {q(plan.cert_key_remote)}")

    def _ca_upload():
        ui.log("Uploading CA-issued cert.pem/cert.key to DATA_DIR/config/ssl.")
        ex.upload_file(plan.cert_key_path, plan.cert_key_remote, owner_spec=f"{plan.os_user}:{plan.os_group}", permissions="600", use_sudo=True)
        ex.upload_file(plan.cert_pem_path, plan.cert_pem_remote, owner_spec=f"{plan.os_user}:{plan.os_group}", permissions="644", use_sudo=True)

    def _acme_prepare():
        ui.log("Installing certbot and checking port 80 availability for HTTP-01 challenge.")
        # Install certbot on Ubuntu/Debian.
        ensure_apt_updated(ex)
        apt_install(ex, ["certbot"])

        # Preflight checks: show DNS resolution and check port 80 usage.
        _, dns_out, _ = run(ex, f"getent ahostsv4 {q(plan.domain)} 2>/dev/null || true")
        if dns_out.strip():
            ui.info(f"DNS resolution (from target):\n{dns_out.strip()}")
        else:
            ui.warning("DNS resolution from the target returned no records (best-effort check).")

        _, pub_ip, _ = run(ex, "curl -s https://api.ipify.org || true")
        if pub_ip.strip():
            ui.info(f"Target public IP (best-effort): {pub_ip.strip()}")

        # Check if something is listening on port 80.
        # ss always prints a header line; skip it with tail -n +2.
        _, port80, _ = run(ex, "sudo -n ss -ltnp 'sport = :80' 2>/dev/null | tail -n +2 || true")
        if port80.strip():
            ui.warning(f"Port 80 appears to be in use:\n{port80.strip()}")
            if not plan.stop_nginx:
                raise ExecutorError(
                    "ACME HTTP-01 standalone requires free port 80.\n"
                    "Either free port 80 manually, or rerun with --stop-nginx to attempt a temporary nginx stop."
                )

        # Record nginx running state and (optionally) stop it to free up port 80.
        _, out, _ = run(ex, "systemctl is-active nginx 2>/dev/null || true")
        runtime["nginx_was_active"] = out.strip() == "active"
        if plan.stop_nginx:
            run(ex, "sudo -n systemctl stop nginx")  # ignore errors
            _, port80_after, _ = run(ex, "sudo -n ss -ltnp 'sport = :80' 2>/dev/null | tail -n +2 || true")
            if port80_after.strip():
                ui.warning(f"Port 80 is still in use after stopping nginx:\n{port80_after.strip()}")
                raise ExecutorError("Port 80 is still in use. Standalone HTTP-01 cannot continue.")

    def _acme_issue_and_copy():
        ui.log("Running certbot standalone, then copying fullchain/privkey into DATA_DIR/config/ssl.")
        # Certbot needs port 80 reachable and public DNS for domain.
        cmd = (
            "sudo -n certbot certonly --standalone "
            f"-d {q(plan.domain)} "
            "--agree-tos --non-interactive "
            f"-m {q(plan.email)} "
            "--preferred-challenges http "
            "--keep-until-expiring"
        )
        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 chown -R {q(plan.os_user)}:{q(plan.os_group)} {q(plan.ssl_dir)}")
        check(ex, f"sudo -n chmod 600 {q(plan.cert_key_remote)}")

    def _acme_restore_nginx():
        if runtime.get("nginx_was_active") is True:
            ui.log("Restoring nginx (it was active before ACME issuance).")
            run(ex, "sudo -n systemctl start nginx")  # ignore errors

    def _update_ini():
        ui.log("Setting [server] ssl=true and cert/key paths in install.ini.")
        content = ex.read_remote_file_content(plan.install_ini, use_sudo=True)
        content = set_ini_value(content, section="server", option="ssl", value="true")
        content = set_ini_value(content, section="server", option="ssl_certificate", value=plan.cert_pem_remote)
        content = set_ini_value(content, section="server", option="ssl_certificate_key", value=plan.cert_key_remote)
        content = set_ini_value(content, section="server", option="ssl_ciphers", value="recommended")
        ex.write_remote_file_content(plan.install_ini, content, use_sudo=True)
        check(ex, f"sudo -n chown {q(plan.os_user)}:{q(plan.os_group)} {q(plan.install_ini)}")

    def _regen_config():
        ui.log("Running dssadmin regenerate-config after TLS changes.")
        cmd = f"sudo -n -u {q(plan.os_user)} {q(plan.data_dir)}/bin/dssadmin regenerate-config"
        check(ex, cmd)

    def _start_dss():
        ui.log("Starting DSS after TLS changes.")
        cmd = f"sudo -n -u {q(plan.os_user)} {q(plan.data_dir)}/bin/dss start"
        run(ex, cmd)  # ignore errors

    issuer_steps: list[Step] = []
    if issuer == "self-signed":
        issuer_steps.append(
            Step(title="Generate self-signed certificate", run=_self_signed)
        )
    elif issuer == "ca":
        issuer_steps.append(
            Step(title="Upload CA-issued certificate", run=_ca_upload)
        )
    elif issuer == "acme":
        issuer_steps.extend(
            [
                Step(title="Install certbot and free port 80", run=_acme_prepare),
                Step(title="Issue ACME certificate and copy to Dataiku", run=_acme_issue_and_copy),
            ]
        )

    steps: list[Step] = [
        Step(title="Validate TLS inputs", run=_validate),
        Step(title="Stop Dataiku DSS (if running)", run=_stop_dss),
        Step(title="Prepare SSL directory", run=_ensure_ssl_dir),
    ]

    steps.extend(issuer_steps)
    steps.extend(
        [
            Step(title="Enable TLS in install.ini", run=_update_ini),
            Step(title="Regenerate Dataiku configuration", run=_regen_config),
            Step(title="Start Dataiku DSS", run=_start_dss),
        ]
    )

    if issuer == "acme":
        steps.append(
            Step(title="Restore nginx (if it was running)", run=_acme_restore_nginx)
        )

    return steps
