"""Blueprint loader for Autopilot Architect."""

from __future__ import annotations

import hashlib
import logging
from pathlib import Path
from typing import Any, TYPE_CHECKING

import yaml

from g_gremlin.paths import gremlin_path

from .models import BlueprintDefinition, BlueprintPhase, BlueprintSafety, PlayParameter, PlayStep
from .plays import render_args, render_template

if TYPE_CHECKING:
    from .tools import ToolRegistry

logger = logging.getLogger(__name__)

from g_gremlin.config import get_config_dir as _get_config_dir

DEFAULT_BLUEPRINTS_DIR = _get_config_dir() / "blueprints"
USER_BLUEPRINTS_DIR = gremlin_path("blueprints")


class BlueprintLoader:
    """Loads and manages Architect blueprint definitions."""

    def __init__(
        self,
        blueprints_dirs: list[Path] | None = None,
        user_credentials: set[str] | None = None,
    ) -> None:
        self.blueprints_dirs = blueprints_dirs or [DEFAULT_BLUEPRINTS_DIR, USER_BLUEPRINTS_DIR]
        self.user_credentials = user_credentials or set()
        self._blueprints: dict[str, BlueprintDefinition] = {}
        self._paths: dict[str, Path] = {}
        self._load_all()

    def _load_all(self) -> None:
        for blueprints_dir in self.blueprints_dirs:
            if not blueprints_dir.exists():
                continue
            for yaml_file in blueprints_dir.glob("*.yaml"):
                try:
                    blueprint = self._load_blueprint(yaml_file)
                    if blueprint:
                        self._blueprints[blueprint.name] = blueprint
                        self._paths[blueprint.name] = yaml_file
                except Exception as exc:
                    logger.warning("Failed to load blueprint %s: %s", yaml_file, exc)

    def _load_blueprint(self, path: Path) -> BlueprintDefinition | None:
        data = yaml.safe_load(path.read_text(encoding="utf-8"))
        if not data:
            return None

        variants = data.get("variants") or []
        required_creds = data.get("requires_credentials", [])
        if not variants:
            required_list = _resolve_requires_credentials(required_creds, None)
            if required_list and not all(c in self.user_credentials for c in required_list):
                logger.debug("Skipping blueprint %s - missing credentials", data.get("name"))
                return None

        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"),
                when=param_data.get("when"),
            ))

        phases: dict[str, BlueprintPhase] = {}
        for phase_name, phase_data in (data.get("phases") or {}).items():
            steps = []
            for step_data in phase_data.get("steps", []) or []:
                when_condition = step_data.get("when") or step_data.get("condition")
                steps.append(PlayStep(
                    tool=step_data.get("tool"),
                    args={str(k): v for k, v in (step_data.get("args", {}) or {}).items()},
                    command=step_data.get("command"),
                    shell=step_data.get("shell", False),
                    label=step_data.get("label", step_data.get("name", phase_name)),
                    destructive=step_data.get("destructive", False),
                    when=when_condition,
                    name=step_data.get("name"),
                ))

            phases[phase_name] = BlueprintPhase(
                label=phase_data.get("label", phase_name),
                steps=steps,
                outputs=phase_data.get("outputs", []) or [],
                approval_required=phase_data.get("approval_required", False),
                llm_assisted=phase_data.get("llm_assisted", False),
                prompt_template=phase_data.get("prompt_template"),
                target_org=phase_data.get("target_org"),
            )

        safety_data = data.get("safety") or {}
        safety = BlueprintSafety(
            max_custom_fields=safety_data.get("max_custom_fields"),
            max_validation_rules=safety_data.get("max_validation_rules"),
            max_custom_properties=safety_data.get("max_custom_properties"),
            max_workflows=safety_data.get("max_workflows"),
            requires_sandbox_verification=safety_data.get("requires_sandbox_verification", True),
            rollback_plan=safety_data.get("rollback_plan"),
        )

        return BlueprintDefinition(
            name=data["name"],
            description=data.get("description", ""),
            parameters=parameters,
            phases=phases,
            requires_credentials=required_creds or [],
            variants=variants,
            requires_tools=data.get("requires_tools", {}) or {},
            safety=safety,
            verification_checklist=data.get("verification_checklist", []) or [],
        )

    def list_blueprints(self) -> list[BlueprintDefinition]:
        return list(self._blueprints.values())

    def get_blueprint(self, name: str) -> BlueprintDefinition | None:
        return self._blueprints.get(name)

    def resolve_blueprint(
        self,
        name: str,
        variant: str,
        *,
        registry: "ToolRegistry | None" = None,
    ) -> BlueprintDefinition:
        """Return blueprint resolved to the active variant phases."""
        blueprint = self.get_blueprint(name)
        if not blueprint:
            raise ValueError(f"Blueprint not found: {name}")

        if blueprint.variants and variant not in blueprint.variants:
            raise ValueError(f"Variant '{variant}' not supported for blueprint '{name}'")

        variants = blueprint.variants or []
        resolved_phases: dict[str, BlueprintPhase] = {}
        for phase_name, phase in blueprint.phases.items():
            suffix_variant = _match_variant_suffix(phase_name, variants)
            if suffix_variant:
                if suffix_variant != variant:
                    continue
                logical_name = phase_name[: -(len(suffix_variant) + 1)]
            else:
                logical_name = phase_name
            resolved_phases[logical_name] = phase.model_copy()

        resolved_creds = _resolve_requires_credentials(blueprint.requires_credentials, variant)

        if registry and blueprint.requires_tools:
            required_tools = blueprint.requires_tools.get(variant) or []
            missing = [tool for tool in required_tools if not registry.get_handler(tool)]
            if missing:
                raise ValueError(
                    f"Missing required tools for {name} ({variant}): {', '.join(missing)}"
                )

        return blueprint.model_copy(
            update={
                "phases": resolved_phases,
                "requires_credentials": resolved_creds,
                "active_variant": variant,
            }
        )

    def compute_blueprint_hash(self, name: str, *, active_variant: str | None = None) -> str:
        path = self._paths.get(name)
        if not path:
            raise ValueError(f"Blueprint not found: {name}")
        base_text = path.read_text(encoding="utf-8")
        digest = hashlib.sha256(base_text.encode("utf-8"))

        blueprint = self._blueprints.get(name)
        variant_value = active_variant or (blueprint.active_variant if blueprint else None)
        if variant_value:
            digest.update(variant_value.encode("utf-8"))
        if blueprint:
            for phase in blueprint.phases.values():
                if not phase.prompt_template:
                    continue
                prompt_path = _resolve_prompt_path(path, phase.prompt_template)
                if prompt_path.exists():
                    prompt_text = prompt_path.read_text(encoding="utf-8")
                    digest.update(prompt_text.encode("utf-8"))

        return f"sha256:{digest.hexdigest()}"

    def reload(self) -> None:
        self._blueprints.clear()
        self._paths.clear()
        self._load_all()


def _resolve_prompt_path(blueprint_path: Path, prompt_template: str) -> Path:
    candidate = Path(prompt_template)
    if candidate.is_absolute():
        return candidate
    return (blueprint_path.parent / prompt_template).resolve()


def render_phase_args(args: dict[str, Any], params: dict[str, Any]) -> dict[str, Any]:
    """Render args dict using play template syntax."""
    return render_args(args, params)


def render_phase_command(command: str, params: dict[str, Any]) -> str:
    """Render command string using play template syntax."""
    return render_template(command, params)


def _match_variant_suffix(phase_name: str, variants: list[str]) -> str | None:
    for variant in variants:
        if phase_name.endswith(f"_{variant}"):
            return variant
    return None


def _resolve_requires_credentials(
    requires_credentials: list[str] | dict[str, list[str]],
    variant: str | None,
) -> list[str]:
    if isinstance(requires_credentials, dict):
        if variant:
            return list(requires_credentials.get(variant) or [])
        flattened: list[str] = []
        for items in requires_credentials.values():
            if items:
                flattened.extend(items)
        # preserve order, remove dupes
        return list(dict.fromkeys(flattened))
    if isinstance(requires_credentials, list):
        return list(requires_credentials)
    return []


__all__ = [
    "BlueprintLoader",
    "DEFAULT_BLUEPRINTS_DIR",
    "USER_BLUEPRINTS_DIR",
    "render_phase_args",
    "render_phase_command",
    "_resolve_requires_credentials",
]
