"""Docker build commands for HexDAG CLI."""

import os
import re
import shlex
from pathlib import Path
from typing import Annotated

import typer
import yaml
from rich.console import Console

app = typer.Typer()
console = Console()

# Valid identifier pattern for pipeline names (alphanumeric, dash, underscore only)
_VALID_IDENTIFIER_PATTERN = re.compile(r"^[a-zA-Z0-9_-]+$")


def _sanitize_shell_string(value: str) -> str:
    """Sanitize a string for safe use in shell scripts.

    Uses shlex.quote() to properly escape the value for shell interpolation.

    Parameters
    ----------
    value : str
        Value to sanitize

    Returns
    -------
    str
        Sanitized value safe for shell interpolation

    Examples
    --------
    >>> _sanitize_shell_string("my-pipeline")
    "'my-pipeline'"
    >>> _sanitize_shell_string("pipe; rm -rf /")
    "'pipe; rm -rf /'"
    """
    return shlex.quote(value)


def _validate_identifier(name: str, context: str = "identifier") -> None:
    """Validate that a name is a safe identifier.

    Parameters
    ----------
    name : str
        Name to validate
    context : str
        Context for error messages (e.g., "pipeline name", "image name")

    Raises
    ------
    typer.Exit
        If name contains invalid characters
    """
    if not _VALID_IDENTIFIER_PATTERN.match(name):
        console.print(
            f"[red]Error:[/red] Invalid {context}: {name!r}. "
            f"Only alphanumeric characters, dashes, and underscores are allowed."
        )
        raise typer.Exit(1)


def _read_pipeline_yaml(path: Path) -> dict:
    """Read and validate pipeline YAML file."""
    if not path.exists():
        console.print(f"[red]Error:[/red] Pipeline file not found: {path}")
        raise typer.Exit(1)

    with Path.open(path) as f:
        try:
            data = yaml.safe_load(f)
            if not isinstance(data, dict):
                console.print(f"[red]Error:[/red] Invalid YAML structure in {path}")
                raise typer.Exit(1)
            return data
        except yaml.YAMLError as e:
            console.print(f"[red]Error:[/red] Failed to parse YAML: {e}")
            raise typer.Exit(1) from e


def _validate_pipeline_structure(pipeline_path: Path, data: dict) -> None:
    """Validate pipeline structure before building Docker image.

    Parameters
    ----------
    pipeline_path : Path
        Path to the pipeline file (for error messages)
    data : dict
        Parsed pipeline data

    Raises
    ------
    typer.Exit
        If validation fails
    """
    try:
        # Check for required top-level keys (support both old and new format)
        if "name" not in data and "metadata" not in data:
            raise ValueError("Pipeline must have a 'name' or 'metadata' field")

        if "nodes" not in data and "spec" not in data:
            raise ValueError("Pipeline must have a 'nodes' or 'spec' field")

        nodes = data.get("nodes") or data.get("spec", {}).get("nodes", [])

        if not isinstance(nodes, list):
            raise ValueError("'nodes' field must be a list")

        if not nodes:
            raise ValueError("Pipeline must have at least one node")

        # Validate each node has required fields
        for i, node in enumerate(nodes):
            if not isinstance(node, dict):
                raise ValueError(f"Node {i} must be a dictionary")

            # Support both old (type/id) and new (kind/metadata.name) formats
            node_type = node.get("type") or node.get("kind")
            if not node_type:
                raise ValueError(f"Node {i} must have a 'type' or 'kind' field")

            node_id = node.get("id") or node.get("name")
            if not node_id and isinstance(node.get("metadata"), dict):
                node_id = node["metadata"].get("name")

            if not node_id:
                raise ValueError(f"Node {i} must have an 'id', 'name', or 'metadata.name' field")

    except ValueError as e:
        console.print(f"[red]Error:[/red] Invalid pipeline structure in {pipeline_path.name}: {e}")
        raise typer.Exit(1) from e


def _generate_dockerfile(
    pipeline_files: list[Path],
    output_dir: Path,
    python_version: str = "3.12",
    base_image: str | None = None,
    local_install: bool = False,
    extras: str = "",
) -> Path:
    """Generate Dockerfile for pipeline(s)."""
    if base_image is None:
        base_image = f"python:{python_version}-slim"

    # Collect all pipeline names for the image
    pipeline_names = [p.stem for p in pipeline_files]

    # Format extras for pip install
    extras_str = f"[{extras}]" if extras else ""

    dockerfile_content = (
        f"""# HexDAG Pipeline Container
# Generated by hexdag build
"""  # nosec B608
        f"""# Pipelines: {", ".join(pipeline_names)}

FROM {base_image}

# Install system dependencies
RUN apt-get update && apt-get install -y \\
    git \\
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

RUN mkdir -p /app/pipelines /app/src

"""
    )

    # Install hexdag - either from PyPI or local source
    if local_install:
        dockerfile_content += f"""# Install hexdag from local source
# Extras: {extras if extras else "none (base install only)"}
COPY hexdag/ /tmp/hexdag/
RUN pip install --no-cache-dir /tmp/hexdag{extras_str} && rm -rf /tmp/hexdag

"""
    else:
        dockerfile_content += f"""# Install hexdag with dependencies
# Extras: {extras if extras else "none (base install only)"}
RUN pip install --no-cache-dir hexdag{extras_str}

"""

    dockerfile_content += "# Copy pipeline files\n"

    for pipeline_file in pipeline_files:
        dockerfile_content += (
            f"COPY pipelines/{pipeline_file.name} /app/pipelines/{pipeline_file.name}\n"
        )

    # Copy requirements.txt and src directory
    dockerfile_content += """
# Copy custom dependencies and code
COPY requirements.txt /app/requirements.txt
COPY src/ /app/src/

# Install custom requirements if not empty
RUN if [ -s requirements.txt ]; then pip install --no-cache-dir -r requirements.txt; fi

ENV PYTHONPATH=/app:$PYTHONPATH

COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["--help"]
"""

    dockerfile_path = output_dir / "Dockerfile"
    with Path.open(dockerfile_path, "w") as f:
        f.write(dockerfile_content)

    return dockerfile_path


def _generate_entrypoint_script(output_dir: Path, pipeline_files: list[Path]) -> Path:
    """Generate docker-entrypoint.sh script."""
    pipeline_names = [p.stem for p in pipeline_files]

    # Validate all pipeline names are safe identifiers
    for name in pipeline_names:
        _validate_identifier(name, "pipeline name")

    script_content = """#!/bin/bash
set -e

# HexDAG Pipeline Entrypoint
# This script runs hexdag pipelines in a containerized environment

"""

    script_content += "# Available pipelines:\n"
    for name in pipeline_names:
        script_content += f"#   - {name}\n"

    script_content += """
# Parse arguments
PIPELINE_NAME="${1:-}"
PIPELINE_INPUT="${2:-\\{\\}}"

if [ "$PIPELINE_NAME" = "--help" ] || [ -z "$PIPELINE_NAME" ]; then
    echo "Usage: docker run <image> <pipeline-name> [input-json]"
    echo ""
    echo "Available pipelines:"
"""

    # Use sanitized names for echo commands (defense in depth)
    for name in pipeline_names:
        safe_name = _sanitize_shell_string(name)
        script_content += f'    echo "  - {safe_name}"\n'

    script_content += """    echo ""
    echo "Examples:"
    echo "  docker run <image> my-pipeline '{\"input\": \"data\"}'"
    echo "  docker run <image> my-pipeline @input.json"
    exit 0
fi

# Find pipeline file
PIPELINE_FILE="/app/pipelines/${PIPELINE_NAME}.yaml"
if [ ! -f "$PIPELINE_FILE" ]; then
    PIPELINE_FILE="/app/pipelines/${PIPELINE_NAME}.yml"
fi

if [ ! -f "$PIPELINE_FILE" ]; then
    echo "Error: Pipeline '${PIPELINE_NAME}' not found"
    echo "Available pipelines:"
"""

    # Use sanitized names for echo commands
    for name in pipeline_names:
        safe_name = _sanitize_shell_string(name)
        script_content += f'    echo "  - {safe_name}"\n'

    script_content += '''    exit 1
fi

# Run pipeline using Python
python3 <<EOF
import asyncio
import json
import sys
from pathlib import Path

from hexdag.core.pipeline_builder import YamlPipelineBuilder

async def main():
    try:
        # Load pipeline
        builder = YamlPipelineBuilder()
        with Path.open("$PIPELINE_FILE") as f:
            builder.load_from_yaml(f.read())

        # Parse input - support both JSON string and file input
        input_str = """$PIPELINE_INPUT"""

        if input_str.startswith("@"):
            input_file = input_str[1:]  # Remove @ prefix
            try:
                with Path.open(input_file) as f:
                    input_data = json.load(f)
            except FileNotFoundError:
                print(f"Error: Input file not found: {input_file}", file=sys.stderr)
                sys.exit(1)
            except json.JSONDecodeError as e:
                print(f"Error: Invalid JSON in file {input_file}: {e}", file=sys.stderr)
                sys.exit(1)
        else:
            # Parse as JSON string
            try:
                input_data = json.loads(input_str)
            except json.JSONDecodeError as e:
                print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
                print(f"Received: {input_str[:100]}...", file=sys.stderr)
                sys.exit(1)

        dag = builder.build()
        results = await dag.aexecute(input_data)

        # Output results
        print(json.dumps(results, indent=2))

    except Exception as e:
        print(f"Error executing pipeline: {e}", file=sys.stderr)
        import traceback
        traceback.print_exc()
        sys.exit(1)

if __name__ == "__main__":
    asyncio.run(main())
EOF
'''

    script_path = output_dir / "docker-entrypoint.sh"
    with Path.open(script_path, "w") as f:
        f.write(script_content)

    return script_path


def _generate_docker_compose(
    pipeline_files: list[Path],
    output_dir: Path,
    image_name: str,
) -> Path:
    """Generate docker-compose.yml for multi-pipeline orchestration."""
    services = {}

    for pipeline_file in pipeline_files:
        pipeline_name = pipeline_file.stem
        pipeline_data = _read_pipeline_yaml(pipeline_file)

        env_vars = {}
        if "metadata" in pipeline_data:
            metadata = pipeline_data["metadata"]
            if "environment" in metadata:
                env_vars = metadata["environment"]

        service_config: dict[str, str | list[str] | dict[str, str]] = {
            "image": image_name,
            "container_name": f"hexdag-{pipeline_name}",
            "command": [pipeline_name, "${{INPUT:-{}}}"],
            "env_file": [".env"],  # Support .env file for configuration
            "volumes": ["./data:/app/data"],
            "restart": "unless-stopped",
        }

        if env_vars:
            service_config["environment"] = env_vars

        services[pipeline_name] = service_config

    compose_data = {
        "version": "3.8",
        "services": services,
        "volumes": {"data": {"driver": "local"}},
    }

    compose_path = output_dir / "docker-compose.yml"
    with Path.open(compose_path, "w") as f:
        yaml.dump(compose_data, f, default_flow_style=False, sort_keys=False)

    return compose_path


def _generate_readme(
    output_dir: Path,
    pipeline_files: list[Path],
    image_name: str,
) -> Path:
    """Generate README.md with usage instructions."""
    pipeline_names = [p.stem for p in pipeline_files]

    readme_content = f"""# HexDAG Pipeline Container

This container includes the following pipelines:
{chr(10).join(f"- `{name}`" for name in pipeline_names)}

## Building the Image

> **Note**: If hexdag is not published to PyPI, you'll need to install it from source.
> Replace the `RUN pip install --no-cache-dir hexdag[all]` line in the Dockerfile with:
> ```dockerfile
> COPY . /tmp/hexdag
> RUN pip install --no-cache-dir /tmp/hexdag[all]
> ```
> And copy the hexdag source code to the build directory before building.

```bash
docker build -t {image_name} .
```

## Running Pipelines

### Single Pipeline

```bash
# Run with JSON input
docker run {image_name} <pipeline-name> '{{"input": "data"}}'

# Run with input file
docker run -v $(pwd)/input.json:/app/input.json {image_name} <pipeline-name> @/app/input.json
```

### Using Docker Compose

```bash
# Start all pipelines
docker-compose up -d

# Run specific pipeline
docker-compose run <pipeline-name>

# View logs
docker-compose logs -f <pipeline-name>

# Stop all pipelines
docker-compose down
```

## Configuration

### Environment Variables

Configure adapters using environment variables:

```bash
# LLM Configuration
export OPENAI_API_KEY="your-key"
export ANTHROPIC_API_KEY="your-key"

# Database Configuration
export DATABASE_URL="postgresql://..."

# Run with environment
docker run --env-file .env {image_name} <pipeline-name> '{{"input": "data"}}'
```

### Custom Dependencies

Add custom Python dependencies to `requirements.txt` before building:

```bash
echo "pandas>=2.0.0" >> requirements.txt
docker build -t {image_name} .
```

### Custom Code

Add custom Python modules to `src/` directory:

```
src/
  ├── my_module/
  │   ├── __init__.py
  │   └── functions.py
```

## Pipeline Details

"""

    for pipeline_file in pipeline_files:
        pipeline_data = _read_pipeline_yaml(pipeline_file)
        name = pipeline_file.stem
        description = pipeline_data.get("metadata", {}).get("description", "No description")

        readme_content += f"""### {name}

**Description:** {description}

**Run:**
```bash
docker run {image_name} {name} '{{"input": "data"}}'
```

"""

    readme_path = output_dir / "README.md"
    with Path.open(readme_path, "w") as f:
        f.write(readme_content)

    return readme_path


@app.command()
def build(
    pipeline: Annotated[
        list[Path],
        typer.Argument(
            help="Pipeline YAML file(s) to build",
            exists=True,
            dir_okay=False,
        ),
    ],
    output: Annotated[
        str,
        typer.Option(
            "--output",
            "-o",
            help="Output directory for Docker files (default: ./build)",
        ),
    ] = "./build",
    image: Annotated[
        str | None,
        typer.Option(
            "--image",
            "-i",
            help="Docker image name (default: hexdag-<pipeline-name>)",
        ),
    ] = None,
    python_version: Annotated[
        str,
        typer.Option(
            "--python-version",
            "-p",
            help="Python version for base image",
        ),
    ] = "3.12",
    base_image: Annotated[
        str | None,
        typer.Option(
            "--base-image",
            "-b",
            help="Custom base Docker image",
        ),
    ] = None,
    compose: Annotated[
        bool,
        typer.Option(
            "--compose/--no-compose",
            "-c/-C",
            help="Generate docker-compose.yml for multi-pipeline orchestration",
        ),
    ] = True,
    local: Annotated[
        bool,
        typer.Option(
            "--local",
            "-l",
            help="Install hexdag from local source (copies hexdag/ directory to build context)",
        ),
    ] = False,
    extras: Annotated[
        str,
        typer.Option(
            "--extras",
            "-e",
            help=(
                "Comma-separated list of extras to install (e.g., 'yaml,openai,anthropic,cli'). "
                "Available: yaml, viz, openai, anthropic, database, cli, docs, all"
            ),
        ),
    ] = "yaml,openai,anthropic,cli",
) -> None:
    """Build Docker container(s) for pipeline(s).

    Security Warning
    ----------------
    This command is intended for DEVELOPMENT USE ONLY with trusted pipelines.

    ⚠️  DO NOT use in production environments with untrusted YAML files.

    For production deployments:
    - Build containers in CI/CD with trusted YAML files
    - Deploy pre-built, verified images only
    - Set HEXDAG_DISABLE_BUILD=1 to disable this command

    Examples
    --------
    # Single pipeline
    hexdag build my-pipeline.yaml

    # Multiple pipelines
    hexdag build pipeline1.yaml pipeline2.yaml pipeline3.yaml

    # Custom output directory
    hexdag build my-pipeline.yaml --output ./docker

    # Custom image name
    hexdag build my-pipeline.yaml --image my-org/my-pipeline:latest

    # Skip docker-compose generation
    hexdag build my-pipeline.yaml --no-compose
    """
    # Security: Check if build command is disabled (production safety)

    if os.getenv("HEXDAG_DISABLE_BUILD", "").lower() in ("1", "true", "yes"):
        console.print(
            "[red]Error:[/red] Docker build command is disabled "
            "(HEXDAG_DISABLE_BUILD is set).\n"
            "[yellow]This is a security feature to prevent untrusted pipeline execution.[/yellow]\n"
            "If you need to build containers, unset HEXDAG_DISABLE_BUILD in development."
        )
        raise typer.Exit(1)

    output_path = Path(output)

    output_path.mkdir(parents=True, exist_ok=True)

    pipelines_dir = output_path / "pipelines"
    pipelines_dir.mkdir(exist_ok=True)

    # Determine image name
    if image is None:
        image = f"hexdag-{pipeline[0].stem}" if len(pipeline) == 1 else "hexdag-pipelines"

    console.print(f"[cyan]Building Docker container for {len(pipeline)} pipeline(s)...[/cyan]\n")

    # Validate all pipeline files and names before building
    console.print("[cyan]Validating pipeline files...[/cyan]")
    for p in pipeline:
        # Validate pipeline filename is a safe identifier
        _validate_identifier(p.stem, f"pipeline filename '{p.name}'")

        pipeline_data = _read_pipeline_yaml(p)
        _validate_pipeline_structure(p, pipeline_data)
        console.print(f"  [green]✓[/green] Valid: {p.name}")
    console.print()

    # Copy pipeline files to output
    copied_pipelines = []
    for p in pipeline:
        dest = pipelines_dir / p.name
        with Path.open(p) as src_file, open(dest, "w") as dst_file:
            dst_file.write(src_file.read())
        copied_pipelines.append(dest)
        console.print(f"  [green]✓[/green] Copied: {p.name}")

    # Copy hexdag source if --local flag is set
    if local:
        import shutil

        # Find hexdag source directory more robustly
        import hexdag

        hexdag_src = Path(hexdag.__file__).parent.parent

        # Verify we found the right directory
        if not (hexdag_src / "pyproject.toml").exists():
            console.print(
                f"[red]Error:[/red] Could not find hexdag source directory. "
                f"Expected pyproject.toml in {hexdag_src}"
            )
            raise typer.Exit(1)

        hexdag_dest = output_path / "hexdag"

        console.print("\n[cyan]Copying hexdag source for local installation...[/cyan]")

        # Remove existing directory with error handling
        if hexdag_dest.exists():
            try:
                shutil.rmtree(hexdag_dest)
            except PermissionError as e:
                console.print(f"[red]Error:[/red] Cannot remove {hexdag_dest}: {e}")
                console.print(
                    "[yellow]Hint:[/yellow] Check file permissions or close any programs "
                    "using these files"
                )
                raise typer.Exit(1) from e

        try:
            shutil.copytree(
                hexdag_src,
                hexdag_dest,
                ignore=shutil.ignore_patterns(
                    "__pycache__",
                    "*.pyc",
                    "*.pyo",
                    ".git",
                    ".venv",
                    "venv",
                    "*.egg-info",
                    "build",
                    "dist",
                    ".pytest_cache",
                    ".mypy_cache",
                    "tests",
                    "examples",
                    "docs",
                    ".coverage",
                    "htmlcov",
                    "*.egg",
                    ".eggs",
                    ".tox",
                    ".ruff_cache",
                ),
            )
            console.print(f"  [green]✓[/green] Copied hexdag source from {hexdag_src}")
        except Exception as e:
            console.print(f"[red]Error:[/red] Failed to copy hexdag source: {e}")
            raise typer.Exit(1) from e

    # Generate Dockerfile
    console.print("\n[cyan]Generating Dockerfile...[/cyan]")
    dockerfile = _generate_dockerfile(
        [pipelines_dir / p.name for p in pipeline],
        output_path,
        python_version,
        base_image,
        local_install=local,
        extras=extras,
    )
    console.print(f"  [green]✓[/green] Created: {dockerfile.relative_to(output_path.parent)}")
    if extras:
        console.print(f"  [dim]Installing with extras: {extras}[/dim]")
    else:
        console.print("  [dim]Installing base package only (no extras)[/dim]")

    # Generate entrypoint script
    console.print("\n[cyan]Generating entrypoint script...[/cyan]")
    entrypoint = _generate_entrypoint_script(
        output_path,
        [pipelines_dir / p.name for p in pipeline],
    )
    console.print(f"  [green]✓[/green] Created: {entrypoint.relative_to(output_path.parent)}")

    # Generate docker-compose if requested and multiple pipelines
    if compose and len(pipeline) > 1:
        console.print("\n[cyan]Generating docker-compose.yml...[/cyan]")
        compose_file = _generate_docker_compose(
            [pipelines_dir / p.name for p in pipeline],
            output_path,
            image,
        )
        console.print(f"  [green]✓[/green] Created: {compose_file.relative_to(output_path.parent)}")

    # Generate README
    console.print("\n[cyan]Generating README.md...[/cyan]")
    readme = _generate_readme(output_path, [pipelines_dir / p.name for p in pipeline], image)
    console.print(f"  [green]✓[/green] Created: {readme.relative_to(output_path.parent)}")

    dockerignore = output_path / ".dockerignore"
    with Path.open(dockerignore, "w") as f:
        f.write("**/__pycache__\n**/*.pyc\n**/*.pyo\n**/.git\n**/.venv\n**/venv\n")
    console.print(f"  [green]✓[/green] Created: {dockerignore.relative_to(output_path.parent)}")

    requirements = output_path / "requirements.txt"
    if not requirements.exists():
        requirements.touch()
        console.print(f"  [green]✓[/green] Created: {requirements.relative_to(output_path.parent)}")

    src_dir = output_path / "src"
    src_dir.mkdir(exist_ok=True)
    (src_dir / "__init__.py").touch()
    console.print(f"  [green]✓[/green] Created: {src_dir.relative_to(output_path.parent)}/")

    env_file = output_path / ".env.example"
    with Path.open(env_file, "w") as f:
        f.write("""# HexDAG Environment Configuration
# Copy this file to .env and fill in your values

# LLM API Keys
OPENAI_API_KEY=your-openai-api-key-here
ANTHROPIC_API_KEY=your-anthropic-api-key-here

# Database Configuration (if using database extra)
DATABASE_URL=sqlite:///app/data/hexdag.db

# Logging
LOG_LEVEL=INFO

# Custom environment variables
""")
    console.print(f"  [green]✓[/green] Created: {env_file.relative_to(output_path.parent)}")

    console.print("\n[green]✓ Docker build files generated successfully![/green]\n")
    console.print("[cyan]Next steps:[/cyan]")
    console.print(f"  1. Review generated files in: [bold]{output_path}[/bold]")
    console.print(f"  2. Build image: [bold]cd {output_path} && docker build -t {image} .[/bold]")

    if compose and len(pipeline) > 1:
        console.print(f"  3. Run with compose: [bold]cd {output_path} && docker-compose up[/bold]")
    else:
        pipeline_name = pipeline[0].stem
        console.print(
            f"  3. Run pipeline: [bold]docker run {image} {pipeline_name} "
            f'\'{{"{pipeline_name}_input": "value"}}\'[/bold]'
        )

    console.print(
        f"\n[dim]See {readme.relative_to(output_path.parent)} for detailed usage instructions[/dim]"
    )
