"""Engine compatibility specification generator.

This module generates a compatibility spec that declares what the infer engine
supports. The `models` system consumes this spec to filter compatible models
before download.
"""

from __future__ import annotations

import importlib.metadata
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

import appinfra.time
import yaml
from appinfra.log import Logger

# Known model types that work with LlamaArchitecture (default)
# These are not explicitly in ARCHITECTURES but are known to work
DEFAULT_ARCHITECTURE_TYPES = ["llama", "qwen", "qwen2"]

# Cached ARCHITECTURES to avoid repeated slow imports
_architectures_cache: dict[str, type] | None = None


def _get_architectures(lg: Logger) -> dict[str, type]:
    """Get ARCHITECTURES registry with deferred import and caching.

    The import is deferred because it pulls in torch (~4s).

    Args:
        lg: Logger instance.
    """
    global _architectures_cache
    if _architectures_cache is not None:
        return _architectures_cache

    lg.debug("importing model architectures...")
    start = appinfra.time.start()

    from .engines.native.model.architecture import ARCHITECTURES

    lg.debug(
        "model architectures imported",
        extra={"after": appinfra.time.since_str(start)},
    )

    _architectures_cache = ARCHITECTURES
    return _architectures_cache


def get_spec_header(template_path: Path | None = None) -> str:
    """Extract header comments from template and append generation info.

    Reads leading comment lines from the template file and appends
    a "Generated by" notice.

    Args:
        template_path: Path to template file. If None, uses default.

    Returns:
        Header string to prepend to generated specs.
    """
    if template_path is None:
        template_path = _get_default_template_path()

    header_lines: list[str] = []

    try:
        with open(template_path) as f:
            for line in f:
                # Stop at first non-comment, non-empty line
                stripped = line.strip()
                if stripped and not stripped.startswith("#"):
                    break
                header_lines.append(line.rstrip())
    except FileNotFoundError:
        pass

    # Append generation notice
    header_lines.append("#")
    header_lines.append("# Generated by: infer compat generate")
    header_lines.append("# Validate with: infer compat check <this-file>")
    header_lines.append("")

    return "\n".join(header_lines) + "\n"


def _get_default_template_path() -> Path:
    """Get default template path (etc/compat_template.yaml relative to package)."""
    project_root = Path(__file__).parent.parent
    return project_root / "etc" / "compat_template.yaml"


def load_template(template_path: Path | None = None) -> dict[str, Any]:
    """Load the compat template YAML file.

    Args:
        template_path: Path to template file. If None, uses default.

    Returns:
        Parsed template as a dictionary.

    Raises:
        FileNotFoundError: If template file doesn't exist.
    """
    if template_path is None:
        template_path = _get_default_template_path()

    with open(template_path) as f:
        result: dict[str, Any] = yaml.safe_load(f)
        return result


def get_version() -> str:
    """Get engine version from pyproject.toml.

    Returns:
        Version string (e.g., "0.2.0").
    """
    try:
        return importlib.metadata.version("llm-infer")
    except importlib.metadata.PackageNotFoundError:
        return "unknown"


def get_supported_model_types(lg: Logger) -> list[str]:
    """Get list of supported model types.

    Combines:
    - Keys from ARCHITECTURES (explicitly registered)
    - Known types that work with LlamaArchitecture (default)

    Args:
        lg: Logger instance.

    Returns:
        Sorted list of model type strings.
    """
    model_types = set(_get_architectures(lg).keys())
    model_types.update(DEFAULT_ARCHITECTURE_TYPES)
    return sorted(model_types)


def generate_compat_spec(
    lg: Logger, template_path: Path | None = None
) -> dict[str, Any]:
    """Generate the complete compatibility specification.

    Loads the template and fills in dynamic values:
    - engine.version: from pyproject.toml
    - generated_at: current timestamp
    - architecture.model_types: from ARCHITECTURES + defaults

    Args:
        lg: Logger instance.
        template_path: Path to template file. If None, uses default.

    Returns:
        Complete spec dictionary ready for serialization.
    """
    spec = load_template(template_path)

    # Fill dynamic values
    spec["generated_at"] = datetime.now(UTC).isoformat()
    spec["engine"]["version"] = get_version()
    spec["architecture"]["model_types"] = get_supported_model_types(lg)

    return spec


def _load_spec_file(
    spec_file: str | Path | None, template_path: Path | None = None
) -> tuple[dict | None, str | None]:
    """Load spec file. Returns (spec, error_message)."""
    try:
        if spec_file:
            with open(spec_file) as f:
                return yaml.safe_load(f), None
        return load_template(template_path), None
    except FileNotFoundError:
        return None, f"File not found: {spec_file or 'compat_template.yaml'}"
    except yaml.YAMLError as e:
        return None, f"YAML parse error: {e}"


def _validate_spec(lg: Logger, spec: dict) -> list[str]:
    """Validate spec contents. Returns list of issues."""
    issues: list[str] = []

    # Check engine name
    if spec.get("engine", {}).get("name") != "infer":
        issues.append("Engine name mismatch: expected 'infer'")

    # Check model_types
    spec_types = set(spec.get("architecture", {}).get("model_types", []))
    expected = set(get_supported_model_types(lg))
    if missing := expected - spec_types:
        issues.append(f"Missing model_types: {sorted(missing)}")
    if extra := spec_types - expected:
        issues.append(f"Extra model_types: {sorted(extra)}")

    # Check required sections
    for section in ["version", "engine", "architecture", "format", "unsupported_tags"]:
        if section not in spec:
            issues.append(f"Missing section: {section}")

    return issues


def check_spec_accuracy(
    lg: Logger,
    spec_file: str | Path | None = None,
    template_path: Path | None = None,
) -> tuple[bool, list[str]]:
    """Verify that a spec file accurately reflects implementation.

    Args:
        lg: Logger instance.
        spec_file: Path to spec file to check. If None, checks template.
        template_path: Path to template file. If None, uses default.

    Returns:
        Tuple of (is_valid, list_of_issues).
    """
    spec, error = _load_spec_file(spec_file, template_path)
    if error or spec is None:
        return False, [error or "Failed to load spec"]
    issues = _validate_spec(lg, spec)
    return len(issues) == 0, issues
