# -*- coding: utf-8 -*-
"""Location: ./mcpgateway/tools/builder/pipeline.py
Copyright 2026
SPDX-License-Identifier: Apache-2.0
Authors: Teryl Taylor

Abstract base class for MCP Stack deployment implementations.

This module defines the CICDModule interface that all deployment implementations
must implement. It provides a common API for building, deploying, and managing
ContextForge stacks with external plugin servers.

The base class implements shared functionality (validation) while requiring
subclasses to implement deployment-specific logic (build, deploy, etc.).

Design Pattern:
    Strategy Pattern - Different implementations (Dagger vs Python) can be
    swapped transparently via the DeployFactory.

Example:
    >>> from mcpgateway.tools.builder.factory import DeployFactory
    >>> deployer, mode = DeployFactory.create_deployer("dagger", verbose=False)
    ⚠ Dagger not installed. Using plain python.
    >>> # Validate configuration (output varies by config)
    >>> # deployer.validate("mcp-stack.yaml")
    >>> # Async methods must be called with await (see method examples below)
"""

# Standard
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional

# Third-Party
from pydantic import ValidationError
from rich.console import Console
import yaml

# First-Party
from mcpgateway.tools.builder.schema import MCPStackConfig

# Shared console instance for consistent output formatting
console = Console()


class CICDModule(ABC):
    """Abstract base class for MCP Stack deployment implementations.

    This class defines the interface that all deployment implementations must
    implement. It provides common initialization and validation logic while
    deferring implementation-specific details to subclasses.

    Attributes:
        verbose (bool): Enable verbose output during operations
        console (Console): Rich console for formatted output

    Implementations:
        - MCPStackDagger: High-performance implementation using Dagger SDK
        - MCPStackPython: Fallback implementation using plain Python + Docker/Podman

    Examples:
        >>> # Test that CICDModule is abstract
        >>> from abc import ABC
        >>> issubclass(CICDModule, ABC)
        True

        >>> # Test initialization with defaults
        >>> class TestDeployer(CICDModule):
        ...     async def build(self, config_file: str, **kwargs) -> None:
        ...         pass
        ...     async def generate_certificates(self, config_file: str) -> None:
        ...         pass
        ...     async def deploy(self, config_file: str, **kwargs) -> None:
        ...         pass
        ...     async def verify(self, config_file: str, **kwargs) -> None:
        ...         pass
        ...     async def destroy(self, config_file: str) -> None:
        ...         pass
        ...     def generate_manifests(self, config_file: str, **kwargs) -> Path:
        ...         return Path(".")
        >>> deployer = TestDeployer()
        >>> deployer.verbose
        False

        >>> # Test initialization with verbose=True
        >>> verbose_deployer = TestDeployer(verbose=True)
        >>> verbose_deployer.verbose
        True

        >>> # Test that console is available
        >>> hasattr(deployer, 'console')
        True
    """

    def __init__(self, verbose: bool = False):
        """Initialize the deployment module.

        Args:
            verbose: Enable verbose output during all operations

        Examples:
            >>> # Cannot instantiate abstract class directly
            >>> try:
            ...     CICDModule()
            ... except TypeError as e:
            ...     "abstract" in str(e).lower()
            True
        """
        self.verbose = verbose
        self.console = console

    def validate(self, config_file: str) -> None:
        """Validate mcp-stack.yaml configuration using Pydantic schemas.

        This method provides comprehensive validation of the MCP stack configuration
        using Pydantic models defined in schema.py. It validates:
        - Required sections (deployment, gateway, plugins)
        - Deployment type (kubernetes or compose)
        - Gateway image specification
        - Plugin configurations (name, repo/image, etc.)
        - Custom business rules (unique names, valid combinations)

        Args:
            config_file: Path to mcp-stack.yaml configuration file

        Raises:
            ValueError: If configuration is invalid, with formatted error details
            ValidationError: If Pydantic schema validation fails
            FileNotFoundError: If config_file does not exist

        Examples:
            >>> import tempfile
            >>> import yaml
            >>> from pathlib import Path
            >>> # Create a test deployer
            >>> class TestDeployer(CICDModule):
            ...     async def build(self, config_file: str, **kwargs) -> None:
            ...         pass
            ...     async def generate_certificates(self, config_file: str) -> None:
            ...         pass
            ...     async def deploy(self, config_file: str, **kwargs) -> None:
            ...         pass
            ...     async def verify(self, config_file: str, **kwargs) -> None:
            ...         pass
            ...     async def destroy(self, config_file: str) -> None:
            ...         pass
            ...     def generate_manifests(self, config_file: str, **kwargs) -> Path:
            ...         return Path(".")
            >>> deployer = TestDeployer(verbose=False)

            >>> # Test with valid minimal config
            >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            ...     config = {
            ...         'deployment': {'type': 'compose'},
            ...         'gateway': {'image': 'test:latest'},
            ...         'plugins': []
            ...     }
            ...     yaml.dump(config, f)
            ...     config_path = f.name
            >>> deployer.validate(config_path)
            >>> import os
            >>> os.unlink(config_path)

            >>> # Test with missing file
            >>> try:
            ...     deployer.validate("/nonexistent/config.yaml")
            ... except FileNotFoundError as e:
            ...     "config.yaml" in str(e)
            True

            >>> # Test with invalid config (missing required fields)
            >>> with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f:
            ...     bad_config = {'deployment': {'type': 'compose'}}
            ...     yaml.dump(bad_config, f)
            ...     bad_path = f.name
            >>> try:
            ...     deployer.validate(bad_path)
            ... except ValueError as e:
            ...     "validation failed" in str(e).lower()
            True
            >>> os.unlink(bad_path)
        """
        if self.verbose:
            self.console.print(f"[blue]Validating {config_file}...[/blue]")

        # Load YAML configuration
        with open(config_file, "r", encoding="utf-8") as f:
            config_dict = yaml.safe_load(f)

        # Validate using Pydantic schema
        try:
            # Local

            MCPStackConfig(**config_dict)
        except ValidationError as e:
            # Format validation errors for better readability
            error_msg = "Configuration validation failed:\n"
            for error in e.errors():
                # Join the error location path (e.g., plugins -> 0 -> name)
                loc = " -> ".join(str(x) for x in error["loc"])
                error_msg += f"  • {loc}: {error['msg']}\n"
            raise ValueError(error_msg) from e

        if self.verbose:
            self.console.print("[green]✓ Configuration valid[/green]")

    @abstractmethod
    async def build(self, config_file: str, plugins_only: bool = False, specific_plugins: Optional[list[str]] = None, no_cache: bool = False, copy_env_templates: bool = False) -> None:
        """Build container images for plugins and/or gateway.

        Subclasses must implement this to build Docker/Podman images from
        Git repositories or use pre-built images.

        Args:
            config_file: Path to mcp-stack.yaml
            plugins_only: Only build plugins, skip gateway
            specific_plugins: List of specific plugin names to build (optional)
            no_cache: Disable build cache for fresh builds
            copy_env_templates: Copy .env.template files from cloned repos

        Raises:
            RuntimeError: If build fails
            ValueError: If plugin configuration is invalid

        Example:
            # await deployer.build("mcp-stack.yaml", plugins_only=True)
            # ✓ Built OPAPluginFilter
            # ✓ Built LLMGuardPlugin
        """

    @abstractmethod
    async def generate_certificates(self, config_file: str) -> None:
        """Generate mTLS certificates for gateway and plugins.

        Creates a certificate authority (CA) and issues certificates for:
        - Gateway (client certificates for connecting to plugins)
        - Each plugin (server certificates for accepting connections)

        Certificates are stored in the paths defined in the config's
        certificates section (default: ./certs/mcp/).

        Args:
            config_file: Path to mcp-stack.yaml

        Raises:
            RuntimeError: If certificate generation fails
            FileNotFoundError: If required tools (openssl) are not available

        Example:
            # await deployer.generate_certificates("mcp-stack.yaml")
            # ✓ Certificates generated
        """

    @abstractmethod
    async def deploy(self, config_file: str, dry_run: bool = False, skip_build: bool = False, skip_certs: bool = False) -> None:
        """Deploy the MCP stack to Kubernetes or Docker Compose.

        This is the main deployment method that orchestrates:
        1. Building containers (unless skip_build=True)
        2. Generating mTLS certificates (unless skip_certs=True or mTLS disabled)
        3. Generating manifests (Kubernetes YAML or docker-compose.yaml)
        4. Applying the deployment (unless dry_run=True)

        Args:
            config_file: Path to mcp-stack.yaml
            dry_run: Generate manifests without actually deploying
            skip_build: Skip building containers (use existing images)
            skip_certs: Skip certificate generation (use existing certs)

        Raises:
            RuntimeError: If deployment fails at any stage
            ValueError: If configuration is invalid

        Example:
            # Full deployment
            # await deployer.deploy("mcp-stack.yaml")
            # ✓ Build complete
            # ✓ Certificates generated
            # ✓ Deployment complete

            # Dry run (generate manifests only)
            # await deployer.deploy("mcp-stack.yaml", dry_run=True)
            # ✓ Dry-run complete (no changes made)
        """

    @abstractmethod
    async def verify(self, config_file: str, wait: bool = False, timeout: int = 300) -> None:
        """Verify deployment health and readiness.

        Checks that all deployed services are healthy and ready:
        - Kubernetes: Checks pod status, optionally waits for Ready
        - Docker Compose: Checks container status

        Args:
            config_file: Path to mcp-stack.yaml
            wait: Wait for deployment to become ready
            timeout: Maximum time to wait in seconds (default: 300)

        Raises:
            RuntimeError: If verification fails or timeout is reached
            TimeoutError: If wait=True and deployment doesn't become ready

        Example:
            # Quick health check
            # await deployer.verify("mcp-stack.yaml")
            # NAME                    READY   STATUS    RESTARTS   AGE
            # mcpgateway-xxx          1/1     Running   0          2m
            # mcp-plugin-opa-xxx      1/1     Running   0          2m

            # Wait for ready state
            # await deployer.verify("mcp-stack.yaml", wait=True, timeout=600)
            # ✓ Deployment healthy
        """

    @abstractmethod
    async def destroy(self, config_file: str) -> None:
        """Destroy the deployed MCP stack.

        Removes all deployed resources:
        - Kubernetes: Deletes all resources in the namespace
        - Docker Compose: Stops and removes containers, networks, volumes

        WARNING: This is destructive and cannot be undone!

        Args:
            config_file: Path to mcp-stack.yaml

        Raises:
            RuntimeError: If destruction fails

        Example:
            # await deployer.destroy("mcp-stack.yaml")
            # ✓ Deployment destroyed
        """

    @abstractmethod
    def generate_manifests(self, config_file: str, output_dir: Optional[str] = None) -> Path:
        """Generate deployment manifests (Kubernetes YAML or docker-compose.yaml).

        Creates deployment manifests based on configuration:
        - Kubernetes: Generates Deployment, Service, ConfigMap, Secret YAML files
        - Docker Compose: Generates docker-compose.yaml with all services

        Also generates:
        - plugins-config.yaml: Plugin manager configuration for gateway
        - Environment files: .env files for each service

        Args:
            config_file: Path to mcp-stack.yaml
            output_dir: Output directory for manifests (default: ./deploy/manifests)

        Returns:
            Path: Directory containing generated manifests

        Raises:
            ValueError: If configuration is invalid
            OSError: If output directory cannot be created

        Example:
            # manifests_path = deployer.generate_manifests("mcp-stack.yaml")
            # print(f"Manifests generated in: {manifests_path}")
            # Manifests generated in: /path/to/deploy/manifests

            # Custom output directory
            # deployer.generate_manifests("mcp-stack.yaml", output_dir="./my-manifests")
            # ✓ Manifests generated: ./my-manifests
        """
