"""Hetzner Cloud ephemeral CI runner management.

Manages the lifecycle of Hetzner Cloud VMs used as ephemeral
GitHub Actions self-hosted runners: create → run job → destroy.

Requires:
- ``hcloud`` CLI installed and on PATH
- ``HCLOUD_TOKEN`` environment variable set
- A GitHub personal access token (for runner registration)
"""

from __future__ import annotations

import json
import secrets
import subprocess
import time
from dataclasses import dataclass, field
from enum import StrEnum
from pathlib import Path
from typing import Any

from rich.console import Console
from rich.table import Table

console = Console()

# Cloud-init template that bootstraps the GitHub Actions runner
_RUNNER_CLOUD_INIT = """\
#cloud-config
package_update: true
packages:
  - curl
  - jq
  - docker.io

runcmd:
  # Add runner user
  - useradd -m -s /bin/bash runner
  - usermod -aG docker runner

  # Install GitHub Actions runner
  - mkdir -p /home/runner/actions-runner
  - cd /home/runner/actions-runner
  - 'curl -sL "https://github.com/actions/runner/releases/download/v{runner_version}/actions-runner-linux-{arch}-{runner_version}.tar.gz" | tar xz -C /home/runner/actions-runner'
  - chown -R runner:runner /home/runner/actions-runner

  # Register as an ephemeral runner
  - |
    su - runner -c 'cd /home/runner/actions-runner && ./config.sh \\
      --url "https://github.com/{github_repo}" \\
      --token "{registration_token}" \\
      --name "{runner_name}" \\
      --labels "{labels}" \\
      --ephemeral \\
      --unattended'

  # Start the runner (will exit after one job)
  - su - runner -c 'cd /home/runner/actions-runner && ./run.sh'
"""

RUNNER_VERSION = "2.321.0"


class RunnerStatus(StrEnum):
    """Status of a Hetzner runner VM."""

    CREATING = "creating"
    RUNNING = "running"
    IDLE = "idle"
    OFFLINE = "offline"
    UNKNOWN = "unknown"


@dataclass
class RunnerVM:
    """Represents a Hetzner Cloud runner VM."""

    server_id: int
    name: str
    ipv4: str
    status: str
    labels: dict[str, str] = field(default_factory=dict)
    created: str = ""


@dataclass
class HetznerRunnerConfig:
    """Configuration for ephemeral Hetzner runners."""

    server_type: str = "cx22"
    location: str = "fsn1"
    image: str = "ubuntu-24.04"
    labels: dict[str, str] = field(
        default_factory=lambda: {"managed-by": "prisme", "purpose": "ci-runner"}
    )
    runner_version: str = RUNNER_VERSION
    ssh_key_name: str | None = None


class HetznerRunnerManager:
    """Manage ephemeral Hetzner Cloud VMs for GitHub Actions runners.

    Each VM is created with cloud-init that:
    1. Installs Docker and the GitHub Actions runner agent
    2. Registers as an ephemeral self-hosted runner
    3. Runs one job, then the runner exits
    4. The VM should be destroyed after the job completes

    Usage::

        manager = HetznerRunnerManager(config)
        vm = manager.create_runner(
            github_repo="owner/repo",
            registration_token="AABBC...",
            runner_labels=["self-hosted", "hetzner"],
        )
        # ... runner picks up a job and executes it ...
        manager.destroy_runner(vm.server_id)
    """

    def __init__(self, config: HetznerRunnerConfig | None = None) -> None:
        self.config = config or HetznerRunnerConfig()
        self._verify_hcloud_cli()

    def _verify_hcloud_cli(self) -> None:
        """Verify that hcloud CLI is available."""
        try:
            subprocess.run(
                ["hcloud", "version"],
                capture_output=True,
                check=True,
                timeout=10,
            )
        except FileNotFoundError:
            msg = (
                "hcloud CLI not found. Install it:\n"
                "  brew install hcloud  (macOS)\n"
                "  https://github.com/hetznercloud/cli/releases  (Linux/Windows)"
            )
            raise RuntimeError(msg) from None
        except subprocess.CalledProcessError as e:
            msg = f"hcloud CLI error: {e.stderr}"
            raise RuntimeError(msg) from e

    def _run_hcloud(self, args: list[str], *, check: bool = True) -> subprocess.CompletedProcess:
        """Run an hcloud CLI command and return the result."""
        cmd = ["hcloud", *args]
        return subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            check=check,
            timeout=120,
        )

    def _run_hcloud_json(self, args: list[str]) -> Any:
        """Run an hcloud CLI command and parse JSON output."""
        result = self._run_hcloud([*args, "-o", "json"])
        if result.stdout.strip():
            return json.loads(result.stdout)
        return None

    def create_runner(
        self,
        *,
        github_repo: str,
        registration_token: str,
        runner_labels: list[str] | None = None,
        name_prefix: str = "prisme-runner",
    ) -> RunnerVM:
        """Create a new ephemeral runner VM on Hetzner Cloud.

        Args:
            github_repo: GitHub repository in ``owner/repo`` format.
            registration_token: GitHub Actions runner registration token.
            runner_labels: Labels for the runner (default: self-hosted, hetzner).
            name_prefix: Prefix for the VM name.

        Returns:
            RunnerVM with the created server details.
        """
        if runner_labels is None:
            runner_labels = ["self-hosted", "hetzner"]

        runner_name = f"{name_prefix}-{secrets.token_hex(4)}"

        arch = "x64"
        if self.config.server_type.startswith("ca"):
            arch = "arm64"

        labels_str = ",".join(runner_labels)

        cloud_init = _RUNNER_CLOUD_INIT.format(
            runner_version=self.config.runner_version,
            arch=arch,
            github_repo=github_repo,
            registration_token=registration_token,
            runner_name=runner_name,
            labels=labels_str,
        )

        # Write cloud-init to a temp file
        cloud_init_path = Path(f"/tmp/prisme-runner-{runner_name}.yml")
        cloud_init_path.write_text(cloud_init)

        try:
            # Build labels for the server
            server_labels = {**self.config.labels, "runner-name": runner_name}
            label_args: list[str] = []
            for k, v in server_labels.items():
                label_args.extend(["--label", f"{k}={v}"])

            create_args = [
                "server",
                "create",
                "--name",
                runner_name,
                "--type",
                self.config.server_type,
                "--location",
                self.config.location,
                "--image",
                self.config.image,
                "--user-data-from-file",
                str(cloud_init_path),
                *label_args,
            ]

            if self.config.ssh_key_name:
                create_args.extend(["--ssh-key", self.config.ssh_key_name])

            result = self._run_hcloud([*create_args, "-o", "json"])
            server_data = json.loads(result.stdout)

            server = server_data.get("server", server_data)
            server_id = server["id"]
            ipv4 = server.get("public_net", {}).get("ipv4", {}).get("ip", "pending")

            console.print(f"[green]✓ Runner VM created: {runner_name} (ID: {server_id})[/green]")
            console.print(f"  Server type: {self.config.server_type}")
            console.print(f"  Location: {self.config.location}")
            console.print(f"  IPv4: {ipv4}")

            return RunnerVM(
                server_id=server_id,
                name=runner_name,
                ipv4=ipv4,
                status="creating",
                labels=server_labels,
            )
        finally:
            cloud_init_path.unlink(missing_ok=True)

    def destroy_runner(self, server_id: int) -> None:
        """Destroy a runner VM.

        Args:
            server_id: Hetzner Cloud server ID to destroy.
        """
        self._run_hcloud(["server", "delete", str(server_id)])
        console.print(f"[green]✓ Runner VM destroyed (ID: {server_id})[/green]")

    def destroy_runner_by_name(self, name: str) -> None:
        """Destroy a runner VM by name.

        Args:
            name: Server name.
        """
        self._run_hcloud(["server", "delete", name])
        console.print(f"[green]✓ Runner VM destroyed: {name}[/green]")

    def list_runners(self) -> list[RunnerVM]:
        """List all Prisme-managed runner VMs.

        Returns:
            List of RunnerVM objects.
        """
        data = self._run_hcloud_json(
            ["server", "list", "--label", "managed-by=prisme,purpose=ci-runner"]
        )

        if not data:
            return []

        runners: list[RunnerVM] = []
        for server in data:
            runners.append(
                RunnerVM(
                    server_id=server["id"],
                    name=server["name"],
                    ipv4=server.get("public_net", {}).get("ipv4", {}).get("ip", ""),
                    status=server.get("status", "unknown"),
                    labels=server.get("labels", {}),
                    created=server.get("created", ""),
                )
            )
        return runners

    def cleanup_stale_runners(self, max_age_minutes: int = 60) -> int:
        """Destroy runner VMs older than max_age_minutes.

        Ephemeral runners should self-terminate, but this catches
        any that got stuck.

        Args:
            max_age_minutes: Maximum age in minutes before a runner is considered stale.

        Returns:
            Number of runners destroyed.
        """
        runners = self.list_runners()
        destroyed = 0
        now = time.time()

        for runner in runners:
            if not runner.created:
                continue
            try:
                # Parse ISO 8601 timestamp
                from datetime import datetime

                created_dt = datetime.fromisoformat(runner.created.replace("Z", "+00:00"))
                age_minutes = (now - created_dt.timestamp()) / 60

                if age_minutes > max_age_minutes:
                    console.print(
                        f"[yellow]Destroying stale runner: {runner.name} "
                        f"(age: {age_minutes:.0f}m)[/yellow]"
                    )
                    self.destroy_runner(runner.server_id)
                    destroyed += 1
            except (ValueError, TypeError):
                continue

        if destroyed:
            console.print(f"[green]✓ Cleaned up {destroyed} stale runner(s)[/green]")
        else:
            console.print("[green]✓ No stale runners found[/green]")

        return destroyed

    def show_runners(self) -> None:
        """Display a table of active runner VMs."""
        runners = self.list_runners()

        if not runners:
            console.print("[dim]No active Prisme runner VMs found[/dim]")
            return

        table = Table(title="Prisme Hetzner Runners")
        table.add_column("ID", style="cyan")
        table.add_column("Name", style="bold")
        table.add_column("IPv4")
        table.add_column("Status")
        table.add_column("Created")

        for r in runners:
            status_style = {
                "running": "green",
                "creating": "yellow",
                "off": "red",
            }.get(r.status, "dim")
            table.add_row(
                str(r.server_id),
                r.name,
                r.ipv4,
                f"[{status_style}]{r.status}[/{status_style}]",
                r.created[:19] if r.created else "",
            )

        console.print(table)
