"""Shared helpers for resolving emitter templates from multiple roots.

XML templates are the Single Source of Truth (SSOT) for document structure.
Emitters load templates and modify attribute values - they never construct
new XML elements programmatically.

Caching:
    Class-level caches ensure templates are loaded only once per process,
    improving performance when multiple emitters are instantiated.

    Two caches are maintained:
    - _GLOBAL_CACHE: Raw string templates (for read_text())
    - _TREE_CACHE: Parsed lxml Element trees (for read_tree())

    Both caches share the same thread-safe locking mechanism.
"""

from __future__ import annotations

import copy
import os
from pathlib import Path
from threading import Lock
from typing import Iterable, Tuple

from lxml import etree


# Class-level caches shared across all TemplateResolver instances
_GLOBAL_CACHE: dict[tuple[str, Path, str, str | None], str] = {}
_TREE_CACHE: dict[tuple[str, Path, str, str | None], etree._Element] = {}
_CACHE_LOCK = Lock()


class TemplateResolver:
    """Resolve format-specific template files with override support.

    Templates are resolved in priority order:
    1. Custom root (passed to constructor)
    2. Environment variable TOKENMOULDS_TEMPLATE_ROOT
    3. xml-structures spec folders (SSOT)
    4. Default emitter templates

    Caching is done at the class level to avoid repeated file I/O
    when multiple emitters are instantiated.
    """

    _PACKAGE_DIR = Path(__file__).resolve().parent
    _DEFAULT_TEMPLATE_DIR = _PACKAGE_DIR / "templates"

    # Spec folders are only available in a development checkout, not when
    # installed as a wheel.  Resolve _REPO_ROOT on a best-effort basis.
    _REPO_ROOT: Path | None = (
        _PACKAGE_DIR.parents[2]
        if (_PACKAGE_DIR.parents[2] / "xml-structures").is_dir()
        else None
    )
    _SPEC_ROOT: Path | None = (
        _REPO_ROOT / "xml-structures" / "ecma-376" if _REPO_ROOT else None
    )
    _SPEC_FOLDERS: dict[str, Path | None] = (
        {
            "word": _SPEC_ROOT / "wordprocessingml",
            "powerpoint": _SPEC_ROOT / "presentationml",
            "excel": _SPEC_ROOT / "spreadsheetml",
            "thmx": _SPEC_ROOT / "thmx",
            "supertheme": _SPEC_ROOT / "supertheme",
            "writer": _REPO_ROOT / "xml-structures" / "oasis-odf" / "writer",
            "impress": _REPO_ROOT / "xml-structures" / "oasis-odf" / "impress",
            "calc": _REPO_ROOT / "xml-structures" / "oasis-odf" / "calc",
            "draw": _REPO_ROOT / "xml-structures" / "oasis-odf" / "draw",
            "google-docs": _REPO_ROOT
            / "xml-structures"
            / "google-workspace"
            / "wordprocessingml",
            # Google Workspace format variants use the same OOXML spec folders
            "google-slides": _SPEC_ROOT / "presentationml",
            "google-sheets": _SPEC_ROOT / "spreadsheetml",
        }
        if _REPO_ROOT and _SPEC_ROOT
        else {}
    )

    def __init__(
        self, format_key: str, template_root: str | Path | None = None
    ) -> None:
        self.format_key = format_key
        self.custom_root = self._normalize_path(template_root)
        self.env_root = self._normalize_path(os.getenv("TOKENMOULDS_TEMPLATE_ROOT"))
        self.spec_root = self._SPEC_FOLDERS.get(format_key)
        self.default_root = self._DEFAULT_TEMPLATE_DIR / format_key

    def _normalize_path(self, path: str | Path | None) -> Path | None:
        if not path:
            return None
        return Path(path).expanduser().resolve()

    def _candidate_roots(self) -> Iterable[Tuple[Path, bool]]:
        """Yield (path, is_spec) tuples ordered by priority."""
        if self.custom_root:
            yield self.custom_root / self.format_key, False
        if self.env_root:
            yield self.env_root / self.format_key, False
        if self.spec_root:
            spec_templates = self.spec_root / "templates"
            if spec_templates.exists():
                yield spec_templates, True
            yield self.spec_root, True
        yield self.default_root, False

    def read_text(
        self,
        rel_path: str,
        *,
        spec_rel_path: str | None = None,
        allow_spec: bool = True,
    ) -> str:
        """Read template text honoring override roots and spec fallbacks.

        Uses class-level caching for performance.

        Args:
            rel_path: Relative path to the template file
            spec_rel_path: Alternative path to use when loading from spec folders
            allow_spec: Whether to allow loading from spec folders

        Returns:
            Template content as string

        Raises:
            FileNotFoundError: If template not found in any candidate location
        """
        # Build cache key including spec_root to differentiate between formats
        spec_root_key = self.spec_root or Path("")
        cache_key = (self.format_key, spec_root_key, rel_path, spec_rel_path)

        # Check class-level cache (thread-safe)
        with _CACHE_LOCK:
            if cache_key in _GLOBAL_CACHE:
                return _GLOBAL_CACHE[cache_key]

        # Load from file system
        for base, is_spec in self._candidate_roots():
            if is_spec and not allow_spec:
                continue
            if not base.exists():
                continue
            candidate_rel = spec_rel_path if (is_spec and spec_rel_path) else rel_path
            candidate = base / candidate_rel
            if candidate.exists():
                content = candidate.read_text(encoding="utf-8")
                # Store in class-level cache (thread-safe)
                with _CACHE_LOCK:
                    _GLOBAL_CACHE[cache_key] = content
                return content

        raise FileNotFoundError(
            f"Template '{rel_path}' not found for format '{self.format_key}'."
        )

    def read_tree(
        self,
        rel_path: str,
        *,
        spec_rel_path: str | None = None,
        allow_spec: bool = True,
    ) -> etree._Element:
        """Read and parse template XML, returning a deep-copied tree.

        Uses class-level caching for performance. The original parsed tree
        is cached, and each call returns a deep copy to ensure thread-safe
        mutation by emitters.

        This method is optimized for the pattern:
        1. Parse template once (expensive: ~5-10ms)
        2. Cache the parsed tree
        3. Return deep copies (cheap: ~0.1-1ms)
        4. Emitters mutate their copies safely

        Args:
            rel_path: Relative path to the template file
            spec_rel_path: Alternative path to use when loading from spec folders
            allow_spec: Whether to allow loading from spec folders

        Returns:
            Deep-copied lxml._Element ready for mutation. Each caller gets
            an independent copy that can be modified without affecting the cache
            or other concurrent callers.

        Raises:
            FileNotFoundError: If template not found in any candidate location
            etree.XMLSyntaxError: If XML parsing fails

        Example:
            >>> resolver = TemplateResolver("word")
            >>> tree = resolver.read_tree("styles.xml")
            >>> # Modify tree safely - changes won't affect cache
            >>> tree.set("customAttr", "value")
        """
        # Build cache key (same as read_text)
        spec_root_key = self.spec_root or Path("")
        cache_key = (self.format_key, spec_root_key, rel_path, spec_rel_path)

        # Check tree cache (thread-safe)
        with _CACHE_LOCK:
            if cache_key in _TREE_CACHE:
                # Return deep copy to allow safe mutation
                return copy.deepcopy(_TREE_CACHE[cache_key])

        # Load string content (uses _GLOBAL_CACHE)
        content = self.read_text(
            rel_path, spec_rel_path=spec_rel_path, allow_spec=allow_spec
        )

        # Parse XML
        try:
            tree = etree.fromstring(content.encode("utf-8"))
        except etree.XMLSyntaxError as e:
            raise ValueError(
                f"Failed to parse template '{rel_path}' for format '{self.format_key}': {e}"
            ) from e

        # Store in tree cache (thread-safe)
        with _CACHE_LOCK:
            _TREE_CACHE[cache_key] = tree

        # Return deep copy for safe mutation
        return copy.deepcopy(tree)

    @classmethod
    def clear_cache(cls) -> None:
        """Clear the global template caches (both string and tree).

        Useful for testing or when templates may have changed on disk.
        Clears both _GLOBAL_CACHE and _TREE_CACHE atomically.
        """
        with _CACHE_LOCK:
            _GLOBAL_CACHE.clear()
            _TREE_CACHE.clear()

    @classmethod
    def preload_all(cls) -> int:
        """Eagerly parse and cache all templates for all known formats.

        Call this at application startup to front-load parsing costs.
        Templates are parsed once and cached; subsequent calls to read_tree()
        return deep copies from cache.

        Returns:
            Number of templates loaded
        """
        count = 0
        for format_key in cls._SPEC_FOLDERS.keys():
            resolver = cls(format_key)
            spec_root = resolver.spec_root
            if spec_root is None:
                continue

            # Find all XML templates in the spec folder
            templates_dir = spec_root / "templates"
            if templates_dir.exists():
                for xml_file in templates_dir.rglob("*.xml"):
                    rel_path = xml_file.relative_to(templates_dir)
                    try:
                        resolver.read_tree(
                            str(rel_path), spec_rel_path=f"templates/{rel_path}"
                        )
                        count += 1
                    except (FileNotFoundError, ValueError):
                        # Skip files that can't be parsed (may have placeholders)
                        pass
        return count
