"""``ilum upgrade`` command."""

from __future__ import annotations

from pathlib import Path

import typer

import ilum.cli.output as output_mod
from ilum.cli.defaults import resolve_profile_defaults
from ilum.config.manager import ConfigManager
from ilum.config.paths import IlumPaths
from ilum.constants import DEFAULT_CHART_REF, is_local_chart
from ilum.core.helm import HelmClient
from ilum.core.kubernetes import KubeClient
from ilum.core.modules import ModuleResolver
from ilum.core.release import ReleaseManager
from ilum.errors import IlumError
from ilum.wizard.deps import ensure_tools


def _build_manager(
    context: str,
    namespace: str,
    dry_run: bool,
    timeout: str,
) -> ReleaseManager:
    paths = IlumPaths.default()
    paths.ensure_dirs()
    return ReleaseManager(
        helm=HelmClient(
            kubecontext=context,
            namespace=namespace,
            dry_run=dry_run,
            timeout=timeout,
        ),
        k8s=KubeClient(kubecontext=context),
        resolver=ModuleResolver(),
        config_mgr=ConfigManager(paths),
        paths=paths,
    )


def upgrade(
    release: str | None = typer.Option(
        None, "--release", "-r", show_default="from profile", help="Helm release name."
    ),
    namespace: str | None = typer.Option(
        None, "--namespace", "-n", show_default="from profile", help="Kubernetes namespace."
    ),
    context: str | None = typer.Option(
        None, "--context", show_default="from profile", help="Kubernetes context."
    ),
    version: str = typer.Option("", "--version", help="Chart version to upgrade to."),
    values: list[Path] | None = typer.Option(  # noqa: B008
        None,
        "--values",
        "-f",
        help="Values file (repeatable).",
    ),
    set_flags: list[str] | None = typer.Option(  # noqa: B008
        None,
        "--set",
        help="Helm --set flag (repeatable).",
    ),
    module: list[str] | None = typer.Option(  # noqa: B008
        None,
        "--module",
        "-m",
        help="Module to enable (repeatable).",
    ),
    atomic: bool = typer.Option(False, help="Use --atomic for Helm upgrade."),
    chart: str = typer.Option(
        "", "--chart", help="Chart reference or local path (default: ilum/ilum)."
    ),
    devel: bool = typer.Option(False, "--devel", help="Include pre-release chart versions."),
    reset_defaults: bool = typer.Option(
        False,
        "--reset-defaults",
        help="Reset to new chart defaults before applying user values."
        " Use when upgrading across chart versions with breaking changes.",
    ),
    force_rollback: bool = typer.Option(
        False,
        "--force-rollback",
        help="Rollback a stuck release before upgrading.",
    ),
    yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
    dry_run: bool = typer.Option(False, "--dry-run", help="Preview without executing."),
    timeout: str = typer.Option("10m", "--timeout", help="Helm timeout."),
) -> None:
    """Upgrade an existing Ilum installation."""
    release, namespace, context = resolve_profile_defaults(release, namespace, context)
    console = output_mod.console

    try:
        # Validate values files exist
        for vf in values or []:
            if not vf.exists():
                from ilum.errors import ConfigError

                raise ConfigError(
                    f"Values file not found: {vf}",
                    suggestion="Check the file path and try again.",
                    error_code="ILUM-030",
                )

        ensure_tools(["helm"], console, interactive=not yes)
        mgr = _build_manager(context, namespace, dry_run, timeout)
        chart_ref = chart or DEFAULT_CHART_REF
        if not is_local_chart(chart_ref):
            mgr.ensure_repo()

        # Handle stuck release
        if mgr.is_stuck(release):
            if force_rollback:
                console.warning(f"Release '{release}' is stuck — rolling back.")
                with console.status_spinner("Rolling back..."):
                    mgr.rollback_if_stuck(release)
                console.success("Rollback complete.")
            else:
                console.error(f"Release '{release}' is stuck. Use --force-rollback to recover.")
                raise typer.Exit(code=1)

        cli_modules = list(module or []) or None
        # Load modules from profile so that profile changes (e.g. via
        # ``ilum init`` with a different preset) are picked up.
        modules = cli_modules or mgr.get_enabled_modules(release=release) or None

        plan = mgr.plan_upgrade(
            release=release,
            chart=chart_ref,
            modules=modules,
            values_files=values,
            set_flags=set_flags,
            version=version,
            atomic=atomic,
            devel=devel,
            reset_defaults=reset_defaults,
        )

        # Resolve versions
        try:
            info = mgr.get_release_info(release)
            current_ver = info.chart_version
        except IlumError:
            current_ver = "unknown"

        target_ver = version or (mgr.resolve_latest_version(chart_ref, devel=devel) or current_ver)

        # Check for breaking changes even when --version is not explicit
        if not version and current_ver != target_ver:
            from ilum.core.release import ReleaseManager

            plan.warnings.extend(ReleaseManager._check_breaking_changes(current_ver, target_ver))

        # Detect active modules from live computed values
        try:
            computed_vals = mgr.fetch_computed_values(release)
            active_modules = mgr.resolver.detect_enabled_modules(computed_vals)
        except IlumError:
            active_modules = []

        has_value_changes = plan.effective_diff and not plan.effective_diff.is_empty
        same_version = current_ver == target_ver

        # If nothing would change, tell the user and exit early.
        # When modules were explicitly passed via --module we proceed anyway
        # because the user signalled intent even if the diff happens to be empty.
        if same_version and not has_value_changes and not cli_modules:
            console.success(
                f"Already on version {current_ver} with no value changes. Nothing to upgrade."
            )
            console.info("Upgrade notes: https://ilum.cloud/docs/upgrade-notes/")
            return

        # Show upgrade summary
        version_str = f"{current_ver} \u2192 {target_ver}"

        summary_rows = [
            ["Release", release],
            ["Namespace", namespace],
            ["Chart", chart_ref],
            ["Version", version_str],
        ]
        if active_modules:
            summary_rows.append(["Modules", ", ".join(sorted(active_modules))])
        summary_rows.append(["Atomic", str(atomic)])
        console.operation_summary(summary_rows, title="Upgrade Summary")

        # Show drift warning
        if plan.drift and plan.drift.has_drift and plan.drift.diff:
            console.warning("External changes detected since last CLI operation:")
            console.diff_table(plan.drift.diff, title="External Changes (Drift)")
            if not yes:
                console.info(
                    "These external changes will be preserved. CLI changes will be applied on top."
                )

        # Show values diff
        if has_value_changes:
            console.diff_table(plan.effective_diff)
        else:
            console.info("No value changes.")

        # Show breaking-change warnings
        for w in plan.warnings:
            console.warning(w)

        console.info("Upgrade notes: https://ilum.cloud/docs/upgrade-notes/")
        console.command_preview(mgr.preview_command(plan))

        if dry_run:
            console.info("Dry-run mode — no changes applied.")
            return

        if not yes and not console.confirm("Proceed with upgrade?"):
            console.info("Aborted.")
            raise typer.Exit()

        from ilum.cli.progress import execute_with_progress

        execute_with_progress(mgr, plan, console, message="Upgrading Ilum...")

        if modules:
            current = mgr.get_enabled_modules(release=release)
            combined = list(dict.fromkeys(current + modules))
            mgr.save_enabled_modules(combined, release=release)

        console.success(f"Ilum upgraded successfully (release: {release}).")

    except IlumError as exc:
        console.handle_error(exc)
        if not reset_defaults and _is_values_coalesce_error(str(exc)):
            console.info(
                "Hint: This may be caused by stale chart defaults."
                " Retry with --reset-defaults to start from new chart defaults."
            )
        raise typer.Exit(code=1) from exc


def _is_values_coalesce_error(msg: str) -> bool:
    """Return True if *msg* looks like a Helm values schema/coalesce failure."""
    indicators = [
        "cannot overwrite table with non table",
        "cannot overwrite non table with table",
        "PASSWORDS ERROR",
        "coalesce.go",
    ]
    msg_lower = msg.lower()
    return any(ind.lower() in msg_lower for ind in indicators)
