from __future__ import annotations

import time
from typing import List, Optional

from k4s.core.downloader import AssetDownloader
from k4s.core.executor import Executor, ExecutorError
from k4s.core.products import Step
from k4s.recipes.common.docker import add_insecure_registry
from k4s.recipes.common.linux import apt_install, detect_ubuntu_like, ensure_dir_owned, ensure_group, ensure_user
from k4s.recipes.common.run import check, q, run
from k4s.ui.ui import Ui

from .model import NexusInstallPlan


def run_preflight(ui: Ui, ex: Executor, plan: NexusInstallPlan) -> None:
    """Run read-only checks for Nexus install prerequisites."""
    ui.log("Checking sudo, systemctl, tar, curl availability.")

    if not detect_ubuntu_like(ex):
        ui.warning("OS detection: this recipe currently targets Ubuntu/Debian (apt).")

    check(ex, "sudo -n true", error_hint="Configure passwordless sudo (NOPASSWD) for the SSH user.")

    check(ex, "command -v tar")
    check(ex, "command -v systemctl")
    check(ex, "command -v curl")


def build_install_steps(ui: Ui, ex: Executor, plan: NexusInstallPlan) -> List[Step]:
    """Build Nexus installation steps."""

    tmp_dir = f"/tmp/k4s/nexus/{plan.version}"
    archive_path = f"{tmp_dir}/nexus-{plan.version}.tar.gz"
    extract_dir = f"{tmp_dir}/extract"

    dl = AssetDownloader(ex)
    checksums = [{"type": "sha256", "value": u} for u in plan.archive_sha256_urls] if plan.verify_checksum else None

    def _preflight() -> None:
        run_preflight(ui, ex, plan)

    def _install_prereqs() -> None:
        ui.log(f"Installing Java ({plan.java_package}) and common utilities via apt.")
        _install_prereqs_impl(ex, plan)

    def _create_tmp_dir() -> None:
        ui.log(f"Creating working directory at {tmp_dir}.")
        check(ex, f"sudo -n mkdir -p {q(tmp_dir)}")

    def _ensure_user() -> None:
        ui.log(f"Ensuring dedicated system user/group ({plan.os_user}).")
        _ensure_user_group(ex, plan)

    def _create_dirs() -> None:
        ui.log(f"Creating {plan.install_dir} and {plan.data_dir}.")
        check(ex, f"sudo -n mkdir -p {q(plan.install_dir)} {q(plan.data_dir)}")

    def _download() -> None:
        ui.log("Downloading Nexus tarball to target machine.")
        last_err: Exception | None = None
        for i, url in enumerate(plan.archive_urls):
            checksum = checksums[i] if checksums else None
            ui.log(f"Trying: {url}")
            try:
                dl.download(
                    url,
                    archive_path,
                    checksum_config=checksum,
                    sudo=True,
                )
                return
            except Exception as e:
                last_err = e
                msg = str(e)
                # If the URL is not found, try the next naming scheme.
                if "returned error: 404" in msg or "404" in msg:
                    ui.debug(f"Archive URL not found: {url}")
                    continue
                # For other errors (network, checksum, permissions), fail fast.
                raise
        if last_err:
            raise last_err

    def _extract() -> None:
        ui.log(f"Extracting Nexus into {plan.install_dir}.")
        _extract_if_needed(ui, ex, plan, archive_path, extract_dir)

    def _ownership() -> None:
        ui.log(f"Setting ownership of {plan.install_dir} and {plan.data_dir} to {plan.os_user}.")
        _ensure_ownership(ex, plan)

    def _vmoptions() -> None:
        ui.log("Setting -Dkaraf.data in vmoptions for Nexus data directory.")
        _configure_vmoptions(ex, plan)

    def _systemd() -> None:
        ui.log("Creating and enabling nexus.service systemd unit.")
        _install_systemd_service(ex, plan)

    def _start() -> None:
        ui.log("Starting Nexus and verifying the service becomes active.")
        _start_and_check(ui, ex, plan)

    def _add_insecure_registry() -> None:
        registry = f"{ex.host}:{plan.http_port}"
        ui.log(f"Adding {registry} to Docker insecure-registries on this host.")
        add_insecure_registry(ex, registry)

    def _create_docker_repo() -> None:
        _create_docker_hosted_repo(ui, ex, plan)

    def _hints() -> None:
        _post_install_hints(ui, ex, plan)

    steps: List[Step] = [
        Step(title=f"Preflight (Nexus) on {ex.host}", run=_preflight),
        Step(title="Install prerequisites (Java, curl, wget, tar, gnupg, ca-certificates)", run=_install_prereqs),
        Step(title="Create temp directory", run=_create_tmp_dir),
        Step(title="Ensure Nexus OS user/group", run=_ensure_user),
        Step(title="Create installation and data directories", run=_create_dirs),
        Step(title="Download Nexus archive", run=_download),
        Step(title="Extract Nexus archive", run=_extract),
        Step(title="Set ownership for Nexus directories", run=_ownership),
        Step(title="Configure Nexus data directory in vmoptions", run=_vmoptions),
        Step(title="Install systemd service", run=_systemd),
        Step(title="Start Nexus service", run=_start),
    ]

    steps.append(Step(title="Add Nexus to Docker insecure-registries", run=_add_insecure_registry))

    if plan.docker_repo_enabled:
        steps.append(Step(title="Create Docker hosted repository", run=_create_docker_repo))

    steps.append(Step(title="Post-install hints", run=_hints))

    return steps


def _install_prereqs_impl(ex: Executor, plan: NexusInstallPlan) -> None:
    apt_install(ex, ["curl", "wget", "tar", "gnupg", "ca-certificates"])
    _install_java_best_effort(ex, plan.java_package)


def _install_java_best_effort(ex: Executor, java_package: str) -> None:
    """Install Java via apt, with a few fallbacks for common Ubuntu releases."""
    candidates = [java_package]
    for p in ("openjdk-17-jre-headless", "openjdk-11-jre-headless"):
        if p not in candidates:
            candidates.append(p)

    last_err: Optional[BaseException] = None
    for pkg in candidates:
        try:
            apt_install(ex, [pkg])
            return
        except BaseException as e:
            last_err = e
    if last_err:
        raise last_err


def _ensure_user_group(ex: Executor, plan: NexusInstallPlan) -> None:
    ensure_group(ex, plan.os_user)
    ensure_user(ex, user=plan.os_user, group=plan.os_user)


def _extract_if_needed(ui: Ui, ex: Executor, plan: NexusInstallPlan, archive_path: str, extract_dir: str) -> None:
    rc, _, _ = run(ex, f"test -x {q(plan.install_dir)}/bin/nexus")
    if rc == 0:
        ui.log("Nexus binary already exists; skipping extraction.")
        return

    check(ex, f"sudo -n rm -rf {q(extract_dir)} && sudo -n mkdir -p {q(extract_dir)}")
    check(ex, f"sudo -n tar xzf {q(archive_path)} -C {q(extract_dir)}")

    script = (
        "set -euo pipefail; "
        f"src=$(ls -d {q(extract_dir)}/nexus-* 2>/dev/null | head -n1); "
        'test -n "$src"; '
        f"sudo -n mkdir -p {q(plan.install_dir)}; "
        f'sudo -n cp -a "$src/." {q(plan.install_dir)}/'
    )
    check(ex, f"bash -lc {q(script)}")


def _ensure_ownership(ex: Executor, plan: NexusInstallPlan) -> None:
    ensure_dir_owned(ex, plan.install_dir, owner=plan.os_user, group=plan.os_user)
    ensure_dir_owned(ex, plan.data_dir, owner=plan.os_user, group=plan.os_user)


def _configure_vmoptions(ex: Executor, plan: NexusInstallPlan) -> None:
    vmoptions = f"{plan.install_dir}/bin/nexus.vmoptions"
    cmd = (
        "sudo -n sh -c "
        + q(
            f"sed -i 's|^-Dkaraf.data=.*|-Dkaraf.data={plan.data_dir}|' {q(vmoptions)} && "
            f"if ! grep -q '^-Dkaraf.data=' {q(vmoptions)}; then "
            f"echo '-Dkaraf.data={plan.data_dir}' >> {q(vmoptions)}; "
            "fi && "
            f"chown {q(plan.os_user)}:{q(plan.os_user)} {q(vmoptions)}"
        )
    )
    check(ex, cmd)


def _install_systemd_service(ex: Executor, plan: NexusInstallPlan) -> None:
    service = f"""[Unit]
Description=Nexus Repository Manager
After=network.target

[Service]
Type=forking
LimitNOFILE=65536
ExecStart={plan.install_dir}/bin/nexus start
ExecStop={plan.install_dir}/bin/nexus stop
User={plan.os_user}
Group={plan.os_user}
Restart=on-failure
TimeoutStartSec=300s
TimeoutStopSec=60s

[Install]
WantedBy=multi-user.target
"""
    check(ex, f"cat <<'EOF' | sudo -n tee /etc/systemd/system/nexus.service >/dev/null\n{service}EOF")
    check(ex, "sudo -n systemctl daemon-reload")
    check(ex, "sudo -n systemctl enable nexus.service")


def _start_and_check(ui: Ui, ex: Executor, plan: NexusInstallPlan) -> None:
    try:
        check(ex, "sudo -n systemctl start nexus.service")
        check(ex, "sudo -n systemctl is-active --quiet nexus.service")
    except ExecutorError:
        if _service_logs_suggest_java8_required(ex, "nexus.service"):
            ui.log("Service failed with Java version issue; auto-installing Temurin JRE 8.")
            java8_home = _ensure_temurin_java8_jre(ex, plan)
            _apply_systemd_java_override(ex, "nexus.service", java_home=java8_home)
            run(ex, "sudo -n systemctl reset-failed nexus.service || true")
            check(ex, "sudo -n systemctl restart nexus.service")
            check(ex, "sudo -n systemctl is-active --quiet nexus.service")
        else:
            raise

    run(ex, f"bash -lc {q(f'curl -fsS -m 3 http://127.0.0.1:{plan.http_port}/ >/dev/null || true')}")


def _service_logs_suggest_java8_required(ex: Executor, unit: str) -> bool:
    _, status, _ = run(ex, f"sudo -n systemctl status --no-pager -l {q(unit)} 2>&1 || true")
    _, journal, _ = run(ex, f"sudo -n journalctl --no-pager -u {q(unit)} -n 120 -o cat 2>&1 || true")
    blob = f"{status}\n{journal}".lower()
    return "must be 1.8" in blob or "no suitable java virtual machine" in blob or "install4j_java_home" in blob


def _ensure_temurin_java8_jre(ex: Executor, plan: NexusInstallPlan) -> str:
    """Install a private Java 8 JRE under /opt/k4s/java8 and return its JAVA_HOME."""
    base_dir = "/opt/k4s"
    java8_dir = f"{base_dir}/java8"
    tmp_dir = f"/tmp/k4s/nexus/{plan.version}"
    archive = f"{tmp_dir}/temurin8-jre.tar.gz"

    rc, _, _ = run(ex, f"test -x {q(java8_dir)}/bin/java")
    if rc == 0:
        return java8_dir

    check(ex, f"sudo -n mkdir -p {q(base_dir)} {q(tmp_dir)}")

    url = "https://api.adoptium.net/v3/binary/latest/8/ga/linux/x64/jre/hotspot/normal/eclipse"
    AssetDownloader(ex).download(url, archive, sudo=True)

    script = (
        "set -euo pipefail; "
        f"sudo -n rm -rf {q(java8_dir)} {q(base_dir)}/.java8-extract; "
        f"sudo -n mkdir -p {q(base_dir)}/.java8-extract; "
        f"sudo -n tar xzf {q(archive)} -C {q(base_dir)}/.java8-extract; "
        f"dir=$(ls -d {q(base_dir)}/.java8-extract/* 2>/dev/null | head -n1); "
        'test -n "$dir"; '
        f"sudo -n mv \"$dir\" {q(java8_dir)}; "
        f"sudo -n chown -R root:root {q(java8_dir)}"
    )
    check(ex, f"bash -lc {q(script)}")
    return java8_dir


def _apply_systemd_java_override(ex: Executor, unit: str, *, java_home: str) -> None:
    dropin_dir = f"/etc/systemd/system/{unit}.d"
    content = f"""[Service]
Environment=JAVA_HOME={java_home}
Environment=INSTALL4J_JAVA_HOME={java_home}
"""
    check(ex, f"sudo -n mkdir -p {q(dropin_dir)}")
    check(ex, f"cat <<'EOF' | sudo -n tee {q(dropin_dir)}/k4s-java.conf >/dev/null\n{content}EOF")
    check(ex, "sudo -n systemctl daemon-reload")


def _post_install_hints(ui: Ui, ex: Executor, plan: NexusInstallPlan) -> None:
    ui.info(f"Nexus UI: http://{ex.host}:{plan.http_port}/")

    max_attempts = 20
    sleep_seconds = 5
    total_wait = max_attempts * sleep_seconds
    pw_file: Optional[str] = None
    for attempt in range(max_attempts):
        pw_file = _find_admin_password_path(ex, plan.data_dir)
        if pw_file:
            break
        ui.debug(f"Waiting for admin.password file (attempt {attempt + 1}/{max_attempts})...")
        time.sleep(sleep_seconds)

    if pw_file:
        rc, out, _ = run(ex, f"sudo -n cat {q(pw_file)} 2>/dev/null || true")
        password = (out or "").strip() if rc == 0 else ""
        if password:
            ui.info(f"Initial admin password: {password}")
            ui.info("Note: rotate the admin password after first login.")
        else:
            ui.info(f"Initial admin password file: {pw_file} (could not read)")
    else:
        ui.warning(
            f"Initial admin password file not found after {total_wait}s.\n"
            "  Nexus may still be initializing. Check manually:\n"
            f"  sudo find {plan.data_dir} -name admin.password"
        )


def _create_docker_hosted_repo(ui: Ui, ex: Executor, plan: NexusInstallPlan) -> None:
    """Create a Docker hosted repository in Nexus via REST API."""
    import json as _json

    repo_name = (plan.docker_repo_name or "dataiku").strip()
    if not repo_name:
        repo_name = "dataiku"

    routing = (getattr(plan, "docker_repo_routing", None) or "path").strip().lower()
    if routing not in {"path", "connector"}:
        raise ExecutorError(f"Unsupported docker repo routing mode: {routing!r}")

    if routing == "path":
        # Sonatype docs note: docker repo names must be lowercase for path-based routing.
        # (Also keeps UX consistent with typical docker registry naming.)
        repo_name = repo_name.lower()

    def _pick_free_port(preferred: int = 8083) -> int:
        for p in range(preferred, preferred + 20):
            _, out, _ = run(ex, f"sudo -n ss -ltn 'sport = :{p}' 2>/dev/null | tail -n +2 || true")
            if not (out or "").strip():
                return p
        raise ExecutorError("Could not find a free port for Nexus Docker connector (tried 8083-8102).")

    base_url = f"http://127.0.0.1:{plan.http_port}"

    # Wait for the API to be ready.
    ui.log("Waiting for Nexus REST API to become available.")
    for attempt in range(30):
        rc, _, _ = run(ex, f"curl -fsS -m 3 {q(base_url)}/service/rest/v1/status >/dev/null 2>&1")
        if rc == 0:
            break
        ui.debug(f"Nexus API not ready yet (attempt {attempt + 1}/30).")
        import time as _time
        _time.sleep(5)
    else:
        raise ExecutorError("Nexus REST API did not become available in time.")

    # Read admin password.
    pw_file = _find_admin_password_path(ex, plan.data_dir)
    if not pw_file:
        raise ExecutorError(
            "Cannot create Docker repo: admin.password file not found.\n"
            "  Nexus may still be initializing. Re-run after Nexus finishes startup."
        )
    rc, pw_out, _ = run(ex, f"sudo -n cat {q(pw_file)} 2>/dev/null")
    admin_pw = (pw_out or "").strip()
    if not admin_pw:
        raise ExecutorError("Cannot create Docker repo: admin.password file is empty.")

    # Check if repo already exists (best-effort).
    rc, repos_json, _ = run(
        ex,
        f"curl -fsS -u admin:{q(admin_pw)} {q(base_url)}/service/rest/v1/repositories 2>/dev/null || true",
    )
    if rc == 0 and (repos_json or "").strip():
        if f'\"name\":\"{repo_name}\"' in repos_json.replace(" ", ""):
            ui.log(f"Docker repository '{repo_name}' already exists; skipping creation.")
            return

    def _post_repo(payload: str) -> tuple[int, str, str]:
        return run(
            ex,
            f"curl -fsS -X POST -u admin:{q(admin_pw)} "
            f"-H 'Content-Type: application/json' "
            f"-d {q(payload)} "
            f"{q(base_url)}/service/rest/v1/repositories/docker/hosted",
        )

    def _payload_common() -> dict:
        return {
            "name": repo_name,
            "online": True,
            "storage": {
                "blobStoreName": "default",
                "strictContentTypeValidation": True,
                "writePolicy": "ALLOW",
            },
        }

    # Create the repository.
    if routing == "path":
        # Nexus Repository 3.83+ supports Docker path-based routing with
        # docker.pathEnabled=true. See Docker Registry docs:
        # https://help.sonatype.com/en/docker-registry.html
        docker_cfg: dict[str, object] = {"v1Enabled": False, "forceBasicAuth": True, "pathEnabled": True}

        payload = _json.dumps(
            {
                **_payload_common(),
                "docker": docker_cfg,
            }
        )
        ui.log(f"Creating Docker hosted repository '{repo_name}' (path-based routing).")
        rc, out, err = _post_repo(payload)
        if rc == 0:
            # Verify config reflects path-based routing (best-effort).
            rc2, repo_json, _ = run(
                ex,
                f"curl -fsS -u admin:{q(admin_pw)} "
                f"{q(base_url)}/service/rest/v1/repositories/docker/hosted/{q(repo_name)} 2>/dev/null || true",
            )
            if rc2 == 0 and (repo_json or "").strip():
                try:
                    repo_cfg = _json.loads(repo_json)
                    docker_part = (repo_cfg or {}).get("docker") if isinstance(repo_cfg, dict) else None
                    if isinstance(docker_part, dict):
                        got = docker_part.get("pathEnabled")
                        if got is not True:
                            raise ExecutorError(
                                "Docker repository was created but path-based routing did not apply.\n"
                                f"  Expected docker.pathEnabled=true, got {got!r}\n"
                                "Hint: Retry with connector mode:\n"
                                "  k4s install nexus --with-docker-repo --docker-repo-routing connector ..."
                            )
                except ExecutorError:
                    raise
                except Exception:
                    # If parsing fails, continue; creation itself succeeded.
                    pass

            ui.info(f"Docker hosted repository '{repo_name}' created (path-based routing).")
            ui.info("Example usage:")
            ui.info(f"  docker pull <nexus-host>/{repo_name}/<namespace>/<image>:<tag>")
            return
        raise ExecutorError(
            "Failed to create Docker hosted repository with path-based routing.\n"
            f"  {err or out}\n"
            "Hint: Retry with legacy connector mode:\n"
            "  k4s install nexus --with-docker-repo --docker-repo-routing connector ..."
        )

    port = _pick_free_port(8083)
    payload = _json.dumps(
        {
            **_payload_common(),
            "docker": {
                "v1Enabled": False,
                "forceBasicAuth": True,
                "httpPort": port,
            },
        }
    )
    ui.log(f"Creating Docker hosted repository '{repo_name}' with HTTP connector on port {port}.")
    rc, out, err = _post_repo(payload)
    if rc != 0:
        raise ExecutorError(f"Failed to create Docker repository via Nexus API (rc={rc}).\n  {err or out}")
    ui.info(f"Docker hosted repository '{repo_name}' created (HTTP connector: {port}).")
    ui.info("Use this connector port when enabling TLS for Docker registry:")
    ui.info(f"  k4s tls enable nexus ... --docker-connector-port {port}")


def _find_admin_password_path(ex: Executor, data_dir: str) -> Optional[str]:
    candidates = [
        f"{data_dir}/admin.password",
        f"{data_dir}/nexus3/admin.password",
        f"{data_dir}/nexus-data/admin.password",
    ]
    for c in candidates:
        rc, _, _ = run(ex, f"sudo -n test -f {q(c)}")
        if rc == 0:
            return c
    return None
