"""
Registry management for composable docker-compose stacks
"""

import json
import os
import shutil
import subprocess
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass
from .config import Config
from .users import UserManager


@dataclass
class StackInfo:
    """Information about a docker-compose stack"""
    name: str
    path: str  # Path within the registry (for fetching files)
    description: str
    ports: List[int]
    requires: List[str]
    env_vars: List[str]
    category: str  # 'infrastructure' or 'services'
    files: List[str] = None  # Files to fetch from repo
    registry: str = "default"  # Registry name (default or external repo name)

    # Runtime status
    installed: bool = False
    enabled: bool = False
    running: bool = False


@dataclass
class ExternalRegistry:
    """Information about an external registry"""
    name: str
    url: str  # Git URL
    branch: str = "main"
    last_sync: str = None  # ISO timestamp
    local_path: str = None  # Path where cloned


class RegistryManager:
    """Manage docker-compose stacks from the composable registry"""

    REGISTRY_URL = "https://raw.githubusercontent.com/weaverbird-io/composable/main/registry.json"
    REPO_BASE_URL = "https://raw.githubusercontent.com/weaverbird-io/composable/main"

    # Local install directory (where services are deployed)
    INSTALL_DIR = Path("/opt/infra-services")

    # Cache directory
    CACHE_DIR = Path.home() / ".cache" / "infra-cli"
    REGISTRY_CACHE = CACHE_DIR / "registry.json"
    EXTERNAL_REGISTRIES_FILE = CACHE_DIR / "external_registries.json"
    CACHE_MAX_AGE = 3600  # 1 hour

    # External registries are cloned to cache (source), installed to INSTALL_DIR
    REGISTRIES_CACHE_DIR = CACHE_DIR / "registries"

    # ==================== External Registry Management ====================

    @classmethod
    def list_external_registries(cls) -> List[ExternalRegistry]:
        """List all configured external registries"""
        if not cls.EXTERNAL_REGISTRIES_FILE.exists():
            return []

        try:
            with open(cls.EXTERNAL_REGISTRIES_FILE) as f:
                data = json.load(f)
                return [ExternalRegistry(**reg) for reg in data.get("registries", [])]
        except Exception:
            return []

    @classmethod
    def save_external_registries(cls, registries: List[ExternalRegistry]) -> None:
        """Save external registries to config file"""
        cls.CACHE_DIR.mkdir(parents=True, exist_ok=True)
        data = {"registries": [vars(reg) for reg in registries]}
        with open(cls.EXTERNAL_REGISTRIES_FILE, "w") as f:
            json.dump(data, f, indent=2)

    @classmethod
    def add_external_registry(cls, name: str, url: str, branch: str = "main") -> Dict:
        """Add an external registry by cloning its git repo

        Args:
            name: Unique name for this registry
            url: Git URL (https or ssh)
            branch: Git branch to use

        Returns:
            Dict with 'success' and 'message' keys
        """
        # Validate name (alphanumeric, hyphens, underscores)
        import re
        if not re.match(r'^[a-zA-Z][a-zA-Z0-9_-]*$', name):
            return {"success": False, "message": "Invalid name. Use letters, numbers, hyphens, underscores."}

        # Check if name already exists
        registries = cls.list_external_registries()
        if any(r.name == name for r in registries):
            return {"success": False, "message": f"Registry '{name}' already exists"}

        # Clone to cache directory (user's home, no special permissions needed)
        cls.REGISTRIES_CACHE_DIR.mkdir(parents=True, exist_ok=True)
        local_path = cls.REGISTRIES_CACHE_DIR / name

        # Clone the repository (as current user to use their git credentials)
        try:
            result = subprocess.run(
                ["git", "clone", "-b", branch, "--depth", "1", url, str(local_path)],
                capture_output=True,
                text=True,
                timeout=120
            )
            if result.returncode != 0:
                # Clean up failed clone directory
                if local_path.exists():
                    shutil.rmtree(local_path, ignore_errors=True)
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                return {"success": False, "message": f"Failed to clone: {error_msg}"}
        except subprocess.TimeoutExpired:
            if local_path.exists():
                shutil.rmtree(local_path, ignore_errors=True)
            return {"success": False, "message": "Timeout cloning repository"}
        except Exception as e:
            if local_path.exists():
                shutil.rmtree(local_path, ignore_errors=True)
            return {"success": False, "message": f"Failed to clone: {e}"}

        # Save registry config
        from datetime import datetime
        new_registry = ExternalRegistry(
            name=name,
            url=url,
            branch=branch,
            last_sync=datetime.now().isoformat(),
            local_path=str(local_path)
        )
        registries.append(new_registry)
        cls.save_external_registries(registries)

        return {"success": True, "message": f"Added external registry '{name}' from {url}"}

    @classmethod
    def remove_external_registry(cls, name: str) -> Dict:
        """Remove an external registry

        Args:
            name: Registry name to remove

        Returns:
            Dict with 'success' and 'message' keys
        """
        registries = cls.list_external_registries()
        registry = next((r for r in registries if r.name == name), None)

        if not registry:
            return {"success": False, "message": f"Registry '{name}' not found"}

        # Remove the cloned directory from cache
        local_path = cls.REGISTRIES_CACHE_DIR / name
        if local_path.exists():
            shutil.rmtree(local_path, ignore_errors=True)

        # Update config
        registries = [r for r in registries if r.name != name]
        cls.save_external_registries(registries)

        return {"success": True, "message": f"Removed external registry '{name}'"}

    @classmethod
    def sync_external_registry(cls, name: str) -> Dict:
        """Sync (git pull) an external registry

        Args:
            name: Registry name to sync

        Returns:
            Dict with 'success' and 'message' keys
        """
        registries = cls.list_external_registries()
        registry = next((r for r in registries if r.name == name), None)

        if not registry:
            return {"success": False, "message": f"Registry '{name}' not found"}

        local_path = cls.REGISTRIES_CACHE_DIR / name
        if not local_path.exists():
            return {"success": False, "message": f"Registry directory not found. Try removing and re-adding."}

        try:
            # Run as current user (repo is in user's home, no permission issues)
            result = subprocess.run(
                ["git", "-C", str(local_path), "pull", "--ff-only"],
                capture_output=True,
                text=True,
                timeout=60
            )
            if result.returncode != 0:
                error_msg = result.stderr.strip() if result.stderr else "Unknown error"
                return {"success": False, "message": f"Failed to sync: {error_msg}"}

            # Update last_sync timestamp
            from datetime import datetime
            registry.last_sync = datetime.now().isoformat()
            cls.save_external_registries(registries)

            return {"success": True, "message": f"Synced '{name}': {result.stdout.strip()}"}
        except subprocess.TimeoutExpired:
            return {"success": False, "message": "Timeout syncing repository"}
        except Exception as e:
            return {"success": False, "message": f"Failed to sync: {e}"}

    @classmethod
    def list_external_stacks(cls) -> List[StackInfo]:
        """List all stacks from external registries

        External stacks are named: {registry}-{original-name}
        They use standard categories (infrastructure, services)
        Path is relative within the registry for fetching files
        """
        stacks = []

        for registry in cls.list_external_registries():
            local_path = cls.REGISTRIES_CACHE_DIR / registry.name

            if not local_path.exists():
                continue

            # Check for registry.json in external repo (same format as composable)
            registry_file = local_path / "registry.json"
            if registry_file.exists():
                try:
                    with open(registry_file) as f:
                        ext_registry = json.load(f)

                    for cat in ['infrastructure', 'services']:
                        for stack_data in ext_registry.get(cat, []):
                            # Name: prefix with registry name to avoid collisions
                            stack_name = f"{registry.name}-{stack_data['name']}"
                            stack = StackInfo(
                                name=stack_name,
                                path=stack_data['path'],  # Path within registry
                                description=stack_data.get('description', ''),
                                ports=stack_data.get('ports', []),
                                requires=stack_data.get('requires', []),
                                env_vars=stack_data.get('env_vars', []),
                                category=cat,  # Standard category
                                files=stack_data.get('files', ['docker-compose.yml', '.env.example']),
                                registry=registry.name
                            )
                            stack.installed = cls.is_installed(stack.name)
                            stack.enabled = cls.is_enabled(stack.name)
                            stack.running = cls.is_running(stack.name)
                            stacks.append(stack)
                except Exception:
                    pass
            else:
                # Scan directory structure for docker-compose.yml files
                for cat in ['infrastructure', 'services', 'apps']:
                    cat_path = local_path / cat
                    if not cat_path.exists():
                        continue

                    for stack_dir in cat_path.iterdir():
                        if not stack_dir.is_dir():
                            continue
                        for variant_dir in stack_dir.iterdir():
                            if not variant_dir.is_dir():
                                continue
                            compose_file = variant_dir / "docker-compose.yml"
                            if compose_file.exists():
                                # Path relative to registry root
                                rel_path = f"{cat}/{stack_dir.name}/{variant_dir.name}"
                                stack_name = f"{registry.name}-{cat}-{stack_dir.name}-{variant_dir.name}"
                                stack = StackInfo(
                                    name=stack_name,
                                    path=rel_path,
                                    description=f"{stack_dir.name} ({variant_dir.name}) from {registry.name}",
                                    ports=[],
                                    requires=[],
                                    env_vars=[],
                                    category=cat,  # Standard category
                                    files=['docker-compose.yml'],
                                    registry=registry.name
                                )
                                stack.installed = cls.is_installed(stack.name)
                                stack.enabled = cls.is_enabled(stack.name)
                                stack.running = cls.is_running(stack.name)
                                stacks.append(stack)

        return stacks

    # ==================== End External Registry Management ====================

    @classmethod
    def fetch_registry(cls, force: bool = False) -> Dict:
        """Fetch the registry from GitHub, with caching

        Args:
            force: If True, bypass cache and fetch fresh

        Returns:
            Registry data as dict
        """
        import time
        import requests

        # Check cache
        if not force and cls.REGISTRY_CACHE.exists():
            cache_age = time.time() - cls.REGISTRY_CACHE.stat().st_mtime
            if cache_age < cls.CACHE_MAX_AGE:
                try:
                    with open(cls.REGISTRY_CACHE) as f:
                        return json.load(f)
                except:
                    pass

        # Fetch from GitHub
        try:
            resp = requests.get(cls.REGISTRY_URL, timeout=10)
            resp.raise_for_status()
            registry = resp.json()

            # Cache it
            cls.CACHE_DIR.mkdir(parents=True, exist_ok=True)
            with open(cls.REGISTRY_CACHE, "w") as f:
                json.dump(registry, f, indent=2)

            return registry
        except Exception as e:
            # Try to return cached data even if stale
            if cls.REGISTRY_CACHE.exists():
                try:
                    with open(cls.REGISTRY_CACHE) as f:
                        return json.load(f)
                except:
                    pass
            raise Exception(f"Failed to fetch registry: {e}")

    @classmethod
    def fetch_env_example(cls, name: str) -> Dict[str, Dict[str, str]]:
        """Fetch and parse .env.example for a stack

        Args:
            name: Stack name

        Returns:
            Dict of {VAR_NAME: {"value": "default", "comment": "description"}}
        """
        stack = cls.get_stack(name)
        if not stack:
            return {}

        content = None

        if stack.registry != "default":
            # External registry - read from cache
            local_file = cls.REGISTRIES_CACHE_DIR / stack.registry / stack.path / ".env.example"
            try:
                if local_file.exists():
                    content = local_file.read_text()
            except Exception:
                pass
        else:
            # Default registry - fetch from GitHub
            import requests
            url = f"{cls.REPO_BASE_URL}/{stack.path}/.env.example"
            try:
                resp = requests.get(url, timeout=10)
                if resp.status_code == 200:
                    content = resp.text
            except Exception:
                pass

        if not content:
            return {}

        try:
            env_vars = {}
            current_comment = ""

            for line in content.splitlines():
                line = line.strip()

                # Collect comments as descriptions
                if line.startswith("#"):
                    comment = line.lstrip("#").strip()
                    if current_comment:
                        current_comment += " " + comment
                    else:
                        current_comment = comment
                    continue

                # Parse KEY=VALUE
                if "=" in line and not line.startswith("#"):
                    key, value = line.split("=", 1)
                    key = key.strip()
                    value = value.strip()

                    env_vars[key] = {
                        "value": value,
                        "comment": current_comment
                    }
                    current_comment = ""
                elif line == "":
                    current_comment = ""

            return env_vars

        except Exception:
            return {}

    @classmethod
    def list_stacks(cls, category: Optional[str] = None, include_external: bool = True) -> List[StackInfo]:
        """List all available stacks from registry

        Args:
            category: Filter by 'infrastructure', 'services', or '_external/{name}', or None for all
            include_external: Whether to include stacks from external registries

        Returns:
            List of StackInfo objects
        """
        registry = cls.fetch_registry()
        stacks = []

        # Default registry stacks
        for cat in ['infrastructure', 'services']:
            if category and cat != category and not category.startswith('_external'):
                continue

            for stack_data in registry.get(cat, []):
                stack = StackInfo(
                    name=stack_data['name'],
                    path=stack_data['path'],
                    description=stack_data.get('description', ''),
                    ports=stack_data.get('ports', []),
                    requires=stack_data.get('requires', []),
                    env_vars=stack_data.get('env_vars', []),
                    category=cat,
                    files=stack_data.get('files', ['docker-compose.yml', '.env.example']),
                    registry="default"
                )

                # Check local status
                stack.installed = cls.is_installed(stack.name)
                stack.enabled = cls.is_enabled(stack.name)
                stack.running = cls.is_running(stack.name)

                stacks.append(stack)

        # External registry stacks (same category filtering as default stacks)
        if include_external:
            external_stacks = cls.list_external_stacks()
            if category:
                external_stacks = [s for s in external_stacks if s.category == category]
            stacks.extend(external_stacks)

        return stacks

    @classmethod
    def get_stack(cls, name: str) -> Optional[StackInfo]:
        """Get a specific stack by name"""
        stacks = cls.list_stacks()
        for stack in stacks:
            if stack.name == name:
                return stack
        return None

    @classmethod
    def get_install_path(cls, name: str) -> Path:
        """Get the local install path for a stack"""
        # Convert name to path: infrastructure-traefik-default -> infrastructure/traefik/default
        parts = name.split('-', 1)  # Split on first hyphen
        if len(parts) == 2:
            category = parts[0]
            rest = parts[1].replace('-', '/')
            return cls.INSTALL_DIR / category / rest
        return cls.INSTALL_DIR / name

    @classmethod
    def is_installed(cls, name: str) -> bool:
        """Check if a stack is installed locally"""
        install_path = cls.get_install_path(name)
        compose_file = install_path / "docker-compose.yml"
        disabled_file = install_path / "docker-compose.yml.disabled"
        return compose_file.exists() or disabled_file.exists()

    @classmethod
    def is_enabled(cls, name: str) -> bool:
        """Check if a stack is enabled (compose file is active)"""
        install_path = cls.get_install_path(name)
        compose_file = install_path / "docker-compose.yml"
        return compose_file.exists()

    @classmethod
    def is_running(cls, name: str) -> bool:
        """Check if a stack's containers are running (Docker or LXD)"""
        if not cls.is_enabled(name):
            return False

        install_path = cls.get_install_path(name)
        lxd_file = install_path / ".lxd"

        try:
            # Check if this is an LXD-based stack
            if lxd_file.exists():
                lxd_container = lxd_file.read_text().strip()
                result = subprocess.run(
                    ["sudo", "lxc", "list", lxd_container, "-c", "s", "--format", "csv"],
                    capture_output=True,
                    text=True,
                    timeout=10
                )
                return result.stdout.strip().upper() == "RUNNING"

            # Standard Docker compose check
            result = subprocess.run(
                ["docker", "compose", "-f", str(install_path / "docker-compose.yml"), "ps", "-q"],
                capture_output=True,
                text=True,
                timeout=10
            )
            # If there's output, containers exist
            return bool(result.stdout.strip())
        except:
            return False

    @classmethod
    def install(cls, name: str, env_vars: Optional[Dict[str, str]] = None) -> Dict:
        """Install a stack from the registry

        Args:
            name: Stack name (e.g., 'infrastructure-traefik-default')
            env_vars: Environment variables to set in .env file

        Returns:
            Dict with 'success' and 'message' keys
        """
        import requests

        stack = cls.get_stack(name)
        if not stack:
            return {"success": False, "message": f"Stack '{name}' not found in registry"}

        # Check dependencies
        for req in stack.requires:
            if not cls.is_installed(req):
                return {
                    "success": False,
                    "message": f"Missing dependency: {req}. Install it first with: infra registry install {req}"
                }

        install_path = cls.get_install_path(name)

        # Create directory with proper group ownership
        try:
            subprocess.run(["sudo", "mkdir", "-p", str(install_path)], check=True)
            subprocess.run(
                ["sudo", "chown", f"root:{UserManager.GROUP_NAME}", str(install_path)],
                check=False
            )
            subprocess.run(["sudo", "chmod", "g+rwx", str(install_path)], check=False)
        except subprocess.CalledProcessError as e:
            return {"success": False, "message": f"Failed to create directory: {e}"}

        # Get files list
        files_to_fetch = stack.files or ["docker-compose.yml", ".env.example"]
        fetched_files = []

        if stack.registry != "default":
            # External registry - copy files from cache
            source_dir = cls.REGISTRIES_CACHE_DIR / stack.registry / stack.path

            for filename in files_to_fetch:
                source_file = source_dir / filename
                if source_file.exists():
                    dest_file = install_path / filename
                    # Create parent directory if needed
                    if "/" in filename:
                        subprocess.run(
                            ["sudo", "mkdir", "-p", str(dest_file.parent)],
                            check=False
                        )
                    # Copy file
                    subprocess.run(
                        ["sudo", "cp", str(source_file), str(dest_file)],
                        check=True
                    )
                    fetched_files.append(filename)
                elif filename == "docker-compose.yml":
                    return {"success": False, "message": f"docker-compose.yml not found at {source_file}"}
        else:
            # Default registry - fetch from GitHub
            for filename in files_to_fetch:
                url = f"{cls.REPO_BASE_URL}/{stack.path}/{filename}"
                try:
                    resp = requests.get(url, timeout=10)
                    if resp.status_code == 200:
                        # Create parent directory if needed
                        file_path = install_path / filename
                        if "/" in filename:
                            subprocess.run(
                                ["sudo", "mkdir", "-p", str(file_path.parent)],
                                check=False
                            )

                        # Write to temp then sudo mv
                        import tempfile
                        with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=f"_{filename.replace('/', '_')}") as f:
                            f.write(resp.text)
                            temp_path = f.name

                        subprocess.run(
                            ["sudo", "mv", temp_path, str(file_path)],
                            check=True
                        )
                        fetched_files.append(filename)
                except Exception as e:
                    # Only docker-compose.yml is required
                    if filename == "docker-compose.yml":
                        return {"success": False, "message": f"Failed to fetch {filename}: {e}"}

        # Create directories for traefik
        if "traefik" in name:
            subprocess.run(["sudo", "mkdir", "-p", str(install_path / "config")], check=False)
            subprocess.run(["sudo", "mkdir", "-p", str(install_path / "data")], check=False)

        # Setup DNS records for any domain env vars
        dns_results = []
        if env_vars:
            from .cloudflare import CloudflareManager

            for key, value in env_vars.items():
                # Check if this looks like a domain field (DOMAIN or *_DOMAIN)
                if (key == 'DOMAIN' or key.endswith('_DOMAIN')) and value and '.' in value:
                    # Skip if it looks like an IP address
                    if all(part.isdigit() for part in value.split('.')):
                        continue

                    # Try to create/update DNS record
                    result = CloudflareManager.setup_dns_for_domain(value)
                    if result.get('success'):
                        dns_results.append(f"DNS: {result.get('action', 'ok')} {value} -> {result.get('ip')}")
                    else:
                        dns_results.append(f"DNS: Failed for {value} - {result.get('message')}")

        # Create .env file from .env.example if env_vars provided
        if env_vars or stack.env_vars:
            env_content = []

            # Read .env.example if it exists
            env_example = install_path / ".env.example"
            if env_example.exists():
                try:
                    result = subprocess.run(
                        ["sudo", "cat", str(env_example)],
                        capture_output=True,
                        text=True
                    )
                    if result.returncode == 0:
                        for line in result.stdout.splitlines():
                            if "=" in line and not line.strip().startswith("#"):
                                key = line.split("=")[0].strip()
                                if env_vars and key in env_vars:
                                    env_content.append(f"{key}={env_vars[key]}")
                                else:
                                    env_content.append(line)
                            else:
                                env_content.append(line)
                except:
                    pass

            # Add any extra env_vars not in .env.example
            if env_vars:
                existing_keys = {line.split("=")[0].strip() for line in env_content if "=" in line}
                for key, value in env_vars.items():
                    if key not in existing_keys:
                        env_content.append(f"{key}={value}")

            if env_content:
                import tempfile
                with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".env") as f:
                    f.write("\n".join(env_content) + "\n")
                    temp_path = f.name

                subprocess.run(
                    ["sudo", "mv", temp_path, str(install_path / ".env")],
                    check=True
                )

        # Set ownership to root:infra-users with group write and world read permissions
        # World readable needed for docker compose to read files
        subprocess.run(
            ["sudo", "chown", "-R", f"root:{UserManager.GROUP_NAME}", str(install_path)],
            check=False
        )
        subprocess.run(
            ["sudo", "chmod", "-R", "g+rwX,o+rX", str(install_path)],
            check=False
        )

        # Fetch and run setup.sh if it exists
        setup_result = None
        setup_url = f"{cls.REPO_BASE_URL}/{stack.path}/setup.sh"
        try:
            resp = requests.get(setup_url, timeout=10)
            if resp.status_code == 200:
                import tempfile
                with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix=".sh") as f:
                    f.write(resp.text)
                    setup_script = f.name

                subprocess.run(["chmod", "+x", setup_script], check=True)
                # Long timeout for complex setups like MIAB (15+ minutes)
                result = subprocess.run(
                    ["sudo", "-E", "bash", setup_script],
                    cwd=str(install_path),
                    capture_output=True,
                    text=True,
                    timeout=1200,  # 20 minutes
                    env={**os.environ, **(env_vars or {})}
                )
                os.unlink(setup_script)
                if result.returncode == 0:
                    setup_result = result.stdout.strip() if result.stdout else "Setup completed"
                else:
                    setup_result = f"Setup warning: {result.stderr.strip()}"
        except requests.exceptions.RequestException:
            pass  # No setup.sh, that's fine
        except Exception as e:
            setup_result = f"Setup error: {e}"

        # Build result message
        message_parts = [f"Installed {name} to {install_path}. Files: {', '.join(fetched_files)}"]
        if dns_results:
            message_parts.extend(dns_results)
        if setup_result:
            message_parts.append(setup_result)

        return {
            "success": True,
            "message": "\n".join(message_parts),
            "dns_results": dns_results
        }

    @classmethod
    def uninstall(cls, name: str, keep_data: bool = True) -> Dict:
        """Uninstall a stack

        Args:
            name: Stack name
            keep_data: If True, keep data volumes (default)

        Returns:
            Dict with 'success' and 'message' keys
        """
        if not cls.is_installed(name):
            return {"success": False, "message": f"Stack '{name}' is not installed"}

        install_path = cls.get_install_path(name)

        # Stop containers if running
        if cls.is_running(name):
            try:
                subprocess.run(
                    ["docker", "compose", "-f", str(install_path / "docker-compose.yml"), "down"],
                    check=True,
                    capture_output=True
                )
            except:
                pass

        # Remove directory
        try:
            subprocess.run(["sudo", "rm", "-rf", str(install_path)], check=True)
            return {"success": True, "message": f"Uninstalled {name}"}
        except subprocess.CalledProcessError as e:
            return {"success": False, "message": f"Failed to remove directory: {e}"}

    @classmethod
    def enable(cls, name: str) -> Dict:
        """Enable a disabled stack (rename .disabled back to active)"""
        if not cls.is_installed(name):
            return {"success": False, "message": f"Stack '{name}' is not installed"}

        if cls.is_enabled(name):
            return {"success": False, "message": f"Stack '{name}' is already enabled"}

        install_path = cls.get_install_path(name)
        disabled_file = install_path / "docker-compose.yml.disabled"
        enabled_file = install_path / "docker-compose.yml"

        try:
            subprocess.run(
                ["sudo", "mv", str(disabled_file), str(enabled_file)],
                check=True
            )
            return {"success": True, "message": f"Enabled {name}"}
        except subprocess.CalledProcessError as e:
            return {"success": False, "message": f"Failed to enable: {e}"}

    @classmethod
    def disable(cls, name: str) -> Dict:
        """Disable a stack (stop and rename compose file)"""
        if not cls.is_installed(name):
            return {"success": False, "message": f"Stack '{name}' is not installed"}

        if not cls.is_enabled(name):
            return {"success": False, "message": f"Stack '{name}' is already disabled"}

        install_path = cls.get_install_path(name)
        enabled_file = install_path / "docker-compose.yml"
        disabled_file = install_path / "docker-compose.yml.disabled"

        # Stop containers if running
        if cls.is_running(name):
            try:
                subprocess.run(
                    ["docker", "compose", "-f", str(enabled_file), "down"],
                    check=True,
                    capture_output=True
                )
            except:
                pass

        try:
            subprocess.run(
                ["sudo", "mv", str(enabled_file), str(disabled_file)],
                check=True
            )
            return {"success": True, "message": f"Disabled {name}"}
        except subprocess.CalledProcessError as e:
            return {"success": False, "message": f"Failed to disable: {e}"}

    @classmethod
    def _ensure_ghcr_login(cls, compose_file: Path) -> None:
        """Check if compose uses ghcr.io images and auto-login if needed"""
        try:
            # Read compose file (may need sudo)
            result = subprocess.run(
                ["cat", str(compose_file)],
                capture_output=True,
                text=True,
                timeout=5
            )
            content = result.stdout if result.returncode == 0 else ""
            if "ghcr.io" not in content:
                return

            # Get GitHub username
            user_result = subprocess.run(
                ["gh", "api", "user", "-q", ".login"],
                capture_output=True,
                text=True,
                timeout=10
            )
            if user_result.returncode != 0:
                return  # gh not configured, skip

            username = user_result.stdout.strip()

            # Get token and login
            token_result = subprocess.run(
                ["gh", "auth", "token"],
                capture_output=True,
                text=True,
                timeout=10
            )
            if token_result.returncode != 0:
                return  # No token, skip

            subprocess.run(
                ["docker", "login", "ghcr.io", "-u", username, "--password-stdin"],
                input=token_result.stdout,
                capture_output=True,
                text=True,
                timeout=30
            )
        except Exception:
            pass  # Best effort, don't fail the start

    @classmethod
    def start(cls, name: str) -> Dict:
        """Start a stack's containers (Docker or LXD)"""
        if not cls.is_enabled(name):
            return {"success": False, "message": f"Stack '{name}' is not enabled. Enable it first."}

        install_path = cls.get_install_path(name)
        compose_file = install_path / "docker-compose.yml"
        lxd_file = install_path / ".lxd"

        # Auto-login to ghcr.io if needed
        cls._ensure_ghcr_login(compose_file)

        try:
            # Check if this is an LXD-based stack
            if lxd_file.exists():
                lxd_container = lxd_file.read_text().strip()
                # Start LXD container
                result = subprocess.run(
                    ["sudo", "lxc", "start", lxd_container],
                    capture_output=True,
                    text=True,
                    timeout=60
                )
                # Also start Docker status container
                subprocess.run(
                    ["docker", "compose", "-f", str(compose_file), "up", "-d"],
                    capture_output=True,
                    timeout=60
                )
                if result.returncode == 0 or "already running" in result.stderr.lower():
                    return {"success": True, "message": f"Started {name} (LXD: {lxd_container})"}
                else:
                    return {"success": False, "message": f"Failed to start LXD: {result.stderr}"}

            # Standard Docker compose
            result = subprocess.run(
                ["docker", "compose", "-f", str(compose_file), "up", "-d"],
                capture_output=True,
                text=True,
                timeout=120
            )
            if result.returncode == 0:
                return {"success": True, "message": f"Started {name}"}
            else:
                return {"success": False, "message": f"Failed to start: {result.stderr}"}
        except subprocess.TimeoutExpired:
            return {"success": False, "message": "Timeout waiting for containers to start"}
        except Exception as e:
            return {"success": False, "message": f"Failed to start: {e}"}

    @classmethod
    def stop(cls, name: str) -> Dict:
        """Stop a stack's containers (Docker or LXD)"""
        if not cls.is_enabled(name):
            return {"success": False, "message": f"Stack '{name}' is not enabled"}

        install_path = cls.get_install_path(name)
        compose_file = install_path / "docker-compose.yml"
        lxd_file = install_path / ".lxd"

        try:
            # Check if this is an LXD-based stack
            if lxd_file.exists():
                lxd_container = lxd_file.read_text().strip()
                # Stop Docker status container first
                subprocess.run(
                    ["docker", "compose", "-f", str(compose_file), "down"],
                    capture_output=True,
                    timeout=60
                )
                # Stop LXD container
                result = subprocess.run(
                    ["sudo", "lxc", "stop", lxd_container],
                    capture_output=True,
                    text=True,
                    timeout=60
                )
                if result.returncode == 0 or "already stopped" in result.stderr.lower():
                    return {"success": True, "message": f"Stopped {name} (LXD: {lxd_container})"}
                else:
                    return {"success": False, "message": f"Failed to stop LXD: {result.stderr}"}

            # Standard Docker compose
            result = subprocess.run(
                ["docker", "compose", "-f", str(compose_file), "down"],
                capture_output=True,
                text=True,
                timeout=60
            )
            if result.returncode == 0:
                return {"success": True, "message": f"Stopped {name}"}
            else:
                return {"success": False, "message": f"Failed to stop: {result.stderr}"}
        except Exception as e:
            return {"success": False, "message": f"Failed to stop: {e}"}

    @classmethod
    def get_logs(cls, name: str, lines: int = 100, follow: bool = False) -> Dict:
        """Get logs for a stack"""
        if not cls.is_enabled(name):
            return {"success": False, "message": f"Stack '{name}' is not enabled"}

        install_path = cls.get_install_path(name)
        compose_file = install_path / "docker-compose.yml"

        cmd = ["docker", "compose", "-f", str(compose_file), "logs", f"--tail={lines}"]
        if follow:
            cmd.append("-f")

        try:
            if follow:
                # Return the command for interactive use
                return {"success": True, "command": cmd}
            else:
                result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
                return {"success": True, "logs": result.stdout + result.stderr}
        except Exception as e:
            return {"success": False, "message": f"Failed to get logs: {e}"}
