from __future__ import annotations

from k4s.core.executor import ExecutorError
from k4s.core.downloader import AssetDownloader
from k4s.core.executor import Executor
from k4s.core.products import Step
from k4s.recipes.common.docker import build_docker_steps
from k4s.recipes.common.linux import (
    apt_install,
    detect_ubuntu_like,
    ensure_apt_updated,
    ensure_dir_owned,
    ensure_group,
    ensure_user,
)
from k4s.recipes.common.run import check, q, run
from k4s.recipes.dataiku.model import DataikuInstallPlan
from k4s.recipes.dataiku.r_integration import build_r_steps
from k4s.ui.ui import Ui


def run_preflight(ui: Ui, ex: Executor, plan: DataikuInstallPlan) -> None:
    """Run read-only preflight checks for Dataiku install."""
    ui.log("Checking OS basics and non-interactive sudo.")

    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.")

    _, os_release, _ = run(ex, "cat /etc/os-release 2>/dev/null || true")
    os_info: dict[str, str] = {}
    for line in (os_release or "").splitlines():
        if "=" not in line:
            continue
        k, v = line.split("=", 1)
        os_info[k.strip()] = v.strip().strip('"')

    os_id = (os_info.get("ID") or "").lower()
    ver = os_info.get("VERSION_ID") or ""
    # pretty = os_info.get("PRETTY_NAME") or f"{os_id} {ver}".strip()
    # ui.info(f"Target OS: {pretty}")

    if os_id == "ubuntu" and ver.startswith("24.04"):
        raise ExecutorError(
            "Dataiku dependency installer does not support Ubuntu 24.04 for this DSS version.\n"
            "Use a supported OS (commonly Ubuntu 22.04) or try a newer DSS version if Dataiku supports it."
        )


def build_install_steps(ui: Ui, ex: Executor, plan: DataikuInstallPlan, *, force: bool = False) -> list[Step]:
    """Build Dataiku DSS install steps based on the plan."""

    def _preflight():
        run_preflight(ui, ex, plan)

    def _users_and_dirs():
        ui.log("Creating Dataiku OS user/group and required directories (idempotent).")
        ensure_group(ex, plan.os_group)
        ensure_user(ex, user=plan.os_user, group=plan.os_group)
        ensure_dir_owned(ex, plan.root_dir, owner=plan.os_user, group=plan.os_group)
        ensure_dir_owned(ex, plan.install_dir, owner=plan.os_user, group=plan.os_group)
        ensure_dir_owned(ex, plan.data_dir, owner=plan.os_user, group=plan.os_group)

    def _install_prereqs():
        ui.log("Installing base packages (apt): ca-certificates, curl, gnupg, tar, wget.")
        ensure_apt_updated(ex)
        apt_install(ex, ["ca-certificates", "curl", "gnupg", "tar", "wget"])

    def _download_archive():
        if plan.archive_path:
            ui.log("Uploading local DSS archive to target machine (offline mode).")
            ex.upload_file(plan.archive_path, plan.archive_remote_path, owner_spec=f"{plan.os_user}:{plan.os_group}", use_sudo=True)
        else:
            ui.log("Downloading DSS archive from Dataiku CDN.")
            AssetDownloader(ex).download(plan.archive_url, plan.archive_remote_path, sudo=True)
            check(ex, f"sudo -n chown {q(plan.os_user)}:{q(plan.os_group)} {q(plan.archive_remote_path)}")

    def _extract():
        ui.log("Extracting DSS archive.")
        cmd = (
            f"sudo -n -u {q(plan.os_user)} -i -- sh -c "
            f"{q(f'tar xzf {plan.archive_remote_path} -C {plan.install_dir}')}"
        )
        check(ex, cmd)

    def _install_deps():
        ui.log("Running Dataiku dependency installer (install-deps.sh).")
        check(
            ex,
            f"sudo -n DEBIAN_FRONTEND=noninteractive {q(plan.extracted_dir)}/scripts/install/install-deps.sh -yes",
        )

    def _run_installer():
        installer_args = f"-d {plan.data_dir} -p {plan.dss_port} -y"
        if plan.node_type == "automation":
            installer_args = f"-t automation {installer_args}"
        ui.log(f"Running DSS installer ({plan.node_type} node).")
        cmd = (
            f"sudo -n -u {q(plan.os_user)} -i -- sh -c "
            f"{q(f'cd {plan.extracted_dir} && ./installer.sh {installer_args}')}"
        )
        check(ex, cmd)

    def _service_name() -> str:
        """Return the systemd service name for this DSS node type."""
        return f"dataiku.{plan.node_type}"

    def _install_boot():
        """Register this DSS instance as a systemd service via install-boot.sh.

        Uses the -n flag to create a per-node service (e.g. dataiku.design,
        dataiku.automation) so multiple instances can coexist independently.
        """
        svc = _service_name()
        ui.log(f"Installing boot script (service: {svc}).")
        check(
            ex,
            f"sudo -n sh {q(plan.extracted_dir)}/scripts/install/install-boot.sh "
            f"-n {q(plan.node_type)} {q(plan.data_dir)} {q(plan.os_user)}",
        )

    def _start():
        svc = _service_name()
        ui.log(f"Enabling and starting {svc} via systemd.")
        check(ex, "sudo -n systemctl daemon-reload")
        check(ex, f"sudo -n systemctl enable {svc}")
        check(
            ex,
            f"sudo -n systemctl start {svc}",
            error_hint=f"DSS failed to start. Check port conflicts or run: journalctl -xeu {svc}.service",
        )
        ui.log("Waiting for DSS to become ready.")
        run(ex, "sleep 15")

    def _already_installed() -> bool:
        rc, _, _ = run(ex, f"test -x {q(plan.data_dir)}/bin/dss")
        return rc == 0

    def _wipe_existing():
        """Stop DSS and remove the data directory for a clean reinstall."""
        svc = _service_name()
        ui.log(f"Stopping {svc} service.")
        run(ex, f"sudo -n systemctl stop {svc} || true")
        run(ex, f"sudo -n systemctl disable {svc} || true")
        # Fallback: also try dss stop in case the service is not registered.
        run(ex, f"sudo -n -u {q(plan.os_user)} -i -- sh -c {q(f'{plan.data_dir}/bin/dss stop')} || true")
        ui.log(f"Removing existing data directory: {plan.data_dir}")
        check(ex, f"sudo -n rm -rf {q(plan.data_dir)}")
        ui.log(f"Recreating data directory: {plan.data_dir}")
        ensure_dir_owned(ex, plan.data_dir, owner=plan.os_user, group=plan.os_group)

    def _guarded_install():
        if _already_installed() and not force:
            raise ExecutorError(
                f"Dataiku already installed at {plan.data_dir}.\n"
                "  Use --force to reinstall."
            )
        if force and _already_installed():
            _wipe_existing()
        _download_archive()
        _extract()
        _install_deps()
        _run_installer()
        _install_boot()
        _start()

    steps: list[Step] = [
        Step(title=f"Preflight (Dataiku) on {ex.host}", run=_preflight),
        Step(title="Create OS user/group and directories", run=_users_and_dirs),
        Step(title="Install system prerequisites", run=_install_prereqs),
    ]

    if plan.install_docker:
        steps.extend(
            build_docker_steps(
                ui,
                ex,
                data_root=plan.docker_data_root,
                docker_group_user=plan.os_user,
            )
        )

    node_label = "Automation" if plan.node_type == "automation" else "Design"
    steps.append(
        Step(title=f"Install Dataiku DSS ({node_label} node)", run=_guarded_install)
    )

    if plan.install_r:
        steps.extend(
            build_r_steps(
                ui,
                ex,
                extracted_dir=plan.extracted_dir,
                data_dir=plan.data_dir,
                os_user=plan.os_user,
                r_repo=plan.r_repo,
                r_pkg_dir=plan.r_pkg_dir,
            )
        )

    if plan.os_user_password:
        def _set_password():
            ui.log(f"Setting password for OS user '{plan.os_user}'.")
            # Use chpasswd which reads from stdin (password never appears in ps/logs).
            rc, out, err = ex.execute(
                "sudo -n chpasswd",
                stdin_data=f"{plan.os_user}:{plan.os_user_password}\n",
            )
            if rc != 0:
                raise ExecutorError(f"Failed to set OS user password (rc={rc}).\n{err or out}")

        steps.append(Step(title=f"Set password for '{plan.os_user}'", run=_set_password))

    return steps
