"""
Context pre-computation service for Claude Code sessions.

Provides caching and warm-up capabilities for session context to ensure
fast context injection in Claude Code. Uses Redis for distributed caching.
"""

from __future__ import annotations

import hashlib
import json
import logging
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from lightwave.context.session import build_session_context

logger = logging.getLogger(__name__)


# =============================================================================
# CACHE BACKEND
# =============================================================================


class ContextCache:
    """
    Context cache abstraction supporting Redis or in-memory caching.
    """

    _instance: "ContextCache | None" = None
    _memory_cache: dict[str, tuple[Any, float]] = {}

    def __new__(cls) -> "ContextCache":
        """Singleton pattern for cache instance."""
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self):
        """Initialize cache backend."""
        self._redis = None
        self._init_redis()

    def _init_redis(self) -> None:
        """Try to initialize Redis connection."""
        try:
            from django.core.cache import cache

            # Use Django cache if available
            self._django_cache = cache
            logger.debug("Using Django cache backend")
        except ImportError:
            self._django_cache = None

        if self._django_cache is None:
            try:
                import os

                import redis

                redis_url = os.environ.get("REDIS_URL", "redis://localhost:6379/0")
                self._redis = redis.from_url(redis_url)
                self._redis.ping()
                logger.debug("Connected to Redis for context caching")
            except Exception as e:
                logger.debug(f"Redis not available, using in-memory cache: {e}")
                self._redis = None

    def get(self, key: str) -> Any | None:
        """Get value from cache."""
        prefixed_key = f"ctx:{key}"

        # Try Django cache first
        if self._django_cache:
            return self._django_cache.get(prefixed_key)

        # Try Redis
        if self._redis:
            try:
                data = self._redis.get(prefixed_key)
                if data:
                    return json.loads(data)
            except Exception as e:
                logger.warning(f"Redis get failed: {e}")

        # Fallback to memory cache
        if prefixed_key in self._memory_cache:
            value, expiry = self._memory_cache[prefixed_key]
            if expiry > datetime.now(timezone.utc).timestamp():
                return value
            else:
                del self._memory_cache[prefixed_key]

        return None

    def set(self, key: str, value: Any, ttl: int = 900) -> None:
        """Set value in cache with TTL (default 15 minutes)."""
        prefixed_key = f"ctx:{key}"

        # Try Django cache first
        if self._django_cache:
            self._django_cache.set(prefixed_key, value, ttl)
            return

        # Try Redis
        if self._redis:
            try:
                self._redis.setex(prefixed_key, ttl, json.dumps(value, default=str))
                return
            except Exception as e:
                logger.warning(f"Redis set failed: {e}")

        # Fallback to memory cache
        expiry = datetime.now(timezone.utc).timestamp() + ttl
        self._memory_cache[prefixed_key] = (value, expiry)

    def delete(self, key: str) -> None:
        """Delete value from cache."""
        prefixed_key = f"ctx:{key}"

        if self._django_cache:
            self._django_cache.delete(prefixed_key)

        if self._redis:
            try:
                self._redis.delete(prefixed_key)
            except Exception:
                pass

        if prefixed_key in self._memory_cache:
            del self._memory_cache[prefixed_key]

    def delete_pattern(self, pattern: str) -> int:
        """Delete all keys matching pattern."""
        prefixed_pattern = f"ctx:{pattern}"
        deleted = 0

        if self._redis:
            try:
                keys = self._redis.keys(prefixed_pattern)
                if keys:
                    deleted = self._redis.delete(*keys)
            except Exception:
                pass

        # Memory cache pattern matching
        to_delete = [k for k in self._memory_cache if k.startswith(prefixed_pattern.replace("*", ""))]
        for k in to_delete:
            del self._memory_cache[k]
            deleted += 1

        return deleted


# =============================================================================
# CONTEXT PRECOMPUTER
# =============================================================================


class ContextPrecomputer:
    """
    Pre-computes and caches session context for Claude Code.

    Features:
    - Caches full session context
    - Invalidates on model changes
    - Supports forced refresh
    - Tracks cache hit/miss metrics
    """

    def __init__(
        self,
        user_id: str | None = None,
        workspace_root: Path | None = None,
        ttl: int = 900,
    ):
        """
        Initialize the precomputer.

        Args:
            user_id: User ID for context isolation
            workspace_root: Workspace root directory
            ttl: Cache TTL in seconds (default 15 minutes)
        """
        self.user_id = user_id or "default"
        self.workspace_root = workspace_root or Path.cwd()
        self.ttl = ttl
        self.cache = ContextCache()

    def _get_cache_key(self) -> str:
        """Generate cache key for this context."""
        key_parts = [
            self.user_id,
            str(self.workspace_root),
        ]
        key_hash = hashlib.sha256(":".join(key_parts).encode()).hexdigest()[:16]
        return f"session:{self.user_id}:{key_hash}"

    def compute(self, force_refresh: bool = False) -> dict[str, Any]:
        """
        Compute session context, using cache if available.

        Args:
            force_refresh: If True, bypass cache

        Returns:
            Session context as dict
        """
        cache_key = self._get_cache_key()

        # Check cache first
        if not force_refresh:
            cached = self.cache.get(cache_key)
            if cached:
                logger.debug(f"Context cache hit: {cache_key}")
                return cached

        logger.debug(f"Context cache miss: {cache_key}")

        # Build fresh context
        try:
            context = build_session_context(self.workspace_root)
            context_dict = context.model_dump(mode="json")

            # Add metadata
            context_dict["_cache_metadata"] = {
                "computed_at": datetime.now(timezone.utc).isoformat(),
                "user_id": self.user_id,
                "workspace_root": str(self.workspace_root),
                "ttl": self.ttl,
            }

            # Cache the result
            self.cache.set(cache_key, context_dict, self.ttl)

            return context_dict

        except Exception as e:
            logger.error(f"Failed to build session context: {e}")
            # Return minimal context on error
            return {
                "error": str(e),
                "_cache_metadata": {
                    "computed_at": datetime.now(timezone.utc).isoformat(),
                    "user_id": self.user_id,
                    "error": True,
                },
            }

    def invalidate(self) -> None:
        """Invalidate cached context."""
        cache_key = self._get_cache_key()
        self.cache.delete(cache_key)
        logger.debug(f"Context invalidated: {cache_key}")


# =============================================================================
# CONVENIENCE FUNCTIONS
# =============================================================================


def get_session_context(
    user_id: str | None = None,
    workspace_root: Path | None = None,
    force_refresh: bool = False,
    ttl: int = 900,
) -> dict[str, Any]:
    """
    Get session context with caching.

    Args:
        user_id: User ID for context isolation
        workspace_root: Workspace root directory
        force_refresh: If True, bypass cache
        ttl: Cache TTL in seconds

    Returns:
        Session context dict
    """
    precomputer = ContextPrecomputer(
        user_id=user_id,
        workspace_root=workspace_root,
        ttl=ttl,
    )
    return precomputer.compute(force_refresh=force_refresh)


def invalidate_context(
    user_id: str | None = None,
    workspace_root: Path | None = None,
) -> None:
    """
    Invalidate cached context.

    Args:
        user_id: User ID
        workspace_root: Workspace root directory
    """
    precomputer = ContextPrecomputer(
        user_id=user_id,
        workspace_root=workspace_root,
    )
    precomputer.invalidate()


def invalidate_context_for_model(model: str, pk: str) -> int:
    """
    Invalidate context when a model changes.

    Args:
        model: Model name (task, sprint, etc.)
        pk: Primary key of changed model

    Returns:
        Number of cache entries invalidated
    """
    cache = ContextCache()

    # Invalidate all user contexts (conservative approach)
    # In production, would track which users have context for this model
    deleted = cache.delete_pattern("session:*")

    logger.info(f"Invalidated {deleted} context entries for {model}:{pk}")
    return deleted


def warm_context_cache(
    user_id: str | None = None,
    workspace_root: Path | None = None,
    ttl: int = 900,
) -> dict[str, Any]:
    """
    Warm the context cache by pre-computing context.

    Args:
        user_id: User ID
        workspace_root: Workspace root directory
        ttl: Cache TTL in seconds

    Returns:
        Computed context
    """
    precomputer = ContextPrecomputer(
        user_id=user_id,
        workspace_root=workspace_root,
        ttl=ttl,
    )
    return precomputer.compute(force_refresh=True)
