"""Semantic validation service for CleverAgents.

Provides semantic analysis checks for Python projects during the
strategize/execute phases: syntax errors, missing imports, broken
references, duplicate relative imports, API misuse, and missing
symbols.

Semantic checks are exposed as Validation tools attachable per
resource via the Tool Registry.  Results integrate into the
``ValidationPipeline`` as informational by default.

Architecture:
- ``SemanticValidationSeverity``: INFO / WARN / ERROR severity levels
- ``SemanticCheckResult``: normalised result with passed/message/data/severity
- ``SemanticValidationRule``: protocol for pluggable rule implementations
- Built-in rules in ``semantic_validation_rules`` companion module
- ``SemanticRuleRegistry``: register/lookup/list rules by name
- ``SemanticValidationCache``: file-hash keyed LRU cache to skip unchanged files
- ``SemanticValidationService``: orchestrator integrating with the pipeline
- Config keys: ``validation.semantic.enabled``,
  ``validation.semantic.python.enabled``,
  ``validation.semantic.severity_mapping``

Based on ``docs/specification.md`` and ADR-013 (Validation Abstraction).
"""

from __future__ import annotations

import hashlib
import threading
from collections import OrderedDict
from collections.abc import Mapping
from typing import Protocol, TypedDict, runtime_checkable

import structlog

from cleveragents.application.services.semantic_validation_rules import (
    APIMisuseRule,
    BrokenReferenceRule,
    DuplicateImportRule,
    MissingImportRule,
    MissingSymbolRule,
    SemanticCheckResult,
    SemanticValidationSeverity,
    SyntaxCheckRule,
)
from cleveragents.domain.models.core.tool import ValidationMode

logger = structlog.get_logger(__name__)


# ---------------------------------------------------------------------------
# Typed dicts for public return types
# ---------------------------------------------------------------------------


class PipelineResultDict(TypedDict):
    """Typed dict for pipeline-compatible result dictionaries."""

    passed: bool
    message: str
    data: dict[str, object] | None
    severity: str
    mode: str


class NormalisedOutputDict(TypedDict):
    """Typed dict for normalised output dictionaries."""

    passed: bool
    message: str
    data: dict[str, object] | None


# ---------------------------------------------------------------------------
# Config key constants
# ---------------------------------------------------------------------------

CONFIG_KEY_ENABLED = "validation.semantic.enabled"
CONFIG_KEY_PYTHON_ENABLED = "validation.semantic.python.enabled"
CONFIG_KEY_SEVERITY_MAPPING = "validation.semantic.severity_mapping"

_DEFAULT_SEVERITY_MAPPING: dict[str, str] = {
    "syntax_error": "error",
    "missing_import": "warn",
    "broken_reference": "warn",
    "duplicate_import": "error",
    "api_misuse": "warn",
    "missing_symbol": "info",
}

DEFAULT_CONFIG: dict[str, object] = {
    CONFIG_KEY_ENABLED: True,
    CONFIG_KEY_PYTHON_ENABLED: True,
    CONFIG_KEY_SEVERITY_MAPPING: _DEFAULT_SEVERITY_MAPPING,
}


# ---------------------------------------------------------------------------
# Rule protocol
# ---------------------------------------------------------------------------


@runtime_checkable
class SemanticValidationRule(Protocol):
    """Protocol for pluggable semantic validation rules.

    Each rule has a unique ``name`` and a ``check`` method that
    inspects source code and returns a normalised result.
    """

    @property
    def name(self) -> str:
        """Unique name of this validation rule."""
        ...  # pragma: no cover

    def check(self, source: str, filename: str) -> SemanticCheckResult:
        """Run the semantic check on source code.

        Args:
            source: Python source code to check.
            filename: Filename (for error messages).

        Returns:
            SemanticCheckResult with pass/fail and details.
        """
        ...  # pragma: no cover


# ---------------------------------------------------------------------------
# Rule registry
# ---------------------------------------------------------------------------


class SemanticRuleRegistry:
    """Registry for semantic validation rules.

    Supports registration, lookup by name, and listing of all
    registered rules.
    """

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

    def register(self, rule: SemanticValidationRule) -> None:
        """Register a rule.  Overwrites if name already exists."""
        self._rules[rule.name] = rule
        logger.debug("Registered semantic rule: %s", rule.name)

    def get(self, name: str) -> SemanticValidationRule | None:
        """Look up a rule by name."""
        return self._rules.get(name)

    def list_rules(self) -> list[str]:
        """Return sorted list of registered rule names."""
        return sorted(self._rules.keys())

    def all_rules(self) -> list[SemanticValidationRule]:
        """Return all registered rules sorted by name."""
        return [self._rules[n] for n in sorted(self._rules.keys())]

    def remove(self, name: str) -> bool:
        """Remove a rule by name.  Returns True if found."""
        if name in self._rules:
            del self._rules[name]
            return True
        return False

    def __len__(self) -> int:
        return len(self._rules)


def create_default_registry() -> SemanticRuleRegistry:
    """Create a registry pre-loaded with all built-in Python rules."""
    registry = SemanticRuleRegistry()
    registry.register(SyntaxCheckRule())
    registry.register(MissingImportRule())
    registry.register(BrokenReferenceRule())
    registry.register(DuplicateImportRule())
    registry.register(APIMisuseRule())
    registry.register(MissingSymbolRule())
    return registry


# ---------------------------------------------------------------------------
# Cache
# ---------------------------------------------------------------------------

_DEFAULT_CACHE_MAX_SIZE = 4096


class SemanticValidationCache:
    """File-hash keyed LRU cache for semantic check results.

    Avoids re-running checks on unchanged files by caching results
    keyed by ``(rule_name, file_hash)``.  Uses a bounded
    ``OrderedDict`` with LRU eviction to prevent unbounded memory
    growth.  All public methods are thread-safe.

    Args:
        max_size: Maximum number of entries before LRU eviction
            kicks in.  Defaults to ``4096``.
    """

    def __init__(self, max_size: int = _DEFAULT_CACHE_MAX_SIZE) -> None:
        if max_size < 1:
            raise ValueError("max_size must be >= 1")
        self._max_size = max_size
        self._cache: OrderedDict[str, SemanticCheckResult] = OrderedDict()
        self._lock = threading.Lock()

    @staticmethod
    def compute_hash(content: str) -> str:
        """Compute SHA-256 hex digest of source content."""
        return hashlib.sha256(content.encode("utf-8")).hexdigest()

    def _key(self, rule_name: str, file_hash: str) -> str:
        return f"{rule_name}:{file_hash}"

    def get(self, rule_name: str, file_hash: str) -> SemanticCheckResult | None:
        """Retrieve a cached result, or None if not cached."""
        key = self._key(rule_name, file_hash)
        with self._lock:
            if key in self._cache:
                self._cache.move_to_end(key)
                return self._cache[key]
            return None

    def put(self, rule_name: str, file_hash: str, result: SemanticCheckResult) -> None:
        """Store a result in the cache, evicting LRU entries if needed."""
        key = self._key(rule_name, file_hash)
        with self._lock:
            if key in self._cache:
                self._cache.move_to_end(key)
            self._cache[key] = result
            while len(self._cache) > self._max_size:
                self._cache.popitem(last=False)

    def invalidate(self, rule_name: str, file_hash: str) -> None:
        """Remove a cached entry."""
        key = self._key(rule_name, file_hash)
        with self._lock:
            self._cache.pop(key, None)

    def clear(self) -> None:
        """Clear all cached results."""
        with self._lock:
            self._cache.clear()

    def __len__(self) -> int:
        with self._lock:
            return len(self._cache)


# ---------------------------------------------------------------------------
# Severity mapping
# ---------------------------------------------------------------------------


def map_severity_to_mode(
    severity: SemanticValidationSeverity,
) -> ValidationMode:
    """Map a semantic severity to a ValidationPipeline mode.

    - ERROR -> REQUIRED (failure blocks)
    - WARN / INFO -> INFORMATIONAL (failure reported only)
    """
    if severity == SemanticValidationSeverity.ERROR:
        return ValidationMode.REQUIRED
    return ValidationMode.INFORMATIONAL


def resolve_severity(
    rule_name: str,
    severity_mapping: dict[str, str] | None = None,
) -> SemanticValidationSeverity:
    """Resolve the configured severity for a rule.

    Falls back to the default mapping if no override is provided.
    """
    mapping = severity_mapping or _DEFAULT_SEVERITY_MAPPING
    raw = mapping.get(rule_name, "info")
    try:
        return SemanticValidationSeverity(raw)
    except ValueError:
        return SemanticValidationSeverity.INFO


# ---------------------------------------------------------------------------
# Service
# ---------------------------------------------------------------------------


_PYTHON_EXTENSIONS: frozenset[str] = frozenset({".py", ".pyi"})


class SemanticValidationService:
    """Orchestrator for semantic validation checks.

    Integrates with the ``ValidationPipeline`` by producing
    normalised ``SemanticCheckResult`` objects and converting them
    to pipeline-compatible dicts.  Uses a rule registry and cache
    for extensibility and efficiency.

    Args:
        registry: Rule registry to use (default: built-in rules).
        cache: Cache instance (default: new empty cache).
        config: Configuration overrides (default: DEFAULT_CONFIG).
    """

    def __init__(
        self,
        registry: SemanticRuleRegistry | None = None,
        cache: SemanticValidationCache | None = None,
        config: Mapping[str, object] | None = None,
    ) -> None:
        self._registry = registry or create_default_registry()
        self._cache = cache or SemanticValidationCache()
        self._config = {**DEFAULT_CONFIG, **(config or {})}

    @property
    def registry(self) -> SemanticRuleRegistry:
        """The rule registry used by this service."""
        return self._registry

    @property
    def cache(self) -> SemanticValidationCache:
        """The result cache used by this service."""
        return self._cache

    @property
    def enabled(self) -> bool:
        """Whether semantic validation is globally enabled."""
        return bool(self._config.get(CONFIG_KEY_ENABLED, True))

    @property
    def python_enabled(self) -> bool:
        """Whether Python-specific semantic checks are enabled."""
        return bool(self._config.get(CONFIG_KEY_PYTHON_ENABLED, True))

    @staticmethod
    def _is_python_file(filename: str) -> bool:
        """Return True if *filename* has a Python extension."""
        return filename.endswith((".py", ".pyi"))

    def check_file(
        self,
        source: str,
        filename: str,
        rule_names: list[str] | None = None,
    ) -> list[SemanticCheckResult]:
        """Run semantic checks on a single file.

        Args:
            source: Python source code.
            filename: Filename for error messages.
            rule_names: Optional list of specific rules to run.
                If None, all registered rules are run.

        Returns:
            List of SemanticCheckResult objects.
        """
        if not self.enabled:
            return []

        # All built-in rules are Python-specific.  Skip non-Python
        # files entirely when Python checks are disabled, and also
        # skip non-Python files unconditionally since the rules use
        # ``ast.parse`` which only handles Python.
        if not self._is_python_file(filename):
            return []

        if not self.python_enabled:
            return []

        file_hash = self._cache.compute_hash(source)
        rules = (
            [r for n in rule_names if (r := self._registry.get(n)) is not None]
            if rule_names
            else self._registry.all_rules()
        )

        results: list[SemanticCheckResult] = []
        raw_mapping = self._config.get(CONFIG_KEY_SEVERITY_MAPPING)
        severity_mapping: dict[str, str] | None = None
        if isinstance(raw_mapping, dict):
            severity_mapping = {str(k): str(v) for k, v in raw_mapping.items()}

        for rule in rules:
            cached = self._cache.get(rule.name, file_hash)
            if cached is not None:
                results.append(cached)
                continue

            result = rule.check(source, filename)
            # Apply configured severity override
            configured = resolve_severity(rule.name, severity_mapping)
            if result.severity != configured:
                result = SemanticCheckResult(
                    passed=result.passed,
                    message=result.message,
                    data=result.data,
                    severity=configured,
                )
            self._cache.put(rule.name, file_hash, result)
            results.append(result)

        return results

    def as_pipeline_results(
        self,
        source: str,
        filename: str,
        rule_names: list[str] | None = None,
    ) -> list[PipelineResultDict]:
        """Run checks and return pipeline-compatible result dicts.

        Each dict has ``passed``, ``message``, ``data``, ``severity``,
        and ``mode`` keys for integration with ``ValidationPipeline``.
        """
        checks = self.check_file(source, filename, rule_names)
        pipeline_results: list[PipelineResultDict] = []
        for result in checks:
            mode = map_severity_to_mode(result.severity)
            pipeline_results.append(
                PipelineResultDict(
                    passed=result.passed,
                    message=result.message,
                    data=result.data,
                    severity=result.severity.value,
                    mode=mode.value,
                )
            )
        return pipeline_results

    def normalise_output(self, result: SemanticCheckResult) -> NormalisedOutputDict:
        """Normalise a check result to the standard output schema.

        Returns a dict with ``passed``, ``message``, and ``data`` keys.
        """
        return NormalisedOutputDict(
            passed=result.passed,
            message=result.message,
            data=result.data,
        )
