import click

from k4s.cli.state import CliState
from k4s.cli.target import resolve_target, resolve_k8s_target, kubeconfig_path_for
from k4s.cli.verbosity import apply_command_ui_overrides
from k4s.cli.errors import exit_with_error
from k4s.core.executor import Executor
from k4s.core.executor import ExecutorError
from k4s.core.downloader import AssetDownloader
from k4s.core.products import run_steps
from k4s.recipes.common.docker import build_docker_steps
from k4s.recipes.common.linux import install_kubectl as _install_kubectl_impl, ensure_user_in_group as _ensure_user_in_group
from k4s.recipes.dataiku.install import build_install_steps, run_preflight
from k4s.recipes.dataiku.model import DataikuInstallPlan
from k4s.recipes.common.run import q
from k4s.recipes.common.run import check
from k4s.recipes.dataiku.r_integration import build_r_steps
from k4s.recipes.nexus.install import build_install_steps as build_nexus_steps
from k4s.recipes.nexus.install import run_preflight as nexus_run_preflight
from k4s.recipes.nexus.model import NexusInstallPlan
from k4s.recipes.ingress_nginx.install import build_install_steps as build_ingress_nginx_steps
from k4s.recipes.ingress_nginx.model import IngressNginxInstallPlan
from k4s.recipes.starburst_oci.install import build_install_steps as build_starburst_oci_install_steps
from k4s.recipes.starburst_oci.install import build_plan_from_env as starburst_oci_plan_from_env
from k4s.recipes.starburst_oci.model import StarburstOciComponentInstallPlan
from k4s.recipes.starburst.install import build_install_steps as build_starburst_install_steps
from k4s.recipes.starburst.install import build_plan_from_env as starburst_plan_from_env
from k4s.recipes.starburst.model import StarburstInstallPlan
from k4s.recipes.datafloem.install import build_install_steps as build_datafloem_install_steps
from k4s.recipes.datafloem.install import build_plan_from_env as datafloem_plan_from_env
from k4s.recipes.datafloem.model import DatafloemInstallPlan


class OrderedInstallGroup(click.Group):
    """Install subgroup with stable command order in --help."""

    _order = [
        "r",
        "docker",
        "kubectl",
        "nexus",
        "dataiku",
        "ingress-nginx",
        "hive",
        "ranger",
        "starburst",
        "cache",
        "datafloem",
    ]

    def list_commands(self, ctx):
        # Show preferred order first, then anything else.
        names = list(self.commands)
        ordered = [c for c in self._order if c in self.commands]
        rest = [c for c in names if c not in ordered]
        return ordered + rest


@click.group(cls=OrderedInstallGroup)
@click.pass_context
def install(ctx):
    """Install products (verb-first)."""
    pass


@install.command("docker")
@click.option("--context", "context_name", default=None, help="Context name (or 'local'). Uses current context if omitted.")
@click.option("--data-root", default=None, help="Optional Docker data-root path (e.g. /data/docker).")
@click.option(
    "--docker-group-user",
    "docker_group_user",
    default=None,
    help="Add an existing OS user to the docker group (enables docker without sudo).",
)
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def docker(ctx, context_name, data_root, docker_group_user, dry_run, quiet, verbose):
    """Install Docker Engine on the target host."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_target(state, context_name)
        ex = Executor(c.to_server_config())

        from k4s.core.products import Step
        from k4s.recipes.common.run import run as _run_cmd

        steps = build_docker_steps(ui, ex, data_root=data_root)

        if docker_group_user:
            def _add_to_group():
                rc, _, _ = _run_cmd(ex, "getent group docker >/dev/null 2>&1", silent=True)
                if rc != 0:
                    ui.log("Docker group does not exist yet; skipping.")
                    return
                ui.log(f"Adding {docker_group_user} to the docker group.")
                _ensure_user_in_group(ex, user=docker_group_user, group="docker")
            steps.append(Step(title=f"Add {docker_group_user} to docker group", run=_add_to_group))

        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)
            state.history.append(
                action="install",
                product="docker",
                context=c.name,
                host=c.host,
                params={"data_root": data_root, "docker_group_user": docker_group_user},
            )
            ui.success("Docker installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("kubectl")
@click.option("--context", "context_name", default=None, help="Context name (or 'local'). Uses current context if omitted.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def kubectl(ctx, context_name, dry_run, quiet, verbose):
    """Install kubectl on the target host via the official Kubernetes apt repository."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_target(state, context_name)
        ex = Executor(c.to_server_config())

        from k4s.core.products import Step

        def _run():
            ui.log("Installing kubectl via official Kubernetes apt repository.")
            _install_kubectl_impl(ex)

        steps = [Step(title="Install kubectl", run=_run)]

        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)
            state.history.append(
                action="install",
                product="kubectl",
                context=c.name,
                host=c.host,
                params={},
            )
            ui.success("kubectl installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("r")
@click.option("--context", "context_name", default=None, help="Context name (or 'local'). Uses current context if omitted.")
@click.option("--version", required=True, help="Dataiku DSS version to install (e.g. 13.5.4).")
@click.option("--os-user", default="dataiku", show_default=True, help="OS user that owns the DSS installation.")
@click.option("--root-dir", default="/data/dataiku", show_default=True, help="")
@click.option("--install-dir-name", "install_dir_name", default="INSTALL_DIR", show_default=True, help="")
@click.option("--data-dir-name", "data_dir_name", default="DATA_DIR", show_default=True, help="")
@click.option("--archive-path", type=click.Path(exists=True, dir_okay=False), default=None, help="Local tar.gz for offline install (otherwise downloaded from Dataiku CDN).")
@click.option("--r-repo", default=None, help="CRAN mirror URL for R packages (proxy/mirror environments).")
@click.option("--r-pkg-dir", type=click.Path(exists=True, file_okay=False), default=None, help="Local directory with pre-downloaded R packages (air-gapped).")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def r(ctx, context_name, version, os_user, root_dir, install_dir_name, data_dir_name, archive_path, r_repo, r_pkg_dir, dry_run, quiet, verbose):
    """Install R integration for an existing Dataiku DSS instance."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_target(state, context_name)
        root_dir = root_dir.rstrip("/")
        install_dir = f"{root_dir}/{install_dir_name}".rstrip("/")
        data_dir = f"{root_dir}/{data_dir_name}".rstrip("/")
        extracted_dir = f"{install_dir}/dataiku-dss-{version}"
        archive_url = f"https://downloads.dataiku.com/public/dss/{version}/dataiku-dss-{version}.tar.gz"
        archive_remote_path = f"{install_dir}/dataiku-dss-{version}.tar.gz"

        ex = Executor(c.to_server_config())
        # Preflight: ensure Dataiku looks present and ensure the requested installer scripts exist.
        with ex:
            rc, _, _ = ex.execute(f"test -x {q(f'{data_dir}/bin/dssadmin')}")
            if rc != 0:
                raise ExecutorError(
                    f"Dataiku not found at {data_dir}.\n"
                    "Install DSS first with `k4s install dataiku`, or pass the correct "
                    "--root-dir/--data-dir-name."
                )

            deps_script = f"{extracted_dir}/scripts/install/install-deps.sh"
            rc2, _, _ = ex.execute(f"test -x {q(deps_script)}")
            if rc2 != 0:
                if dry_run:
                    ui.warning(
                        f"Requested Dataiku installer scripts not found at {deps_script}.\n"
                        f"In a real run, k4s will download/extract DSS {version} to {install_dir}."
                    )
                else:
                    # Ensure we have the exact requested version available on the target.
                    check(ex, f"sudo -n mkdir -p {q(install_dir)}")
                    if archive_path:
                        ui.log("Uploading DSS archive to target (to obtain installer scripts).")
                        ex.upload_file(archive_path, archive_remote_path, use_sudo=True)
                    else:
                        ui.log(f"Downloading DSS archive (to obtain installer scripts): {archive_url}")
                        AssetDownloader(ex).download(archive_url, archive_remote_path, sudo=True)
                    ui.log("Extracting DSS archive (installer scripts).")
                    check(ex, f"sudo -n tar xzf {q(archive_remote_path)} -C {q(install_dir)}")
                    # Re-check
                    rc4, _, _ = ex.execute(f"test -x {q(deps_script)}")
                    if rc4 != 0:
                        raise ExecutorError(
                            f"install-deps.sh still not found at {deps_script} after extracting DSS {version}.\n"
                            "Check that --root-dir/--install-dir match the target layout."
                        )

        steps = build_r_steps(
            ui,
            ex,
            extracted_dir=extracted_dir,
            data_dir=data_dir,
            os_user=os_user,
            r_repo=r_repo,
            r_pkg_dir=r_pkg_dir,
        )
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)
            state.history.append(
                action="install",
                product="r",
                context=c.name,
                host=c.host,
                params={
                    "data_dir": data_dir,
                    "extracted_dir": extracted_dir,
                    "r_repo": r_repo,
                    "r_pkg_dir": r_pkg_dir,
                },
            )
            ui.success("R integration installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("dataiku")
@click.option("--context", "context_name", default=None, help="Context name (or 'local'). Uses current context if omitted.")
@click.option("--version", required=True, help="Dataiku DSS version to install (e.g. 13.5.4).")
@click.option(
    "--node-type",
    type=click.Choice(["design", "automation", "govern"], case_sensitive=False),
    metavar="TYPE",
    default="design",
    show_default=True,
    help="DSS node type (design|automation|govern).",
)
@click.option("--os-user", default="dataiku", show_default=True)
@click.option("--os-group", default="dataiku", show_default=True)
@click.option("--root-dir", default="/data/dataiku", show_default=True, help="")
@click.option("--install-dir-name", "install_dir_name", default="INSTALL_DIR", show_default=True, help="")
@click.option("--data-dir-name", "data_dir_name", default="DATA_DIR", show_default=True, help="")
@click.option("--port", "dss_port", default=10000, show_default=True, type=int)
@click.option("--archive-path", type=click.Path(exists=True, dir_okay=False), help="Local tar.gz for offline install (otherwise downloaded from Dataiku CDN).")
@click.option("--with-docker/--no-docker", "install_docker", default=False, show_default=True, help="Install Docker Engine alongside DSS.")
@click.option("--docker-data-root", default=None, help="Optional Docker data-root path (e.g. /data/docker).")
@click.option("--with-r/--no-r", "install_r", default=False, show_default=True, help="Install R integration alongside DSS.")
@click.option("--r-repo", default=None, help="CRAN mirror URL for R packages (proxy/mirror environments).")
@click.option("--r-pkg-dir", type=click.Path(exists=True, file_okay=False), default=None, help="Local directory with pre-downloaded R packages (air-gapped).")
@click.option("--with-kubectl/--no-kubectl", "install_kubectl", default=True, show_default=True, help="Install kubectl on the target machine.")
@click.option(
    "--with-docker-group/--no-docker-group",
    "add_to_docker_group",
    default=None,
    help=(
        "Add the DSS OS user to the docker group. "
        "Defaults to true when --with-docker is set, false otherwise. "
        "Use --with-docker-group to add the user even when Docker is pre-installed."
    ),
)
@click.option("--with-jdbc/--no-jdbc", "with_jdbc", default=False, show_default=True, help="Install JDBC driver support (PostgreSQL JDBC for Govern).")
@click.option("--govern-jdbc-url", default=None, help="(govern) PostgreSQL JDBC URL (psql.jdbc.url in dip.properties).")
@click.option("--govern-jdbc-user", default=None, help="(govern) PostgreSQL username (psql.jdbc.user).")
@click.option("--govern-jdbc-password", default=None, help="(govern) PostgreSQL password (discouraged; prefer --ask-govern-jdbc-password).")
@click.option("--ask-govern-jdbc-password", is_flag=True, help="(govern) Prompt for PostgreSQL password (not stored locally).")
@click.option("--no-govern-init-db", "govern_init_db", flag_value=False, default=True, help="(govern) Skip govern-admin init-db bootstrap.")
@click.option("--no-encrypt-govern-password", "encrypt_govern_password", flag_value=False, default=True, help="(govern) Store DB password in cleartext instead of encrypting it.")
@click.option("--force", is_flag=True, help="Reinstall even if DSS appears already installed.")
@click.option("--os-user-password", default=None, help="OS user password (discouraged; use --ask-os-user-password).")
@click.option("--ask-os-user-password", is_flag=True, help="Prompt for OS user password (not stored).")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def dataiku(
    ctx,
    context_name,
    version,
    node_type,
    os_user,
    os_group,
    root_dir,
    install_dir_name,
    data_dir_name,
    dss_port,
    archive_path,
    install_docker,
    docker_data_root,
    install_r,
    r_repo,
    r_pkg_dir,
    install_kubectl,
    add_to_docker_group,
    with_jdbc,
    govern_jdbc_url,
    govern_jdbc_user,
    govern_jdbc_password,
    ask_govern_jdbc_password,
    govern_init_db,
    encrypt_govern_password,
    force,
    os_user_password,
    ask_os_user_password,
    dry_run,
    quiet,
    verbose,
):
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_target(state, context_name)

        if os_user_password and ask_os_user_password:
            raise ValueError("Use either --os-user-password or --ask-os-user-password, not both.")

        if govern_jdbc_password and ask_govern_jdbc_password:
            raise ValueError("Use either --govern-jdbc-password or --ask-govern-jdbc-password, not both.")

        # Prompt for password before anything else (never logged).
        if ask_os_user_password:
            os_user_password = click.prompt(
                f"Password for OS user '{os_user}'",
                hide_input=True,
                confirmation_prompt=True,
            )

        if ask_govern_jdbc_password:
            govern_jdbc_password = click.prompt(
                "PostgreSQL password for Govern",
                hide_input=True,
                confirmation_prompt=True,
            )

        # If the user didn't explicitly pass --with-docker-group / --no-docker-group,
        # default to adding the user to the docker group only when Docker is also
        # being installed by k4s (--with-docker). This avoids a spurious step when
        # the user doesn't want Docker at all.
        if add_to_docker_group is None:
            add_to_docker_group = install_docker

        root_dir = root_dir.rstrip("/")
        install_dir = f"{root_dir}/{install_dir_name}".rstrip("/")
        data_dir = f"{root_dir}/{data_dir_name}".rstrip("/")

        plan = DataikuInstallPlan(
            version=version,
            os_user=os_user,
            os_group=os_group,
            root_dir=root_dir,
            install_dir=install_dir,
            data_dir=data_dir,
            dss_port=dss_port,
            node_type=node_type.lower(),
            archive_path=archive_path,
            install_docker=install_docker,
            docker_data_root=docker_data_root,
            install_r=install_r,
            r_repo=r_repo,
            r_pkg_dir=r_pkg_dir,
            os_user_password=os_user_password,
            install_kubectl=install_kubectl,
            add_to_docker_group=add_to_docker_group,
            with_jdbc=with_jdbc,
            govern_jdbc_url=govern_jdbc_url,
            govern_jdbc_user=govern_jdbc_user,
            govern_jdbc_password=govern_jdbc_password,
            govern_init_db=govern_init_db,
            encrypt_govern_password=encrypt_govern_password,
        )

        ex = Executor(c.to_server_config())
        if dry_run:
            # Dry-run: connect and run read-only preflight, then print the plan.
            with ex:
                with ui.step("Preflight (Dataiku)"):
                    run_preflight(ui, ex, plan)
            steps = build_install_steps(ui, ex, plan, force=force)
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                steps = build_install_steps(ui, ex, plan, force=force)
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="dataiku",
                context=c.name,
                host=c.host,
                params={
                    "version": plan.version,
                    "node_type": plan.node_type,
                    "os_user": plan.os_user,
                    "data_dir": plan.data_dir,
                    "port": plan.dss_port,
                    "r_integration": plan.install_r,
                },
            )
            ui.success("Dataiku installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("nexus")
@click.option("--context", "context_name", default=None, help="Context name (or 'local'). Uses current context if omitted.")
@click.option("--version", default="3.89.1-02", show_default=True, help="Nexus Repository Manager version.")
@click.option("--install-dir", default="/opt/nexus", show_default=True, help="Nexus installation directory.")
@click.option("--data-dir", default="/opt/sonatype-work", show_default=True, help="Nexus data directory (karaf.data).")
@click.option("--os-user", default="nexus", show_default=True, help="OS user/group used to run Nexus.")
@click.option("--java-package", default="openjdk-11-jre-headless", show_default=True, help="Java package to install via apt.")
@click.option("--port", "http_port", default=8081, show_default=True, type=int, help="Nexus HTTP port.")
@click.option("--with-docker-repo/--no-docker-repo", "docker_repo_enabled", default=False, show_default=True, help="Create a Docker hosted repo.")
@click.option("--docker-repo-name", default="dataiku", show_default=True, help="Docker hosted repo name (used when --with-docker-repo is set).")
@click.option(
    "--docker-repo-routing",
    type=click.Choice(["path", "connector"], case_sensitive=False),
    default="path",
    show_default=True,
    help="Docker routing mode: 'path' (path-based routing) or 'connector' (legacy port connector).",
)
@click.option("--verify-checksum/--no-verify-checksum", default=True, show_default=True, help="Verify downloaded archive SHA256.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def nexus(
    ctx,
    context_name,
    version,
    install_dir,
    data_dir,
    os_user,
    java_package,
    http_port,
    docker_repo_enabled,
    docker_repo_name,
    docker_repo_routing,
    verify_checksum,
    dry_run,
    quiet,
    verbose,
):
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_target(state, context_name)

        plan = NexusInstallPlan(
            version=version,
            install_dir=install_dir,
            data_dir=data_dir,
            os_user=os_user,
            java_package=java_package,
            http_port=http_port,
            verify_checksum=verify_checksum,
            docker_repo_enabled=docker_repo_enabled,
            docker_repo_name=docker_repo_name,
            docker_repo_routing=docker_repo_routing,
        )

        ex = Executor(c.to_server_config())
        if dry_run:
            with ex:
                with ui.step("Preflight (Nexus)"):
                    nexus_run_preflight(ui, ex, plan)
            steps = build_nexus_steps(ui, ex, plan)
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                steps = build_nexus_steps(ui, ex, plan)
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="nexus",
                context=c.name,
                host=c.host,
                params={
                    "version": plan.version,
                    "install_dir": plan.install_dir,
                    "data_dir": plan.data_dir,
                    "os_user": plan.os_user,
                    "port": plan.http_port,
                },
            )
            ui.success("Nexus installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("ingress-nginx")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="ingress-nginx", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'ingress-nginx').")
@click.option("--chart-version", default=None, help="Optional Helm chart version to pin.")
@click.option(
    "--values-file",
    "values_files",
    type=click.Path(exists=True, dir_okay=False),
    multiple=True,
    help="Helm values YAML file (repeatable).",
)
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="10m", show_default=True, help="Rollout timeout.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def ingress_nginx(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    values_files,
    set_values,
    timeout,
    dry_run,
    quiet,
    verbose,
):
    """Install ingress-nginx controller via Helm (requires a k8s context)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "ingress-nginx"

        plan = IngressNginxInstallPlan(
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            chart_version=chart_version,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            timeout=timeout,
        )

        ex = Executor()  # local execution (helm/kubectl run locally)
        steps = build_ingress_nginx_steps(ui, ex, plan)
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="ingress-nginx",
                context=c.name,
                params={
                    "namespace": ns,
                    "chart_version": chart_version,
                    "release": name,
                },
            )
            ui.success("ingress-nginx installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("hive")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="hive", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'sep').")
@click.option("--chart-version", required=True, help="Helm chart version to install (e.g. 453.0.0).")
@click.option("--repo-username", default=None, help="Starburst Harbor username (or env K4S_STARBURST_REPO_USERNAME).")
@click.option("--repo-password", default=None, help="Starburst Harbor password (or env K4S_STARBURST_REPO_PASSWORD).")
@click.option("--values-file", "values_files", type=click.Path(exists=True, dir_okay=False), multiple=True, help="Helm values YAML file (repeatable).")
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="20m", show_default=True, help="Rollout timeout.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def hive(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    repo_username,
    repo_password,
    values_files,
    set_values,
    timeout,
    dry_run,
    quiet,
    verbose,
):
    """Install Starburst Hive Metastore Service via Helm (OCI chart)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "sep"

        plan = StarburstOciComponentInstallPlan(
            component="hive",
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            oci_chart_ref="oci://harbor.starburstdata.net/starburstdata/charts/starburst-hive",
            chart_version=chart_version,
            repo_username=repo_username,
            repo_password=repo_password,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            timeout=timeout,
        )
        plan = starburst_oci_plan_from_env(plan)

        ex = Executor()
        steps = build_starburst_oci_install_steps(ui, ex, plan)
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="hive",
                context=c.name,
                params={"namespace": ns, "chart_version": chart_version, "release": name},
            )
            ui.success("Hive installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("ranger")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="ranger", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'sep').")
@click.option("--chart-version", required=True, help="Helm chart version to install (e.g. 453.0.0).")
@click.option("--repo-username", default=None, help="Starburst Harbor username (or env K4S_STARBURST_REPO_USERNAME).")
@click.option("--repo-password", default=None, help="Starburst Harbor password (or env K4S_STARBURST_REPO_PASSWORD).")
@click.option("--values-file", "values_files", type=click.Path(exists=True, dir_okay=False), multiple=True, help="Helm values YAML file (repeatable).")
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="20m", show_default=True, help="Rollout timeout.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def ranger(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    repo_username,
    repo_password,
    values_files,
    set_values,
    timeout,
    dry_run,
    quiet,
    verbose,
):
    """Install Starburst Ranger via Helm (OCI chart)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "sep"

        plan = StarburstOciComponentInstallPlan(
            component="ranger",
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            oci_chart_ref="oci://harbor.starburstdata.net/starburstdata/charts/starburst-ranger",
            chart_version=chart_version,
            repo_username=repo_username,
            repo_password=repo_password,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            timeout=timeout,
        )
        plan = starburst_oci_plan_from_env(plan)

        ex = Executor()
        steps = build_starburst_oci_install_steps(ui, ex, plan)
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="ranger",
                context=c.name,
                params={"namespace": ns, "chart_version": chart_version, "release": name},
            )
            ui.success("Ranger installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("cache")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="cache", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'sep').")
@click.option("--chart-version", required=True, help="Helm chart version to install (e.g. 453.0.0).")
@click.option("--repo-username", default=None, help="Starburst Harbor username (or env K4S_STARBURST_REPO_USERNAME).")
@click.option("--repo-password", default=None, help="Starburst Harbor password (or env K4S_STARBURST_REPO_PASSWORD).")
@click.option("--values-file", "values_files", type=click.Path(exists=True, dir_okay=False), multiple=True, help="Helm values YAML file (repeatable).")
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="20m", show_default=True, help="Rollout timeout.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def cache(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    repo_username,
    repo_password,
    values_files,
    set_values,
    timeout,
    dry_run,
    quiet,
    verbose,
):
    """Install Starburst Cache Service via Helm (OCI chart)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "sep"

        plan = StarburstOciComponentInstallPlan(
            component="cache",
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            oci_chart_ref="oci://harbor.starburstdata.net/starburstdata/charts/starburst-cache-service",
            chart_version=chart_version,
            repo_username=repo_username,
            repo_password=repo_password,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            timeout=timeout,
        )
        plan = starburst_oci_plan_from_env(plan)

        ex = Executor()
        steps = build_starburst_oci_install_steps(ui, ex, plan)
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="cache",
                context=c.name,
                params={"namespace": ns, "chart_version": chart_version, "release": name},
            )
            ui.success("Cache service installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("starburst")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="sep", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'sep').")
@click.option("--chart-version", required=True, help="Helm chart version to install (e.g. 453.0.0).")
@click.option("--repo-username", default=None, help="Starburst Harbor username (or env K4S_STARBURST_REPO_USERNAME).")
@click.option("--repo-password", default=None, help="Starburst Harbor password (or env K4S_STARBURST_REPO_PASSWORD).")
@click.option("--values-file", "values_files", type=click.Path(exists=True, dir_okay=False), multiple=True, help="Helm values YAML file (repeatable).")
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="20m", show_default=True, help="Rollout timeout.")
@click.option("--environment", default=None, help="SEP environment (sets .environment).")
@click.option("--shared-secret", default=None, help="SEP shared secret (sets .sharedSecret).")
@click.option("--license-file", type=click.Path(exists=True, dir_okay=False), default=None, help="Path to starburstdata.license file (k4s creates secret).")
@click.option("--license-secret-name", default=None, help="Existing or desired license secret name (default: starburst-license).")
@click.option("--registry-username", default=None, help="Harbor registry username (or env K4S_STARBURST_REGISTRY_USERNAME).")
@click.option("--registry-password", default=None, help="Harbor registry password (or env K4S_STARBURST_REGISTRY_PASSWORD).")
@click.option("--with-ingress-nginx", is_flag=True, default=False, help="Install ingress-nginx before installing Starburst.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def starburst(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    repo_username,
    repo_password,
    values_files,
    set_values,
    timeout,
    environment,
    shared_secret,
    license_file,
    license_secret_name,
    registry_username,
    registry_password,
    with_ingress_nginx,
    dry_run,
    quiet,
    verbose,
):
    """Install Starburst Enterprise via Helm (requires a k8s context)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "sep"

        plan = StarburstInstallPlan(
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            chart_version=chart_version,
            repo_username=repo_username,
            repo_password=repo_password,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            environment=environment,
            shared_secret=shared_secret,
            license_file=license_file,
            license_secret_name=license_secret_name,
            registry_username=registry_username,
            registry_password=registry_password,
            timeout=timeout,
        )
        plan = starburst_plan_from_env(plan)

        ex = Executor()  # local execution (helm/kubectl run locally)
        steps = build_starburst_install_steps(ui, ex, plan)
        if with_ingress_nginx:
            ingress_plan = IngressNginxInstallPlan(
                release_name="ingress-nginx",
                namespace="ingress-nginx",
                kubeconfig_path=kubeconfig,
                kubectl_context=c.kubectl_context,
            )
            steps = build_ingress_nginx_steps(ui, ex, ingress_plan) + steps

        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="starburst",
                context=c.name,
                params={
                    "namespace": ns,
                    "chart_version": chart_version,
                    "release": name,
                },
            )
            ui.success("Starburst installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)


@install.command("datafloem")
@click.option("--context", "context_name", default=None, help="K8s context name. Uses current context if omitted.")
@click.option("--name", default="dfl", show_default=True, help="Helm release name.")
@click.option("--namespace", default=None, help="Kubernetes namespace (defaults to context namespace, then 'datafloem').")
@click.option("--chart-version", required=True, help="Helm chart version to install.")
@click.option("--repo-username", default=None, help="Datafloem Helm repo username (or env K4S_DATAFLOEM_REPO_USERNAME).")
@click.option("--repo-password", default=None, help="Datafloem Helm repo password (or env K4S_DATAFLOEM_REPO_PASSWORD).")
@click.option("--registry-server", default="docker.repo.datafloem.com", show_default=True, help="Docker registry server for Datafloem images.")
@click.option("--registry-username", default=None, help="Docker registry username (or env K4S_DATAFLOEM_REGISTRY_USERNAME).")
@click.option("--registry-password", default=None, help="Docker registry password (or env K4S_DATAFLOEM_REGISTRY_PASSWORD).")
@click.option("--image-pull-secret-name", default="dflregcred", show_default=True, help="Kubernetes imagePullSecret name to use/create.")
@click.option("--jwt-secret", default=None, help="JWT secret value for backend auth (or env K4S_DATAFLOEM_JWT_SECRET).")
@click.option("--license-file", default=None, type=click.Path(exists=True, dir_okay=False), help="Path to Datafloem license file.")
@click.option("--license-secret-name", default="dfl-license", show_default=True, help="Kubernetes secret name for the license.")
@click.option("--values-file", "values_files", type=click.Path(exists=True, dir_okay=False), multiple=True, help="Helm values YAML file (repeatable).")
@click.option("--set", "set_values", multiple=True, help="Helm --set overrides (repeatable).")
@click.option("--timeout", default="20m", show_default=True, help="Rollout timeout.")
@click.option("--with-ingress-nginx", is_flag=True, default=False, help="Install ingress-nginx before installing Datafloem.")
@click.option("--dry-run", is_flag=True, help="Print the plan without applying changes.")
@click.option("-q", "--quiet", is_flag=True, help="Only print final results and errors.")
@click.option("-v", "--verbose", count=True, help="Increase verbosity (-v logs, -vv debug).")
@click.pass_context
def datafloem(
    ctx,
    context_name,
    name,
    namespace,
    chart_version,
    repo_username,
    repo_password,
    registry_server,
    registry_username,
    registry_password,
    image_pull_secret_name,
    jwt_secret,
    license_file,
    license_secret_name,
    values_files,
    set_values,
    timeout,
    with_ingress_nginx,
    dry_run,
    quiet,
    verbose,
):
    """Install Datafloem via Helm (requires a k8s context)."""
    state: CliState = ctx.obj["state"]
    apply_command_ui_overrides(ctx, state, quiet=quiet, verbose=verbose)
    ui = state.ui

    try:
        c = resolve_k8s_target(state, context_name)
        kubeconfig = kubeconfig_path_for(c)
        ns = namespace or c.namespace or "datafloem"

        plan = DatafloemInstallPlan(
            release_name=name,
            namespace=ns,
            kubeconfig_path=kubeconfig,
            kubectl_context=c.kubectl_context,
            chart_version=chart_version,
            repo_username=repo_username,
            repo_password=repo_password,
            registry_server=registry_server,
            registry_username=registry_username,
            registry_password=registry_password,
            image_pull_secret_name=image_pull_secret_name,
            jwt_secret=jwt_secret,
            license_file=license_file,
            license_secret_name=license_secret_name,
            values_files=list(values_files) if values_files else None,
            set_values=list(set_values) if set_values else None,
            timeout=timeout,
        )
        plan = datafloem_plan_from_env(plan)

        ex = Executor()
        steps = build_datafloem_install_steps(ui, ex, plan)
        if with_ingress_nginx:
            ingress_plan = IngressNginxInstallPlan(
                release_name="ingress-nginx",
                namespace="ingress-nginx",
                kubeconfig_path=kubeconfig,
                kubectl_context=c.kubectl_context,
            )
            steps = build_ingress_nginx_steps(ui, ex, ingress_plan) + steps
        if dry_run:
            run_steps(ui, steps, dry_run=True)
        else:
            with ex:
                run_steps(ui, steps, dry_run=False)

        if not dry_run:
            state.history.append(
                action="install",
                product="datafloem",
                context=c.name,
                params={"namespace": ns, "chart_version": chart_version, "release": name},
            )
            ui.success("Datafloem installed successfully.")
    except Exception as e:
        exit_with_error(ctx, ui, e, code=1)

