"""Compose deploy + stabilization orchestration.

Extracted from `src/armorer/installers/dynamic.py` as part of the Phase 5
modularization. This keeps the dynamic installer monolith smaller while
preserving the existing runtime behavior.
"""

from __future__ import annotations

import time
from typing import Any, Callable

import typer

from armorer.installers.dynamic.streaming_ui import _LiveLogPanel
from armorer.catalog import resolve_playbook


def start_and_stabilize_impl(
    installer: Any,
    max_attempts: int = 10,
    *,
    console: Any,
    step_done: Callable[[str, str], None],
    step_fail: Callable[[str, str], None],
    looks_like_host_disk_full: Callable[[str], bool],
    render_host_disk_full_panel: Callable[[str, str], None],
    looks_like_docker_storage_io_error: Callable[[str], bool],
    render_docker_storage_fault_panel: Callable[[str, str], None],
    looks_like_docker_desktop_mount_denied: Callable[[str], bool],
    render_mount_denied_panel: Callable[[str, str], None],
) -> None:
    """Implementation of `DynamicInstaller._start_and_stabilize`."""
    known_spec = installer._active_playbook or resolve_playbook(installer.app_name)
    if known_spec and not (known_spec.startup_services or []):
        console.print(
            "  [dim]No startup services declared in playbook; skipping background stabilization.[/dim]"
        )
        return

    label = f"Deploying {installer.app_name}"
    history: list[dict] = []

    if installer._is_profile_only_compose():
        console.print("  [dim]Profile-only compose detected; skipping background stabilization.[/dim]")
        return

    def ignorable_exit(container: dict) -> bool:
        service = str(container.get("Service", "")).lower()
        name = str(container.get("Name", "")).lower()
        return "cli" in service or "cli" in name

    live_panel = _LiveLogPanel(
        "Armorer Live",
        enabled=bool(installer.options.get("live_ui", True)),
        console=console,
    )
    live_panel.start()
    last_config_error_sig = ""
    repeated_config_error_count = 0

    try:
        for attempt in range(1, max_attempts + 1):
            runtime_env = installer._compose_runtime_env()
            installer._image_policy_failures = []
            installer._harden_all_compose_files()
            installer._assert_image_policy()
            installer._ensure_local_compose_images(runtime_env)
            live_panel.set_header("Validating compose")

            # 0. Pre-Flight Config Check (Catch syntax/variable errors early)
            from armorer.docker_runner import compose_config

            config_check = compose_config(
                cwd=installer._docker_compose_cwd,
                env=runtime_env,
            )
            if config_check.returncode != 0:
                err_sig = ((config_check.stderr or "") + "\n" + (config_check.stdout or "")).strip()
                if err_sig == last_config_error_sig:
                    repeated_config_error_count += 1
                else:
                    last_config_error_sig = err_sig
                    repeated_config_error_count = 1

                console.print("  [yellow]⚠  Configuration invalid. Armorer is fixing...[/yellow]")
                live_panel.add_line("Configuration invalid, running auto-repair.")

                if repeated_config_error_count >= 4:
                    console.print(
                        "  [red]✗ Configuration stayed invalid after repeated repair attempts.[/red]"
                    )
                    if err_sig:
                        console.print(f"  [dim]{err_sig.splitlines()[0]}[/dim]")
                    raise typer.Exit(1)

                installer._diagnose_and_repair(
                    f"CRITICAL: 'docker compose config' failed. You MUST fix the YAML syntax or variables.\n\nERROR:\n{config_check.stderr}\n{config_check.stdout}",
                    history,
                )
                continue
            else:
                last_config_error_sig = ""
                repeated_config_error_count = 0

            # 1. Attempt Deployment
            console.print(f"\n[bold cyan]  ⠿ {label} — Launching...[/bold cyan]")
            live_panel.set_header("Launching containers")

            from armorer.docker_runner import compose_up_streaming

            process = compose_up_streaming(
                cwd=installer._docker_compose_cwd,
                services=known_spec.startup_services if known_spec and known_spec.startup_services else None,
                env=runtime_env,
                remove_orphans=True,
                detach=True,
            )

            output_buffer: list[str] = []
            for line in process.stdout:
                line_text = line.strip()
                output_buffer.append(line_text)
                if live_panel.enabled:
                    live_panel.add_line(line_text)
                else:
                    console.print(f"    [dim]{line_text}[/dim]")

            process.wait()

            if process.returncode != 0:
                full_output = "\n".join(output_buffer)
                combined_output = "\n".join(output_buffer).lower()

                if looks_like_host_disk_full(combined_output):
                    render_host_disk_full_panel(
                        f"{label} (docker compose up)",
                        "\n".join(output_buffer),
                    )
                    raise typer.Exit(1)
                if looks_like_docker_storage_io_error(combined_output):
                    render_docker_storage_fault_panel(
                        f"{label} (docker compose up)",
                        "\n".join(output_buffer),
                    )
                    raise typer.Exit(1)
                if looks_like_docker_desktop_mount_denied(full_output):
                    render_mount_denied_panel(
                        f"{label} (docker compose up)",
                        full_output,
                    )
                    raise typer.Exit(1)

                if "no service selected" in combined_output:
                    profiles = installer._collect_compose_profiles()
                    if profiles:
                        console.print(
                            f"  [dim]No default services. Retrying with profiles: {', '.join(profiles)}[/dim]"
                        )
                        from armorer.docker_runner import compose_up

                        prof_res = compose_up(
                            cwd=installer._docker_compose_cwd,
                            profiles=profiles,
                            services=known_spec.startup_services
                            if known_spec and known_spec.startup_services
                            else None,
                            env=runtime_env,
                        )
                        if prof_res.returncode == 0:
                            console.print("  [green]✓ Started stack using compose profiles.[/green]")
                            continue

                from armorer.docker_runner import compose_up as _compose_up

                full_capture = _compose_up(cwd=installer._docker_compose_cwd, env=runtime_env)
                error_context = (
                    (full_capture.stderr or "")
                    + "\n"
                    + (full_capture.stdout or "")
                    + "\n"
                    + "\n".join(output_buffer)
                )

                if looks_like_host_disk_full(error_context):
                    render_host_disk_full_panel(
                        f"{label} (docker compose up)",
                        error_context,
                    )
                    raise typer.Exit(1)
                if looks_like_docker_storage_io_error(error_context):
                    render_docker_storage_fault_panel(
                        f"{label} (docker compose up)",
                        error_context,
                    )
                    raise typer.Exit(1)
                if looks_like_docker_desktop_mount_denied(error_context):
                    render_mount_denied_panel(
                        f"{label} (docker compose up)",
                        error_context,
                    )
                    raise typer.Exit(1)

                console.print(f"  [yellow]⚠  {label} — Deployment failed. Investigating...[/yellow]")
                live_panel.add_line("Deployment failed, invoking repair agent.")
                installer._diagnose_and_repair(error_context, history)
                continue

            # 2. Stabilization Check
            stable = False
            start_time = time.time()
            live_panel.set_header("Verifying stability")

            if live_panel.enabled:
                while time.time() - start_time < 30:
                    containers = installer._get_containers()
                    failed = [c for c in containers if c.get("ExitCode", 0) != 0 and not ignorable_exit(c)]
                    if failed:
                        failed_name = failed[0].get("Name", "unknown")
                        live_panel.add_line(f"Crash detected in {failed_name}.")
                        from armorer.docker_runner import docker_logs

                        lr = docker_logs(failed_name, tail=100)
                        logs = (lr.stderr or "") + "\n" + (lr.stdout or "")
                        installer._diagnose_and_repair(
                            f"CONTAINER CRASH: {failed_name}\nLOGS:\n{logs}",
                            history,
                        )
                        break
                    time.sleep(2)
                else:
                    stable = True
            else:
                with console.status(f"[bold cyan]  ⠿ {label} — Verifying stability...[/bold cyan]") as status:
                    while time.time() - start_time < 30:
                        containers = installer._get_containers()
                        failed = [
                            c for c in containers if c.get("ExitCode", 0) != 0 and not ignorable_exit(c)
                        ]
                        if failed:
                            status.update("[bold red]  ! Crash detected![/bold red]")
                            failed_name = failed[0].get("Name", "unknown")
                            from armorer.docker_runner import docker_logs

                            lr = docker_logs(failed_name, tail=100)
                            logs = (lr.stderr or "") + "\n" + (lr.stdout or "")
                            installer._diagnose_and_repair(
                                f"CONTAINER CRASH: {failed_name}\nLOGS:\n{logs}",
                                history,
                            )
                            break
                        time.sleep(2)
                    else:
                        stable = True

            if stable:
                if known_spec and str(getattr(known_spec, "id", "")).strip().lower() == "openclaw":
                    installer._run_openclaw_post_start_security_gate()
                step_done(label, "System is stable")
                live_panel.set_header("System is stable")
                return

        step_fail(label, f"Could not stabilize {installer.app_name}.")
        live_panel.set_header("Failed to stabilize")
        raise typer.Exit(1)
    finally:
        live_panel.stop()
