"""Remote gallery manager for template discovery and fetching.

The gallery provides a centralized index of available templates
that can be fetched on-demand.
"""

from __future__ import annotations

import json
import logging
import time
from datetime import datetime
from pathlib import Path
from typing import Any

from gaol.config import get_data_dir
from gaol.templates.exceptions import (
    GalleryFetchError,
    GalleryIndexError,
    GalleryOfflineError,
    TemplateNotFoundError,
    TemplateParseError,
)
from gaol.templates.fetcher import TemplateFetcher
from gaol.templates.models import (
    CategoryInfo,
    GalleryIndex,
    Template,
    TemplateEntry,
)

logger = logging.getLogger(__name__)


# Default gallery URL (can be overridden in config)
DEFAULT_GALLERY_URL = (
    "https://raw.githubusercontent.com/erewhon/gaol-templates/main/index.json"
)

# Cache TTL in seconds
DEFAULT_INDEX_CACHE_TTL = 3600  # 1 hour


class GalleryManager:
    """Manage remote template gallery operations.

    Handles fetching the gallery index, caching, and downloading
    individual templates.
    """

    def __init__(
        self,
        gallery_url: str | None = None,
        cache_dir: Path | None = None,
        index_cache_ttl: int = DEFAULT_INDEX_CACHE_TTL,
    ) -> None:
        """Initialize the gallery manager.

        Args:
            gallery_url: URL to the gallery index JSON
            cache_dir: Directory for caching
            index_cache_ttl: How long to cache the index (seconds)
        """
        self._gallery_url = gallery_url or DEFAULT_GALLERY_URL
        if cache_dir is None:
            cache_dir = get_data_dir() / "templates"
        self._cache_dir = cache_dir
        self._cache_dir.mkdir(parents=True, exist_ok=True)
        self._index_cache_ttl = index_cache_ttl
        self._fetcher = TemplateFetcher(cache_dir / "cache")
        self._index: GalleryIndex | None = None
        self._index_fetched_at: float = 0

    @property
    def index_cache_path(self) -> Path:
        """Path to the cached gallery index."""
        return self._cache_dir / "index.json"

    @property
    def index_meta_path(self) -> Path:
        """Path to the index cache metadata."""
        return self._cache_dir / "index.meta.json"

    def get_index(self, refresh: bool = False) -> GalleryIndex:
        """Get the gallery index.

        Args:
            refresh: Force refresh even if cache is valid

        Returns:
            GalleryIndex object

        Raises:
            GalleryOfflineError: If gallery is not accessible
            GalleryIndexError: If index is invalid
        """
        # Check memory cache
        if not refresh and self._index is not None:
            if time.time() - self._index_fetched_at < self._index_cache_ttl:
                return self._index

        # Check file cache
        if not refresh and self._is_cache_valid():
            try:
                index = self._load_cached_index()
                self._index = index
                self._index_fetched_at = time.time()
                return index
            except Exception as e:
                logger.debug(f"Failed to load cached index: {e}")

        # Fetch from remote
        try:
            index = self._fetch_index()
            self._index = index
            self._index_fetched_at = time.time()
            self._save_cached_index(index)
            return index
        except GalleryFetchError as e:
            # Try to use stale cache if available
            if self.index_cache_path.exists():
                logger.warning(f"Using stale cache: {e}")
                return self._load_cached_index()
            raise GalleryOfflineError(self._gallery_url, str(e)) from e

    def _is_cache_valid(self) -> bool:
        """Check if the cached index is still valid."""
        if not self.index_cache_path.exists():
            return False
        if not self.index_meta_path.exists():
            return False

        try:
            meta = json.loads(self.index_meta_path.read_text())
            cached_time = meta.get("fetched_at", 0)
            return time.time() - cached_time < self._index_cache_ttl
        except Exception:
            return False

    def _load_cached_index(self) -> GalleryIndex:
        """Load the index from cache."""
        try:
            data = json.loads(self.index_cache_path.read_text())
            return self._parse_index(data)
        except Exception as e:
            raise GalleryIndexError(f"Failed to load cached index: {e}", e)

    def _save_cached_index(self, index: GalleryIndex) -> None:
        """Save the index to cache."""
        try:
            # Save index
            self.index_cache_path.write_text(
                json.dumps(index.model_dump(mode="json"), indent=2)
            )
            # Save metadata
            self.index_meta_path.write_text(
                json.dumps({"fetched_at": time.time(), "url": self._gallery_url})
            )
        except Exception as e:
            logger.warning(f"Failed to cache index: {e}")

    def _fetch_index(self) -> GalleryIndex:
        """Fetch the index from the remote gallery."""
        try:
            data = self._fetcher.fetch_json(self._gallery_url)
            return self._parse_index(data)
        except GalleryFetchError:
            raise
        except Exception as e:
            raise GalleryIndexError(f"Failed to parse index: {e}", e)

    def _parse_index(self, data: dict[str, Any]) -> GalleryIndex:
        """Parse index data into GalleryIndex.

        Args:
            data: Raw index data

        Returns:
            GalleryIndex object

        Raises:
            GalleryIndexError: If data is invalid
        """
        try:
            # Parse templates
            templates = {}
            for template_id, entry_data in data.get("templates", {}).items():
                try:
                    # Handle datetime parsing
                    if "updated_at" in entry_data and isinstance(entry_data["updated_at"], str):
                        entry_data["updated_at"] = datetime.fromisoformat(
                            entry_data["updated_at"].replace("Z", "+00:00")
                        )
                    templates[template_id] = TemplateEntry(**entry_data)
                except Exception as e:
                    logger.warning(f"Failed to parse template entry {template_id}: {e}")

            # Parse categories
            categories = {}
            for cat_name, cat_data in data.get("categories", {}).items():
                try:
                    categories[cat_name] = CategoryInfo(**cat_data)
                except Exception as e:
                    logger.warning(f"Failed to parse category {cat_name}: {e}")

            # Parse updated_at
            updated_at = datetime.now()
            if "updated_at" in data:
                try:
                    if isinstance(data["updated_at"], str):
                        updated_at = datetime.fromisoformat(
                            data["updated_at"].replace("Z", "+00:00")
                        )
                    else:
                        updated_at = data["updated_at"]
                except Exception:
                    pass

            return GalleryIndex(
                version=data.get("version", "1"),
                updated_at=updated_at,
                templates=templates,
                categories=categories,
            )
        except Exception as e:
            raise GalleryIndexError(f"Invalid index format: {e}", e)

    def fetch_template(self, template_id: str) -> Template:
        """Fetch a template from the remote gallery.

        Args:
            template_id: Template ID to fetch

        Returns:
            Template object

        Raises:
            TemplateNotFoundError: If template not in gallery
            TemplateParseError: If template data is invalid
            GalleryFetchError: If fetch fails
        """
        # Get index to find template URL
        index = self.get_index()

        entry = index.templates.get(template_id)
        if not entry:
            raise TemplateNotFoundError(template_id, ["gallery"])

        # Fetch the template
        try:
            data = self._fetcher.fetch_yaml(entry.template_url)
            return Template.from_dict(data)
        except GalleryFetchError:
            raise
        except Exception as e:
            raise TemplateParseError(template_id, str(e), e)

    def search(
        self,
        query: str,
        category: str | None = None,
        tags: list[str] | None = None,
    ) -> list[TemplateEntry]:
        """Search templates in the gallery.

        Args:
            query: Search query
            category: Filter by category
            tags: Filter by tags

        Returns:
            List of matching template entries
        """
        index = self.get_index()
        return index.search(query, category, tags)

    def list_templates(self, category: str | None = None) -> list[TemplateEntry]:
        """List all templates in the gallery.

        Args:
            category: Filter by category

        Returns:
            List of template entries
        """
        index = self.get_index()
        if category:
            return index.get_by_category(category)
        return list(index.templates.values())

    def list_categories(self) -> list[CategoryInfo]:
        """List all categories in the gallery.

        Returns:
            List of category info
        """
        index = self.get_index()
        return index.list_categories()

    def refresh_index(self) -> GalleryIndex:
        """Force refresh the gallery index.

        Returns:
            Refreshed GalleryIndex
        """
        return self.get_index(refresh=True)

    def clear_cache(self) -> None:
        """Clear all cached data."""
        self._index = None
        self._index_fetched_at = 0
        self._fetcher.clear_cache()

        for path in [self.index_cache_path, self.index_meta_path]:
            try:
                path.unlink()
            except FileNotFoundError:
                pass
