"""Script execution service with real-time output streaming."""

from __future__ import annotations

import asyncio
import re
from pathlib import Path
from typing import Callable, List, Optional, Tuple


class ScriptRunner:
    """Execute bash scripts with real-time output streaming."""

    def __init__(self, project_root: Optional[Path] = None):
        """Initialize script runner.

        Args:
            project_root: Path to project root. Defaults to tui/data/ package.
        """
        if project_root is None:
            from tui.data import get_data_path, get_scripts_path, get_packer_path

            self.project_root = get_data_path()
            self.scripts_dir = get_scripts_path()
            self.packer_dir = get_packer_path()
        else:
            self.project_root = Path(project_root)
            self.scripts_dir = self.project_root / "scripts"
            self.packer_dir = self.project_root / "packer"

    async def run_command(
        self,
        args: list[str],
        on_output: Optional[Callable[[str], None]] = None,
        on_error: Optional[Callable[[str], None]] = None,
        cwd: Optional[Path] = None,
    ) -> int:
        """Run a command asynchronously with output callbacks.

        Args:
            args: Command and arguments list.
            on_output: Callback for stdout lines.
            on_error: Callback for stderr lines.
            cwd: Working directory for the command.

        Returns:
            Process return code.
        """
        process = await asyncio.create_subprocess_exec(
            *args,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=cwd or self.project_root,
        )

        async def read_stream(
            stream: asyncio.StreamReader, callback: Optional[Callable[[str], None]]
        ) -> None:
            while True:
                line = await stream.readline()
                if not line:
                    break
                decoded = line.decode().rstrip()
                if callback:
                    callback(decoded)

        await asyncio.gather(
            read_stream(process.stdout, on_output),  # type: ignore
            read_stream(process.stderr, on_error or on_output),  # type: ignore
        )

        return await process.wait()

    async def run_deploy_vm(
        self,
        template_id: int,
        vm_id: int,
        vm_name: str,
        cores: int,
        memory: int,
        disk_size: str,
        network_mode: str,
        clone_type: str,
        playbooks: list[str],
        on_output: Optional[Callable[[str], None]] = None,
    ) -> int:
        """Run deploy-vm.sh with collected parameters.

        Args:
            template_id: Source template VMID.
            vm_id: New VM ID.
            vm_name: Name for the new VM.
            cores: Number of CPU cores.
            memory: RAM in MB.
            disk_size: Disk size string (e.g., "20G").
            network_mode: Network mode ("dhcp" or "static").
            clone_type: Clone type ("linked" or "full").
            playbooks: List of Ansible playbook names.
            on_output: Callback for output lines.

        Returns:
            Process return code.
        """
        # Validate VM name
        if not re.match(r'^[a-zA-Z0-9._-]+$', vm_name):
            if on_output:
                on_output(f"ERROR: Invalid VM name '{vm_name}'. "
                          "Only alphanumeric, dots, hyphens, and underscores allowed.")
            return 1

        script = self.scripts_dir / "deploy-vm.sh"
        args = [
            str(script),
            str(template_id),
            str(vm_id),
            vm_name,
            str(cores),
            str(memory),
            disk_size,
            network_mode,
            clone_type,
            *playbooks,
        ]

        return await self.run_command(args, on_output=on_output)

    async def run_deploy_k3s_cluster(
        self,
        template_id: int,
        cluster_name: str,
        cores: int,
        memory: int,
        disk_size: str,
        clone_type: str,
        k3s_version: str = "stable",
        on_output: Optional[Callable[[str], None]] = None,
    ) -> int:
        """Run deploy-k3s-cluster.sh for 3-node Kubernetes cluster.

        Args:
            template_id: Source template VMID.
            cluster_name: Base name for cluster nodes.
            cores: Number of CPU cores per node.
            memory: RAM in MB per node.
            disk_size: Disk size per node.
            clone_type: Clone type ("linked" or "full").
            k3s_version: K3S version to install.
            on_output: Callback for output lines.

        Returns:
            Process return code.
        """
        script = self.scripts_dir / "deploy-k3s-cluster.sh"
        args = [
            str(script),
            "-t",
            str(template_id),
            "-n",
            cluster_name,
            "-c",
            str(cores),
            "-m",
            str(memory),
            "-d",
            disk_size,
            "-k",
            clone_type,
            "-v",
            k3s_version,
        ]

        return await self.run_command(args, on_output=on_output)

    async def run_packer_build(
        self,
        distro: str,
        version: str,
        template_id: int,
        template_name: str,
        cores: int,
        memory: int,
        disk_size: str,
        package_profile: str,
        on_output: Optional[Callable[[str], None]] = None,
    ) -> int:
        """Run packer build for a template.

        Args:
            distro: Distribution identifier.
            version: Distribution version.
            template_id: Template VMID.
            template_name: Template name.
            cores: CPU cores.
            memory: RAM in MB.
            disk_size: Disk size.
            package_profile: Package profile ("minimal", "base", "extended").
            on_output: Callback for output lines.

        Returns:
            Process return code.
        """
        # Validate distro and version inputs
        if not re.match(r'^[a-zA-Z0-9-]+$', distro):
            if on_output:
                on_output(f"ERROR: Invalid distribution name: {distro}")
            return 1
        if not re.match(r'^[a-zA-Z0-9.-]+$', version):
            if on_output:
                on_output(f"ERROR: Invalid version: {version}")
            return 1

        # Determine packer file
        packer_file = self._get_packer_file(distro, version)
        # Verify path is within expected directory (prevent path traversal)
        try:
            packer_file.resolve().relative_to(self.packer_dir.resolve())
        except ValueError:
            if on_output:
                on_output(f"ERROR: Packer file outside expected directory: {packer_file}")
            return 1

        if not packer_file.exists():
            if on_output:
                on_output(f"ERROR: Packer file not found: {packer_file}")
            return 1

        var_file = self.packer_dir / "proxmox.auto.pkrvars.hcl"
        if not var_file.exists():
            if on_output:
                on_output(f"ERROR: Proxmox config not found: {var_file}")
            return 1

        args = [
            "packer",
            "build",
            f"-var-file={var_file}",
            f"-var=vm_id={template_id}",
            f"-var=vm_name={template_name}",
            f"-var=vm_cores={cores}",
            f"-var=vm_memory={memory}",
            f"-var=vm_disk_size={disk_size}",
            f"-var=package_profile={package_profile}",
            str(packer_file),
        ]

        return await self.run_command(args, on_output=on_output, cwd=self.packer_dir)

    def _get_packer_file(self, distro: str, version: str) -> Path:
        """Get the packer file path for a distribution/version.

        Args:
            distro: Distribution identifier.
            version: Version string.

        Returns:
            Path to packer file.
        """
        if distro == "ubuntu":
            if version == "24.04-desktop":
                return self.packer_dir / "ubuntu" / "ubuntu-24.04-desktop.pkr.hcl"
            return self.packer_dir / "ubuntu" / f"ubuntu-{version}.pkr.hcl"
        elif distro == "debian":
            return self.packer_dir / "debian" / f"debian-{version}.pkr.hcl"
        elif distro == "rocky":
            return self.packer_dir / "rhel-family" / f"rocky-{version}.pkr.hcl"
        elif distro == "almalinux":
            return self.packer_dir / "rhel-family" / f"alma-{version}.pkr.hcl"
        elif distro == "fedora":
            if version == "42-desktop":
                return self.packer_dir / "rhel-family" / "fedora-42-desktop.pkr.hcl"
            return self.packer_dir / "rhel-family" / f"fedora-{version}.pkr.hcl"
        elif distro == "opensuse":
            if version == "15.6-desktop":
                return self.packer_dir / "suse" / "opensuse-leap-15.6-desktop.pkr.hcl"
            return self.packer_dir / "suse" / f"opensuse-leap-{version}.pkr.hcl"
        else:
            return self.packer_dir / f"{distro}-{version}.pkr.hcl"

    def get_available_playbooks(self) -> list[tuple[str, str, bool]]:
        """Get list of available Ansible playbooks.

        Returns:
            List of tuples: (playbook_name, description, default_selected)
        """
        return [
            ("base-config", "Base configuration (hostname, timezone, updates)", True),
            ("users", "User management and SSH keys", False),
            ("nginx", "Nginx web server", False),
            ("postgresql", "PostgreSQL database", False),
            ("mysql", "MySQL/MariaDB database", False),
            ("mssql", "Microsoft SQL Server 2022", False),
            ("mongodb", "MongoDB NoSQL database", False),
            ("docker-compose", "Docker and Docker Compose", False),
            ("k3s", "K3S single-node Kubernetes", False),
            ("k3s-cluster", "K3S 3-node cluster (special)", False),
            ("monitoring", "Prometheus Node Exporter", False),
            ("security-harden", "Security hardening (fail2ban, firewall)", False),
        ]
