"""Hetzner Cloud worker VM management for background jobs.

Manages the lifecycle of Hetzner Cloud VMs used as background worker
nodes. Workers run Docker containers with the application's job
commands, with support for:

- Long-running workers (restart_policy='always')
- Scheduled jobs via cron (restart_policy='on-failure')
- One-shot jobs (restart_policy='never')

Requires:
- ``hcloud`` CLI installed and on PATH
- ``HCLOUD_TOKEN`` environment variable set
"""

from __future__ import annotations

import json
import subprocess
import textwrap
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

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

if TYPE_CHECKING:
    from prisme.spec.infrastructure import HetznerWorkerJobSpec, HetznerWorkersConfig

console = Console()


def _build_cloud_init(
    *,
    job: HetznerWorkerJobSpec,
    docker_registry: str,
    docker_image: str,
    project_name: str,
) -> str:
    """Build a cloud-init script that runs a worker job in Docker.

    Args:
        job: The worker job specification.
        docker_registry: Docker registry to pull from.
        docker_image: Full Docker image reference.
        project_name: Project name for labeling.

    Returns:
        Cloud-init YAML string.
    """
    env_lines = ""
    if job.environment:
        env_flags = " ".join(f'-e {k}="{v}"' for k, v in job.environment.items())
        env_lines = f"      {env_flags} \\"

    restart_flag = {
        "always": "--restart=always",
        "on-failure": f"--restart=on-failure:{job.max_restarts}",
        "never": "--restart=no",
    }[job.restart_policy]

    # For scheduled jobs, wrap in a cron entry
    if job.schedule:
        run_section = textwrap.dedent(f"""\
    # Set up cron-scheduled job
    - |
      cat > /etc/cron.d/{job.name.replace("-", "_")} << 'CRON'
      {job.schedule} root docker run --rm \\
        --name {project_name}-{job.name}-cron \\
{env_lines}
        {docker_image} \\
        {job.command}
      CRON
    - chmod 0644 /etc/cron.d/{job.name.replace("-", "_")}
    - crontab /etc/cron.d/{job.name.replace("-", "_")}
    - systemctl restart cron""")
    else:
        # Long-running or one-shot worker
        run_section = textwrap.dedent(f"""\
    # Run worker container
    - |
      docker run -d \\
        --name {project_name}-{job.name} \\
        {restart_flag} \\
        --stop-timeout {job.timeout_seconds} \\
{env_lines}
        {docker_image} \\
        {job.command}""")

    return textwrap.dedent(f"""\
#cloud-config
package_update: true
packages:
  - docker.io
  - cron

runcmd:
  - systemctl enable docker
  - systemctl start docker

  # Pull the Docker image
  - docker pull {docker_image}

{run_section}
""")


@dataclass
class WorkerVM:
    """Represents a Hetzner Cloud worker VM."""

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


class HetznerWorkerManager:
    """Manage Hetzner Cloud VMs for background worker jobs.

    Each worker job gets a dedicated VM running Docker with the
    configured command and restart policy.

    Usage::

        from prisme.spec.infrastructure import HetznerWorkersConfig, HetznerWorkerJobSpec

        config = HetznerWorkersConfig(
            enabled=True,
            jobs=[HetznerWorkerJobSpec(
                name="data-processor",
                command="python -m app.workers.process",
                restart_policy="always",
            )],
        )
        manager = HetznerWorkerManager(config, project_name="myapp")
        manager.deploy_all()
    """

    def __init__(
        self,
        config: HetznerWorkersConfig,
        project_name: str,
    ) -> None:
        self.config = config
        self.project_name = project_name
        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."""
        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 deploy_job(self, job: HetznerWorkerJobSpec) -> WorkerVM:
        """Deploy a single worker job to a new Hetzner VM.

        Args:
            job: Worker job specification.

        Returns:
            WorkerVM with the created server details.
        """
        server_name = f"{self.project_name}-worker-{job.name}"
        docker_image = (
            job.docker_image or f"{self.config.docker_registry}/{self.project_name}-backend:latest"
        )

        cloud_init = _build_cloud_init(
            job=job,
            docker_registry=self.config.docker_registry,
            docker_image=docker_image,
            project_name=self.project_name,
        )

        # Write cloud-init to temp file
        from pathlib import Path

        cloud_init_path = Path(f"/tmp/prisme-worker-{server_name}.yml")
        cloud_init_path.write_text(cloud_init)

        try:
            server_type = job.server_type or self.config.default_server_type
            location = job.location or self.config.default_location

            labels = {
                **job.labels,
                "job-name": job.name,
                "project": self.project_name,
            }
            label_args: list[str] = []
            for k, v in labels.items():
                label_args.extend(["--label", f"{k}={v}"])

            create_args = [
                "server",
                "create",
                "--name",
                server_name,
                "--type",
                server_type,
                "--location",
                location,
                "--image",
                job.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]✓ Worker VM deployed: {server_name} (ID: {server_id})[/green]")
            console.print(f"  Job: {job.name}")
            console.print(f"  Command: {job.command}")
            console.print(f"  Server type: {server_type}")
            console.print(f"  Restart policy: {job.restart_policy}")
            if job.schedule:
                console.print(f"  Schedule: {job.schedule}")

            return WorkerVM(
                server_id=server_id,
                name=server_name,
                ipv4=ipv4,
                status="creating",
                job_name=job.name,
                labels=labels,
            )
        finally:
            cloud_init_path.unlink(missing_ok=True)

    def deploy_all(self) -> list[WorkerVM]:
        """Deploy all configured worker jobs.

        Returns:
            List of WorkerVM objects for all deployed workers.
        """
        if not self.config.jobs:
            console.print("[yellow]No worker jobs configured[/yellow]")
            return []

        console.print(f"[blue]Deploying {len(self.config.jobs)} worker job(s)...[/blue]")
        vms: list[WorkerVM] = []

        for job in self.config.jobs:
            vm = self.deploy_job(job)
            vms.append(vm)

        console.print(f"\n[green]✓ All {len(vms)} worker(s) deployed[/green]")
        return vms

    def destroy_job(self, job_name: str) -> None:
        """Destroy the worker VM for a specific job.

        Args:
            job_name: The job name (used in the server name).
        """
        server_name = f"{self.project_name}-worker-{job_name}"
        self._run_hcloud(["server", "delete", server_name])
        console.print(f"[green]✓ Worker VM destroyed: {server_name}[/green]")

    def destroy_all(self) -> int:
        """Destroy all worker VMs for this project.

        Returns:
            Number of workers destroyed.
        """
        workers = self.list_workers()
        for worker in workers:
            self._run_hcloud(["server", "delete", str(worker.server_id)])
            console.print(f"[green]✓ Destroyed: {worker.name}[/green]")
        return len(workers)

    def list_workers(self) -> list[WorkerVM]:
        """List all worker VMs for this project.

        Returns:
            List of WorkerVM objects.
        """
        data = self._run_hcloud_json(
            [
                "server",
                "list",
                "--label",
                f"managed-by=prisme,purpose=worker,project={self.project_name}",
            ]
        )

        if not data:
            return []

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

    def show_workers(self) -> None:
        """Display a table of active worker VMs."""
        workers = self.list_workers()

        if not workers:
            console.print("[dim]No active worker VMs found[/dim]")
            return

        table = Table(title=f"Prisme Workers — {self.project_name}")
        table.add_column("ID", style="cyan")
        table.add_column("Name", style="bold")
        table.add_column("Job")
        table.add_column("IPv4")
        table.add_column("Status")
        table.add_column("Created")

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

        console.print(table)
