"""Template cache manager for TokenMoulds MCP server.

Provides persistent caching of generated templates keyed by brand hash.
Templates are stored on disk for cross-session reuse.

Cache structure:
    ~/.tokenmoulds/cache/
        templates/
            {brand_id}/
                metadata.json
                word.dotx
                powerpoint.potx
                excel.xltx
                writer.ott
                impress.otp
                theme.thmx
                google_docs.dotx
        tokens/
            {brand_id}.tokens.json
"""

from __future__ import annotations

import hashlib
import json
import shutil
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from enum import Enum
from pathlib import Path
from typing import Any, Dict, List, Optional


class TemplateFormat(str, Enum):
    """Supported template formats."""

    WORD = "word"
    POWERPOINT = "powerpoint"
    EXCEL = "excel"
    WRITER = "writer"
    IMPRESS = "impress"
    THEME = "theme"
    GOOGLE_DOCS = "google_docs"

    @property
    def extension(self) -> str:
        """Get file extension for format."""
        extensions = {
            TemplateFormat.WORD: ".dotx",
            TemplateFormat.POWERPOINT: ".potx",
            TemplateFormat.EXCEL: ".xltx",
            TemplateFormat.WRITER: ".ott",
            TemplateFormat.IMPRESS: ".otp",
            TemplateFormat.THEME: ".thmx",
            TemplateFormat.GOOGLE_DOCS: ".dotx",  # Google Docs uses Word-compatible format
        }
        return extensions[self]


@dataclass
class CacheMetadata:
    """Metadata for a cached brand."""

    brand_id: str
    token_hash: str
    created_at: str
    updated_at: str
    templates: Dict[str, "TemplateMetadata"] = field(default_factory=dict)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return {
            "brand_id": self.brand_id,
            "token_hash": self.token_hash,
            "created_at": self.created_at,
            "updated_at": self.updated_at,
            "templates": {k: v.to_dict() for k, v in self.templates.items()},
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "CacheMetadata":
        """Create from dictionary."""
        templates = {
            k: TemplateMetadata.from_dict(v)
            for k, v in data.get("templates", {}).items()
        }
        return cls(
            brand_id=data["brand_id"],
            token_hash=data["token_hash"],
            created_at=data["created_at"],
            updated_at=data["updated_at"],
            templates=templates,
        )


@dataclass
class TemplateMetadata:
    """Metadata for a cached template."""

    format: str
    size_bytes: int
    created_at: str
    fonts_embedded: List[str] = field(default_factory=list)
    styles_count: int = 0
    validation_status: str = "unknown"
    iso_compliant: bool = False
    wcag_level: str = "AA"

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for JSON serialization."""
        return asdict(self)

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> "TemplateMetadata":
        """Create from dictionary."""
        return cls(**data)


class TemplateCacheManager:
    """Manages template caching for the MCP server.

    Templates are cached per brand, with each brand having its own directory
    containing templates for all formats. Cache invalidation occurs when
    the token hash changes.
    """

    def __init__(self, cache_root: Optional[Path] = None) -> None:
        """Initialize the cache manager.

        Args:
            cache_root: Root directory for cache. Defaults to ~/.tokenmoulds/cache/
        """
        if cache_root is None:
            cache_root = Path.home() / ".tokenmoulds" / "cache"

        self.cache_root = Path(cache_root)
        self.templates_dir = self.cache_root / "templates"
        self.tokens_dir = self.cache_root / "tokens"

        # Create directories
        self.templates_dir.mkdir(parents=True, exist_ok=True)
        self.tokens_dir.mkdir(parents=True, exist_ok=True)

    def compute_token_hash(
        self,
        tokens: Dict[str, Any],
        *,
        repository: Optional[str] = None,
        ref: Optional[str] = None,
    ) -> str:
        """Compute a stable hash of token content.

        When *repository* and *ref* are provided (GitHub brand source), they
        are mixed into the hash so that the same tokens at different commits
        produce distinct cache keys.

        Args:
            tokens: Token dictionary (DTCG or legacy format)
            repository: Optional GitHub ``owner/repo`` string.
            ref: Optional Git ref (branch, tag, or commit SHA).

        Returns:
            16-character hex hash
        """
        h = hashlib.sha256()
        if repository:
            h.update(f"{repository}@{ref or 'main'}:".encode())
        h.update(json.dumps(tokens, sort_keys=True, default=str).encode())
        return h.hexdigest()[:16]

    def get_brand_dir(self, brand_id: str) -> Path:
        """Get the cache directory for a brand.

        Args:
            brand_id: Brand identifier

        Returns:
            Path to brand cache directory
        """
        # Sanitize brand_id for filesystem
        safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in brand_id)
        return self.templates_dir / safe_id

    def get_metadata_path(self, brand_id: str) -> Path:
        """Get path to brand metadata file."""
        return self.get_brand_dir(brand_id) / "metadata.json"

    def load_metadata(self, brand_id: str) -> Optional[CacheMetadata]:
        """Load metadata for a brand.

        Args:
            brand_id: Brand identifier

        Returns:
            CacheMetadata if exists, None otherwise
        """
        metadata_path = self.get_metadata_path(brand_id)
        if not metadata_path.exists():
            return None

        try:
            with open(metadata_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            return CacheMetadata.from_dict(data)
        except (json.JSONDecodeError, KeyError):
            return None

    def save_metadata(self, metadata: CacheMetadata) -> None:
        """Save metadata for a brand.

        Args:
            metadata: CacheMetadata to save
        """
        brand_dir = self.get_brand_dir(metadata.brand_id)
        brand_dir.mkdir(parents=True, exist_ok=True)

        metadata_path = self.get_metadata_path(metadata.brand_id)
        with open(metadata_path, "w", encoding="utf-8") as f:
            json.dump(metadata.to_dict(), f, indent=2)

    def get_template(
        self,
        brand_id: str,
        format: TemplateFormat,
        token_hash: Optional[str] = None,
    ) -> Optional[bytes]:
        """Get a cached template.

        Args:
            brand_id: Brand identifier
            format: Template format
            token_hash: Expected token hash (for validation)

        Returns:
            Template bytes if cached and valid, None otherwise
        """
        metadata = self.load_metadata(brand_id)
        if metadata is None:
            return None

        # Check token hash if provided
        if token_hash is not None and metadata.token_hash != token_hash:
            # Cache is stale, token content has changed
            return None

        # Check if format is cached
        if format.value not in metadata.templates:
            return None

        # Read template file
        template_path = (
            self.get_brand_dir(brand_id) / f"{format.value}{format.extension}"
        )
        if not template_path.exists():
            return None

        return template_path.read_bytes()

    def set_template(
        self,
        brand_id: str,
        format: TemplateFormat,
        data: bytes,
        token_hash: str,
        fonts_embedded: Optional[List[str]] = None,
        styles_count: int = 0,
        validation_status: str = "unknown",
        iso_compliant: bool = False,
        wcag_level: str = "AA",
    ) -> Path:
        """Cache a generated template.

        Args:
            brand_id: Brand identifier
            format: Template format
            data: Template bytes
            token_hash: Hash of source tokens
            fonts_embedded: List of embedded font names
            styles_count: Number of styles in template
            validation_status: Validation result (passed, failed, unknown)
            iso_compliant: Whether template is ISO 29500 compliant
            wcag_level: WCAG accessibility level (AA, AAA)

        Returns:
            Path to cached template
        """
        brand_dir = self.get_brand_dir(brand_id)
        brand_dir.mkdir(parents=True, exist_ok=True)

        # Save template file
        template_path = brand_dir / f"{format.value}{format.extension}"
        template_path.write_bytes(data)

        # Update metadata
        metadata = self.load_metadata(brand_id)
        now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

        if metadata is None:
            metadata = CacheMetadata(
                brand_id=brand_id,
                token_hash=token_hash,
                created_at=now,
                updated_at=now,
                templates={},
            )
        else:
            metadata.token_hash = token_hash
            metadata.updated_at = now

        # Add template metadata
        metadata.templates[format.value] = TemplateMetadata(
            format=format.value,
            size_bytes=len(data),
            created_at=now,
            fonts_embedded=fonts_embedded or [],
            styles_count=styles_count,
            validation_status=validation_status,
            iso_compliant=iso_compliant,
            wcag_level=wcag_level,
        )

        self.save_metadata(metadata)
        return template_path

    def save_tokens(self, brand_id: str, tokens: Dict[str, Any]) -> Path:
        """Save tokens to cache.

        Args:
            brand_id: Brand identifier
            tokens: Token dictionary

        Returns:
            Path to saved tokens file
        """
        # Sanitize brand_id
        safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in brand_id)
        tokens_path = self.tokens_dir / f"{safe_id}.tokens.json"

        with open(tokens_path, "w", encoding="utf-8") as f:
            json.dump(tokens, f, indent=2)

        return tokens_path

    def load_tokens(self, brand_id: str) -> Optional[Dict[str, Any]]:
        """Load tokens from cache.

        Args:
            brand_id: Brand identifier

        Returns:
            Token dictionary if exists, None otherwise
        """
        safe_id = "".join(c if c.isalnum() or c in "-_" else "_" for c in brand_id)
        tokens_path = self.tokens_dir / f"{safe_id}.tokens.json"

        if not tokens_path.exists():
            return None

        with open(tokens_path, "r", encoding="utf-8") as f:
            return json.load(f)

    def invalidate(self, brand_id: str) -> bool:
        """Invalidate all cached templates for a brand.

        Args:
            brand_id: Brand identifier

        Returns:
            True if cache was cleared, False if no cache existed
        """
        brand_dir = self.get_brand_dir(brand_id)
        if not brand_dir.exists():
            return False

        shutil.rmtree(brand_dir)
        return True

    def clear(self) -> int:
        """Clear all cached templates.

        Returns:
            Number of brands cleared
        """
        count = 0
        if self.templates_dir.exists():
            for brand_dir in self.templates_dir.iterdir():
                if brand_dir.is_dir():
                    shutil.rmtree(brand_dir)
                    count += 1
        return count

    def list_brands(self) -> List[str]:
        """List all cached brands.

        Returns:
            List of brand identifiers
        """
        brands = []
        if self.templates_dir.exists():
            for brand_dir in self.templates_dir.iterdir():
                if brand_dir.is_dir():
                    metadata = self.load_metadata(brand_dir.name)
                    if metadata:
                        brands.append(metadata.brand_id)
                    else:
                        brands.append(brand_dir.name)
        return brands

    def get_cache_info(self) -> Dict[str, Any]:
        """Get cache statistics.

        Returns:
            Dictionary with cache statistics
        """
        brands = self.list_brands()
        total_size = 0
        template_count = 0

        for brand_id in brands:
            metadata = self.load_metadata(brand_id)
            if metadata:
                for tmpl in metadata.templates.values():
                    total_size += tmpl.size_bytes
                    template_count += 1

        return {
            "cache_root": str(self.cache_root),
            "brands_count": len(brands),
            "templates_count": template_count,
            "total_size_bytes": total_size,
            "total_size_mb": round(total_size / (1024 * 1024), 2),
        }

    def get_brand_info(self, brand_id: str) -> Optional[Dict[str, Any]]:
        """Get detailed info for a cached brand.

        Args:
            brand_id: Brand identifier

        Returns:
            Dictionary with brand cache info, or None if not cached
        """
        metadata = self.load_metadata(brand_id)
        if metadata is None:
            return None

        templates_info = {}
        for format_name, tmpl in metadata.templates.items():
            templates_info[format_name] = {
                "size_bytes": tmpl.size_bytes,
                "size_kb": round(tmpl.size_bytes / 1024, 1),
                "created_at": tmpl.created_at,
                "fonts_embedded": tmpl.fonts_embedded,
                "styles_count": tmpl.styles_count,
                "validation_status": tmpl.validation_status,
                "iso_compliant": tmpl.iso_compliant,
                "wcag_level": tmpl.wcag_level,
            }

        return {
            "brand_id": metadata.brand_id,
            "token_hash": metadata.token_hash,
            "created_at": metadata.created_at,
            "updated_at": metadata.updated_at,
            "templates": templates_info,
            "cache_path": str(self.get_brand_dir(brand_id)),
        }
