"""
Minimal ExtensionLoader for EmpowerNow.

Scans extension roots, loads and validates extension.yaml manifests,
resolves service-specific plugin/config paths, and hands them to
service startup routines (PDP, Membership, etc.).
"""

from __future__ import annotations

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

import yaml

logger = logging.getLogger(__name__)

REQUIRED_MANIFEST_FIELDS = {"id", "version", "enabled"}
VALID_STATUSES = {"prototype", "preview", "ga", "deprecated"}
VALID_SURFACES = {
    "pdp_resource_handler",
    "config_pack",
    "policy_pack",
    "sidecar_adapter",
    "membership_projection",
    "experience_plugin",
}


@dataclass(frozen=True)
class ExtensionCapability:
    """A single declared capability within an extension manifest."""

    kind: str  # e.g. "pdp_resource_handlers", "membership_projections"
    id: str
    raw: dict = field(default_factory=dict, repr=False)


@dataclass
class ExtensionManifest:
    """Parsed and validated extension.yaml."""

    id: str
    namespace: str
    version: str
    status: str
    enabled: bool
    root: Path

    owner: dict = field(default_factory=dict)
    host_compatibility: dict = field(default_factory=dict)
    surfaces: list[str] = field(default_factory=list)
    capabilities: dict[str, list[ExtensionCapability]] = field(default_factory=dict)
    dependencies: dict = field(default_factory=dict)

    raw: dict = field(default_factory=dict, repr=False)

    def resolve_path(self, ref: str) -> Path:
        """Resolve a manifest-relative path reference to an absolute path."""
        return self.root / ref

    def get_capability_ids(self, kind: str) -> list[str]:
        """Return the IDs of all capabilities of a given kind."""
        return [c.id for c in self.capabilities.get(kind, [])]


class ManifestValidationError(Exception):
    """Raised when an extension.yaml fails validation."""


class ExtensionCollisionError(Exception):
    """Raised when two extensions declare the same ID or resource type."""


class ExtensionLoader:
    """
    Scans extension directories, loads manifests, validates them,
    and provides a registry of enabled extensions for service startup.
    """

    def __init__(self) -> None:
        self._manifests: dict[str, ExtensionManifest] = {}

    @property
    def manifests(self) -> dict[str, ExtensionManifest]:
        return dict(self._manifests)

    @property
    def enabled_manifests(self) -> dict[str, ExtensionManifest]:
        return {k: v for k, v in self._manifests.items() if v.enabled}

    def scan(self, *roots: str | Path) -> list[ExtensionManifest]:
        """
        Scan one or more root directories for extension packages.

        Each immediate subdirectory containing an extension.yaml is treated
        as an extension package root.

        Returns the list of successfully loaded manifests.
        Raises ExtensionCollisionError on duplicate extension IDs.
        """
        loaded: list[ExtensionManifest] = []
        for root in roots:
            root_path = Path(root)
            if not root_path.is_dir():
                logger.warning("Extension root does not exist: %s", root_path)
                continue

            for child in sorted(root_path.iterdir()):
                if not child.is_dir():
                    continue
                manifest_path = child / "extension.yaml"
                if not manifest_path.is_file():
                    continue

                try:
                    manifest = self._load_and_validate(manifest_path)
                except ManifestValidationError as e:
                    logger.error(
                        "Skipping invalid extension",
                        extra={"path": str(child), "error": str(e)},
                    )
                    continue

                if manifest.id in self._manifests:
                    raise ExtensionCollisionError(
                        f"Duplicate extension ID '{manifest.id}': "
                        f"already loaded from {self._manifests[manifest.id].root}, "
                        f"collision with {child}"
                    )

                self._manifests[manifest.id] = manifest
                loaded.append(manifest)
                logger.info(
                    "Loaded extension",
                    extra={
                        "extension_id": manifest.id,
                        "version": manifest.version,
                        "status": manifest.status,
                        "enabled": manifest.enabled,
                    },
                )

        return loaded

    def get_paths_for_service(
        self, service: str, capability_kind: str
    ) -> list[tuple[ExtensionManifest, list[Path]]]:
        """
        For each enabled extension that declares capabilities of the given kind,
        resolve the paths relevant to the named service.

        Returns a list of (manifest, [resolved_paths]) tuples.
        """
        results: list[tuple[ExtensionManifest, list[Path]]] = []
        for manifest in self._manifests.values():
            if not manifest.enabled:
                continue
            caps = manifest.capabilities.get(capability_kind, [])
            if not caps:
                continue

            paths: list[Path] = []
            for cap in caps:
                ref = cap.raw.get("class", "") or cap.raw.get("ref", "")
                if ref:
                    module_dir = ref.rsplit(".", 1)[0].replace(".", os.sep) if "." in ref else ref
                    resolved = manifest.root / module_dir
                    parent = resolved.parent if resolved.suffix else resolved
                    if parent.is_dir():
                        paths.append(parent)
                    elif manifest.root.is_dir():
                        paths.append(manifest.root)

            if paths:
                results.append((manifest, paths))
            elif caps:
                results.append((manifest, [manifest.root]))

        return results

    def get_projection_declarations(
        self,
    ) -> list[tuple[ExtensionManifest, list[dict[str, Any]]]]:
        """
        Return all membership_projections capability declarations
        from enabled extensions.
        """
        results: list[tuple[ExtensionManifest, list[dict[str, Any]]]] = []
        for manifest in self._manifests.values():
            if not manifest.enabled:
                continue
            caps = manifest.capabilities.get("membership_projections", [])
            if caps:
                results.append((manifest, [c.raw for c in caps]))
        return results

    def _load_and_validate(self, manifest_path: Path) -> ExtensionManifest:
        """Load a single extension.yaml and validate it."""
        with open(manifest_path, "r", encoding="utf-8") as f:
            raw = yaml.safe_load(f) or {}

        missing = REQUIRED_MANIFEST_FIELDS - set(raw.keys())
        if missing:
            raise ManifestValidationError(
                f"Missing required fields: {missing}"
            )

        ext_id = raw["id"]
        if not isinstance(ext_id, str) or not ext_id.strip():
            raise ManifestValidationError("'id' must be a non-empty string")

        status = raw.get("status", "prototype")
        if status not in VALID_STATUSES:
            raise ManifestValidationError(
                f"Invalid status '{status}'. Must be one of: {VALID_STATUSES}"
            )

        for surface in raw.get("surfaces", []):
            if surface not in VALID_SURFACES:
                logger.warning(
                    "Unknown surface '%s' in extension '%s' — ignoring. "
                    "Known surfaces: %s",
                    surface, ext_id, VALID_SURFACES,
                )

        capabilities: dict[str, list[ExtensionCapability]] = {}
        for kind, items in raw.get("capabilities", {}).items():
            if not isinstance(items, list):
                raise ManifestValidationError(
                    f"capabilities.{kind} must be a list"
                )
            caps = []
            for item in items:
                if isinstance(item, dict):
                    cap_id = item.get("id", f"{kind}_{len(caps)}")
                    caps.append(ExtensionCapability(kind=kind, id=cap_id, raw=item))
                elif isinstance(item, str):
                    caps.append(ExtensionCapability(kind=kind, id=item, raw={"ref": item}))
            capabilities[kind] = caps

        return ExtensionManifest(
            id=ext_id,
            namespace=raw.get("namespace", ext_id),
            version=raw["version"],
            status=status,
            enabled=bool(raw.get("enabled", True)),
            root=manifest_path.parent,
            owner=raw.get("owner", {}),
            host_compatibility=raw.get("host_compatibility", {}),
            surfaces=raw.get("surfaces", []),
            capabilities=capabilities,
            dependencies=raw.get("dependencies", {}),
            raw=raw,
        )
