"""Play loader and executor for Gremlin Autopilot.

Plays are pre-built workflow templates that can be run without AI assistance.
They're defined in YAML files and can be parameterized.
"""

from __future__ import annotations

import ast
import logging
import operator
import re
from pathlib import Path
from typing import Any

import yaml

from .models import (
    PlayDefinition,
    PlayParameter,
    PlayStep,
    PlaySummary,
)

logger = logging.getLogger(__name__)

# Default plays directory
from g_gremlin.config import get_config_dir as _get_config_dir

DEFAULT_PLAYS_DIR = _get_config_dir() / "plays"
USER_PLAYS_DIR = Path.home() / ".g_gremlin" / "plays"


class PlayLoader:
    """Loads and manages play definitions."""

    def __init__(
        self,
        plays_dirs: list[Path] | None = None,
        user_credentials: set[str] | None = None,
    ):
        """Initialize the play loader.

        Args:
            plays_dirs: List of directories to search for plays.
            user_credentials: User's available credentials for filtering.
        """
        self.plays_dirs = plays_dirs or [DEFAULT_PLAYS_DIR, USER_PLAYS_DIR]
        self.user_credentials = user_credentials or set()
        self._plays: dict[str, PlayDefinition] = {}
        self._load_all()

    def _load_all(self) -> None:
        """Load all plays from configured directories."""
        for plays_dir in self.plays_dirs:
            if not plays_dir.exists():
                continue

            for yaml_file in plays_dir.glob("*.yaml"):
                try:
                    play = self._load_play(yaml_file)
                    if play:
                        self._plays[play.name] = play
                except Exception as e:
                    logger.warning(f"Failed to load play {yaml_file}: {e}")

    def _load_play(self, path: Path) -> PlayDefinition | None:
        """Load a single play from a YAML file."""
        with open(path, encoding="utf-8") as f:
            data = yaml.safe_load(f)

        if not data:
            return None

        # Check credential requirements (ALL must be present)
        required_creds = data.get("requires_credentials", [])
        if required_creds and not all(c in self.user_credentials for c in required_creds):
            logger.debug(f"Skipping play {data.get('name')} - missing credentials")
            return None

        # Parse parameters
        parameters = []
        for param_data in data.get("parameters", []):
            parameters.append(PlayParameter(
                name=param_data["name"],
                type=param_data.get("type", "string"),
                label=param_data.get("label", param_data["name"]),
                description=param_data.get("description"),
                required=param_data.get("required", False),
                default=param_data.get("default"),
                choices=param_data.get("choices"),
            ))

        # Parse steps
        steps = []
        for step_data in data.get("steps", []):
            # Support both 'when' and 'condition' for backwards compat
            when_condition = step_data.get("when") or step_data.get("condition")

            steps.append(PlayStep(
                tool=step_data.get("tool"),
                args=step_data.get("args", {}),
                command=step_data.get("command"),
                shell=step_data.get("shell", False),
                label=step_data["label"],
                destructive=step_data.get("destructive", False),
                when=when_condition,
                name=step_data.get("name"),
            ))

        return PlayDefinition(
            name=data["name"],
            description=data.get("description", ""),
            icon=data.get("icon", "📋"),
            category=data.get("category", "general"),
            schema_version=data.get("schema_version", "1"),
            version=data.get("version", "1.0.0"),
            parameters=parameters,
            steps=steps,
            requires_credentials=data.get("requires_credentials", []),
            outputs=data.get("outputs", []),
        )

    def get_play(self, name: str) -> PlayDefinition | None:
        """Get a play by name."""
        return self._plays.get(name)

    def list_plays(self) -> list[PlaySummary]:
        """List all available plays as summaries."""
        return [
            PlaySummary(
                name=play.name,
                description=play.description,
                icon=play.icon,
                category=play.category,
            )
            for play in self._plays.values()
        ]

    def get_plays_by_category(self, category: str) -> list[PlayDefinition]:
        """Get all plays in a category."""
        return [p for p in self._plays.values() if p.category == category]

    def reload(self) -> None:
        """Reload all plays from disk."""
        self._plays.clear()
        self._load_all()


def render_template(template: str, params: dict[str, Any]) -> str:
    """Render a Jinja-style template string with parameters.

    Supports simple {{ variable }} substitution only.
    """
    def replace_var(match: re.Match) -> str:
        var_name = match.group(1).strip()
        value = params.get(var_name, match.group(0))
        return str(value) if value is not None else ""

    pattern = r"\{\{\s*([^}]+)\s*\}\}"
    return re.sub(pattern, replace_var, template)


def render_args(args: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
    """Render all template strings in an args dict."""
    result = {}
    for key, value in args.items():
        if isinstance(value, str):
            result[key] = render_template(value, params)
        elif isinstance(value, list):
            result[key] = [
                render_template(v, params) if isinstance(v, str) else v
                for v in value
            ]
        elif isinstance(value, dict):
            result[key] = render_args(value, params)
        else:
            result[key] = value
    return result


# --- Safe AST Condition Evaluator ---

# Safe operators for condition evaluation
SAFE_OPERATORS: dict[type, Any] = {
    ast.Eq: operator.eq,
    ast.NotEq: operator.ne,
    ast.Lt: operator.lt,
    ast.LtE: operator.le,
    ast.Gt: operator.gt,
    ast.GtE: operator.ge,
    ast.And: lambda a, b: a and b,
    ast.Or: lambda a, b: a or b,
    ast.Not: operator.not_,
}


def evaluate_condition(condition: str, params: dict[str, Any]) -> bool:
    """Evaluate a condition expression safely using AST.

    Supports:
    - Variable references: output_format, has_cpq
    - Comparisons: output_format == "gdoc", threshold > 0.5
    - Boolean logic: has_cpq and output_format == "markdown"
    - Truthy checks: has_cpq (true if truthy)

    Args:
        condition: Expression string ({{ }} wrappers are stripped if present)
        params: Parameter values

    Returns:
        Boolean result of evaluation
    """
    # Strip {{ }} if present (backwards compat)
    condition = condition.strip()
    if condition.startswith("{{") and condition.endswith("}}"):
        condition = condition[2:-2].strip()

    try:
        tree = ast.parse(condition, mode="eval")
        return bool(_eval_ast(tree.body, params))
    except Exception:
        # Fallback: simple truthy check on rendered template
        rendered = render_template(f"{{{{ {condition} }}}}", params)
        return rendered.lower() not in ("", "false", "none", "0")


def _eval_ast(node: ast.AST, params: dict[str, Any]) -> Any:
    """Recursively evaluate AST node safely."""
    if isinstance(node, ast.Constant):
        return node.value
    elif isinstance(node, ast.Name):
        return params.get(node.id, False)
    elif isinstance(node, ast.Compare):
        left = _eval_ast(node.left, params)
        for op, comparator in zip(node.ops, node.comparators):
            right = _eval_ast(comparator, params)
            if type(op) not in SAFE_OPERATORS:
                raise ValueError(f"Unsupported operator: {type(op).__name__}")
            if not SAFE_OPERATORS[type(op)](left, right):
                return False
            left = right
        return True
    elif isinstance(node, ast.BoolOp):
        op_func = SAFE_OPERATORS[type(node.op)]
        values = [_eval_ast(v, params) for v in node.values]
        result = values[0]
        for v in values[1:]:
            result = op_func(result, v)
        return result
    elif isinstance(node, ast.UnaryOp):
        if type(node.op) not in SAFE_OPERATORS:
            raise ValueError(f"Unsupported operator: {type(node.op).__name__}")
        return SAFE_OPERATORS[type(node.op)](_eval_ast(node.operand, params))
    else:
        raise ValueError(f"Unsupported AST node: {type(node).__name__}")


class PlayExecutor:
    """Executes plays with given parameters."""

    def __init__(self, loader: PlayLoader):
        """Initialize the executor.

        Args:
            loader: PlayLoader instance for accessing play definitions.
        """
        self.loader = loader

    def validate_parameters(
        self,
        play: PlayDefinition,
        params: dict[str, Any],
    ) -> list[str]:
        """Validate parameters against play definition.

        Args:
            play: Play definition to validate against.
            params: Parameters provided by user.

        Returns:
            List of validation error messages (empty if valid).
        """
        errors = []

        for param_def in play.parameters:
            value = params.get(param_def.name)

            if param_def.required and value is None:
                errors.append(f"Missing required parameter: {param_def.name}")
                continue

            if value is None:
                continue

            # Type validation
            if param_def.type == "number" and not isinstance(value, (int, float)):
                try:
                    float(value)
                except (TypeError, ValueError):
                    errors.append(
                        f"Parameter {param_def.name} must be a number"
                    )

            elif param_def.type == "boolean" and not isinstance(value, bool):
                if str(value).lower() not in ("true", "false", "1", "0"):
                    errors.append(
                        f"Parameter {param_def.name} must be a boolean"
                    )

        return errors

    def prepare_steps(
        self,
        play: PlayDefinition,
        params: dict[str, Any],
    ) -> list[dict[str, Any]]:
        """Prepare play steps with rendered parameters.

        Args:
            play: Play definition.
            params: User-provided parameters.

        Returns:
            List of step dictionaries with rendered args.
        """
        # Fill in defaults
        full_params = {}
        for param_def in play.parameters:
            if param_def.name in params:
                full_params[param_def.name] = params[param_def.name]
            elif param_def.default is not None:
                full_params[param_def.name] = param_def.default

        # Render each step
        rendered_steps = []
        for step in play.steps:
            # Check condition if present
            # Note: simplified condition handling
            rendered_args = render_args(step.args, full_params)

            rendered_steps.append({
                "tool": step.tool,
                "args": rendered_args,
                "label": step.label,
                "destructive": step.destructive,
            })

        return rendered_steps


def get_default_loader(user_credentials: set[str] | None = None) -> PlayLoader:
    """Get the default play loader.

    Args:
        user_credentials: User's available credentials.

    Returns:
        Configured PlayLoader instance.
    """
    return PlayLoader(user_credentials=user_credentials)
