"""Standard-template catalogue loader for Terraform stacks.

Discovers the curated catalogue at
``alphaswarm_platform/configs/terraform/templates/*.yaml`` and turns
each :class:`TerraformStackSpec` into a typed :class:`Template`
descriptor with a parameter contract derived from the spec's
``variables`` block. The result is process-cached so the loader can
be safely called from REST routes / MCP tool bodies / CLI without
paying the YAML-scan cost on every invocation.

Discovery order (first hit wins):

1. Explicit ``search_dir`` argument to :func:`load_templates` /
   :func:`reload_templates`.
2. Environment variable ``ALPHASWARM_PLATFORM_TEMPLATES_DIR``.
3. ``settings.alphaswarm_platform_templates_dir`` if set.
4. Workspace-relative default — walk up from this module's path
   looking for a sibling ``alphaswarm_platform/configs/terraform/templates/``
   directory. Works for the canonical multi-repo workspace layout.

Public surface (used by the REST + CLI + MCP wrappers):

- :class:`TemplateParameter` — one tunable parameter (mirrors a
  :class:`alphaswarm.terraform.spec.TerraformVariableRef`).
- :class:`Template` — the spec + the derived parameter list +
  catalogue annotations + spec hash.
- :func:`list_templates` — every discovered template.
- :func:`get_template` — single template by slug.
- :func:`instantiate_template` — apply user overrides to the
  template's ``variables`` defaults and return a fresh,
  re-validated :class:`TerraformStackSpec` ready for
  :func:`alphaswarm.terraform.registry.persist_spec`.
- :func:`reload_templates` — drop the cache (admin-only).

Hard rules:

- Every instantiation flows through :class:`TerraformStackSpec` strict
  validation so the persisted hash matches AGENTS rule 43.
- The loader NEVER reads a credential out of a template; sensitive
  values stay as :class:`TerraformVariableRef` defaults marked
  ``sensitive=True`` and are resolved by the runner pod via
  :class:`CredentialResolver` (rule 26).
"""
from __future__ import annotations

import logging
import os
import threading
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any

from alphaswarm.terraform.spec import TerraformStackSpec, TerraformVariableRef

logger = logging.getLogger(__name__)


# ---------------------------------------------------------------------------
# Types
# ---------------------------------------------------------------------------


@dataclass(frozen=True, slots=True)
class TemplateParameter:
    """One user-tunable parameter on a template.

    Mirrors :class:`TerraformVariableRef` but presents a simpler
    surface for REST / MCP / CLI consumers (`required` is True
    when no default is set; `sensitive` flags credentials).
    """

    name: str
    type: str
    description: str | None = None
    default: Any = None
    required: bool = False
    sensitive: bool = False

    @classmethod
    def from_variable(cls, var: TerraformVariableRef) -> TemplateParameter:
        return cls(
            name=var.name,
            type=var.type,
            description=var.description,
            default=var.default,
            required=var.default is None,
            sensitive=var.sensitive,
        )

    def to_dict(self) -> dict[str, Any]:
        return {
            "name": self.name,
            "type": self.type,
            "description": self.description,
            "default": self.default,
            "required": self.required,
            "sensitive": self.sensitive,
        }


@dataclass(frozen=True, slots=True)
class Template:
    """A discovered standard-template entry."""

    slug: str
    name: str
    description: str | None
    cloud_provider: str
    environment: str
    module_kind: str
    spec_hash: str
    parameters: tuple[TemplateParameter, ...]
    annotations: dict[str, str]
    source_path: str
    spec: TerraformStackSpec = field(repr=False)

    def to_summary(self) -> dict[str, Any]:
        """Compact catalogue listing — used by ``list_templates``."""
        return {
            "slug": self.slug,
            "name": self.name,
            "description": self.description,
            "cloud_provider": self.cloud_provider,
            "environment": self.environment,
            "module_kind": self.module_kind,
            "spec_hash": self.spec_hash,
            "parameter_count": len(self.parameters),
            "annotations": dict(self.annotations),
        }

    def to_detail(self) -> dict[str, Any]:
        """Detail view — used by ``describe_template``."""
        return {
            **self.to_summary(),
            "parameters": [p.to_dict() for p in self.parameters],
            "source_path": self.source_path,
        }


class TemplateNotFoundError(LookupError):
    """Raised when a slug doesn't match any discovered template."""


class TemplateValidationError(ValueError):
    """Raised when an override fails the spec's strict re-validation."""


# ---------------------------------------------------------------------------
# Discovery
# ---------------------------------------------------------------------------


_LOCK = threading.RLock()
_CACHE: dict[str, Template] | None = None
_CACHE_DIR: Path | None = None


def _candidate_dirs() -> list[Path]:
    """Return the search-path priority list for template discovery."""
    candidates: list[Path] = []
    env_dir = os.environ.get("ALPHASWARM_PLATFORM_TEMPLATES_DIR")
    if env_dir:
        candidates.append(Path(env_dir))
    # settings hook (optional — keeps Settings dependency-light).
    try:
        from alphaswarm.config.settings import get_settings  # type: ignore[import-not-found]

        settings = get_settings()
        custom = getattr(settings, "alphaswarm_platform_templates_dir", "") or ""
        if custom:
            candidates.append(Path(custom))
    except Exception:  # noqa: BLE001 — Settings import is optional
        pass
    # Workspace-relative default.
    here = Path(__file__).resolve()
    for parent in here.parents:
        candidate = parent / "alphaswarm_platform" / "configs" / "terraform" / "templates"
        if candidate.exists():
            candidates.append(candidate)
            break
    # Co-located fallback for unit tests / packaged installs.
    candidates.append(
        Path(__file__).resolve().parent.parent.parent
        / "configs"
        / "terraform"
        / "templates"
    )
    return candidates


def _resolve_search_dir(search_dir: str | Path | None) -> Path | None:
    """Pick the first existing directory from the candidate list."""
    if search_dir:
        path = Path(search_dir)
        return path if path.exists() else None
    for candidate in _candidate_dirs():
        if candidate.exists():
            return candidate
    return None


def _build_template(spec: TerraformStackSpec, source_path: Path) -> Template:
    parameters = tuple(
        TemplateParameter.from_variable(v) for v in spec.variables
    )
    return Template(
        slug=spec.slug or spec.name,
        name=spec.name,
        description=spec.description,
        cloud_provider=spec.cloud_provider,
        environment=spec.environment,
        module_kind=spec.module_kind,
        spec_hash=spec.snapshot_hash(),
        parameters=parameters,
        annotations=dict(spec.annotations or {}),
        source_path=str(source_path),
        spec=spec,
    )


def _load_directory(search_dir: Path) -> dict[str, Template]:
    """Read every ``*.yaml`` in ``search_dir`` and return a slug -> Template map.

    Files that fail strict :class:`TerraformStackSpec` validation are
    logged and skipped; the loader stays tolerant so a single broken
    template doesn't take the catalogue offline.
    """
    out: dict[str, Template] = {}
    for path in sorted(search_dir.glob("*.yaml")):
        try:
            spec = TerraformStackSpec.from_yaml_str(
                path.read_text(encoding="utf-8")
            )
        except Exception as exc:  # noqa: BLE001 — keep catalogue boot tolerant
            logger.warning(
                "skipping invalid template %s: %s", path.name, exc
            )
            continue
        template = _build_template(spec, path)
        if template.slug in out:
            logger.warning(
                "duplicate template slug %r at %s (overrides %s)",
                template.slug,
                path,
                out[template.slug].source_path,
            )
        out[template.slug] = template
    return out


def reload_templates(search_dir: str | Path | None = None) -> dict[str, Template]:
    """Drop the cache and re-scan the catalogue."""
    global _CACHE, _CACHE_DIR
    with _LOCK:
        resolved = _resolve_search_dir(search_dir)
        if resolved is None:
            logger.info("no templates directory found; catalogue is empty")
            _CACHE = {}
            _CACHE_DIR = None
            return {}
        _CACHE = _load_directory(resolved)
        _CACHE_DIR = resolved
        logger.info(
            "loaded %d terraform stack templates from %s",
            len(_CACHE),
            resolved,
        )
        return dict(_CACHE)


def _ensure_loaded(search_dir: str | Path | None = None) -> dict[str, Template]:
    """Idempotent loader — only re-reads when the cache is empty."""
    with _LOCK:
        if _CACHE is None:
            return reload_templates(search_dir)
        return dict(_CACHE)


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def list_templates(
    *,
    cloud_provider: str | None = None,
    environment: str | None = None,
    module_kind: str | None = None,
    search_dir: str | Path | None = None,
) -> list[Template]:
    """Return every template, optionally filtered by cloud / env / kind."""
    items = list(_ensure_loaded(search_dir).values())
    if cloud_provider:
        items = [t for t in items if t.cloud_provider == cloud_provider]
    if environment:
        items = [t for t in items if t.environment == environment]
    if module_kind:
        items = [t for t in items if t.module_kind == module_kind]
    return sorted(items, key=lambda t: t.slug)


def get_template(
    slug: str,
    *,
    search_dir: str | Path | None = None,
) -> Template:
    """Look up a single template by slug.

    Raises :class:`TemplateNotFoundError` when the slug is unknown.
    """
    cache = _ensure_loaded(search_dir)
    template = cache.get(slug)
    if template is None:
        raise TemplateNotFoundError(
            f"template slug {slug!r} not found; "
            f"available: {sorted(cache.keys())}"
        )
    return template


def instantiate_template(
    slug: str,
    *,
    name: str | None = None,
    new_slug: str | None = None,
    overrides: dict[str, Any] | None = None,
    organization_id: str | None = None,
    workspace_id: str | None = None,
    common_tags: dict[str, str] | None = None,
    search_dir: str | Path | None = None,
) -> TerraformStackSpec:
    """Apply user overrides to a template and return a fresh spec.

    The returned :class:`TerraformStackSpec` is freshly validated
    (``extra='forbid'`` enforced) and carries an updated
    ``snapshot_hash``. Pass it to
    :func:`alphaswarm.terraform.registry.persist_spec` to land a
    ``terraform_stack_spec_versions`` row.

    Args:
        slug: Catalogue slug (e.g. ``aws-cell-shared-std``).
        name: Override the spec ``name`` (default: keep template's).
        new_slug: Override the spec ``slug`` (default: keep template's).
        overrides: Map of ``variable_name -> new_default``. Unknown
            keys raise :class:`TemplateValidationError` so typos
            don't silently no-op.
        organization_id, workspace_id: Tenancy stamps that downstream
            tagging picks up.
        common_tags: Additional tags merged into the spec's
            ``common_tags`` map.
        search_dir: Override the catalogue search path (test-only).

    Returns:
        A fresh :class:`TerraformStackSpec` ready for the
        registry / runtime path.
    """
    template = get_template(slug, search_dir=search_dir)
    overrides = dict(overrides or {})

    # Validate override keys against the template's declared variables.
    declared = {v.name for v in template.spec.variables}
    unknown = [k for k in overrides if k not in declared]
    if unknown:
        raise TemplateValidationError(
            f"unknown override keys for template {slug!r}: {sorted(unknown)}; "
            f"declared variables: {sorted(declared)}"
        )

    # Build a fresh variables list with the overrides applied.
    new_variables: list[TerraformVariableRef] = []
    for var in template.spec.variables:
        if var.name in overrides:
            new_variables.append(
                TerraformVariableRef(
                    name=var.name,
                    type=var.type,
                    default=overrides[var.name],
                    description=var.description,
                    sensitive=var.sensitive,
                )
            )
        else:
            new_variables.append(var)

    # Required (no-default) parameters MUST be satisfied at instantiation.
    missing = [
        v.name for v in new_variables if v.default is None and not v.sensitive
    ]
    if missing:
        raise TemplateValidationError(
            f"template {slug!r} requires explicit values for: {sorted(missing)}"
        )

    merged_tags = dict(template.spec.common_tags or {})
    merged_tags.update(common_tags or {})
    merged_tags.setdefault(
        "alphaswarm.io/template-slug", template.slug
    )
    merged_tags.setdefault(
        "alphaswarm.io/template-source-hash", template.spec_hash
    )

    payload = template.spec.model_dump(mode="python")
    payload["variables"] = [v.model_dump(mode="python") for v in new_variables]
    payload["common_tags"] = merged_tags
    if name:
        payload["name"] = name
    if new_slug:
        payload["slug"] = new_slug
    if organization_id:
        payload["organization_id"] = organization_id
    if workspace_id:
        payload["workspace_id"] = workspace_id

    # Fresh strict validation so the returned hash is deterministic.
    return TerraformStackSpec.model_validate(payload)


__all__ = [
    "Template",
    "TemplateNotFoundError",
    "TemplateParameter",
    "TemplateValidationError",
    "get_template",
    "instantiate_template",
    "list_templates",
    "reload_templates",
]
