"""Project context caching and management."""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
from typing import Any

from pydantic import BaseModel, Field

from .settings import Settings, get_settings

# Schema version for context format migrations
CURRENT_SCHEMA_VERSION = 2


def _get_cache_dir() -> Path:
    """Get the context cache directory."""
    cache_dir = Path.home() / ".gjalla" / "context"
    cache_dir.mkdir(parents=True, exist_ok=True)
    return cache_dir


def _get_cache_path(project_id: str | int) -> Path:
    """Get cache file path for a project."""
    return _get_cache_dir() / f"{project_id}.json"


# -----------------------------------------------------------------------------
# ProjectContext Pydantic Model (user-requested API)
# -----------------------------------------------------------------------------


class ProjectContext(BaseModel):
    """Project context with caching support."""

    schema_version: str = "1.0"
    project_id: int
    context_hash: str
    capabilities: list[dict[str, Any]] = Field(default_factory=list)
    rules: list[dict[str, Any]] = Field(default_factory=list)
    architecture: dict[str, Any] = Field(default_factory=dict)
    updated_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())

    @classmethod
    def load(cls, project_id: int) -> ProjectContext | None:
        """Load context from cache.

        Args:
            project_id: The project ID to load context for.

        Returns:
            ProjectContext if found and valid, None otherwise.
        """
        cache_path = _get_cache_path(project_id)
        if not cache_path.exists():
            return None

        try:
            data = json.loads(cache_path.read_text(encoding="utf-8"))
            return cls(**data)
        except (json.JSONDecodeError, ValueError):
            return None

    def save(self) -> None:
        """Save context to cache."""
        cache_path = _get_cache_path(self.project_id)
        cache_path.parent.mkdir(parents=True, exist_ok=True)
        cache_path.write_text(
            self.model_dump_json(indent=2),
            encoding="utf-8",
        )

    def is_stale(self, max_age_hours: int = 24) -> bool:
        """Check if context is older than max_age_hours.

        Args:
            max_age_hours: Maximum age in hours before context is stale.

        Returns:
            True if context is stale, False otherwise.
        """
        try:
            updated = datetime.fromisoformat(self.updated_at.replace("Z", "+00:00"))
            now = datetime.now(timezone.utc)
            age_hours = (now - updated).total_seconds() / 3600
            return age_hours > max_age_hours
        except (ValueError, TypeError):
            return True


def get_context_hash(context: ProjectContext) -> str:
    """Generate hash from context content.

    Args:
        context: The ProjectContext to hash.

    Returns:
        SHA256 hash string of the context content.
    """
    # Hash the meaningful content, excluding timestamps
    content = {
        "capabilities": context.capabilities,
        "rules": context.rules,
        "architecture": context.architecture,
    }
    content_str = json.dumps(content, sort_keys=True)
    return hashlib.sha256(content_str.encode()).hexdigest()


def clear_context_cache(project_id: int | None = None) -> None:
    """Clear context cache.

    Args:
        project_id: If provided, clear only that project's cache.
                   If None, clear all cached contexts.
    """
    cache_dir = _get_cache_dir()

    if project_id is not None:
        cache_path = cache_dir / f"{project_id}.json"
        if cache_path.exists():
            cache_path.unlink()
    else:
        # Clear all cached contexts
        for cache_file in cache_dir.glob("*.json"):
            cache_file.unlink()


# -----------------------------------------------------------------------------
# Function-based API (for test compatibility)
# -----------------------------------------------------------------------------


def save_context(project_id: str, context: dict[str, Any]) -> Path:
    """Save context to cache file.

    Args:
        project_id: The project identifier.
        context: Context data dictionary.

    Returns:
        Path to the saved cache file.
    """
    cache_path = _get_cache_path(project_id)
    cache_path.parent.mkdir(parents=True, exist_ok=True)
    cache_path.write_text(json.dumps(context, indent=2), encoding="utf-8")
    return cache_path


def load_context(project_id: str) -> dict[str, Any] | None:
    """Load context from cache file.

    Args:
        project_id: The project identifier.

    Returns:
        Context dictionary if found, None otherwise.
    """
    cache_path = _get_cache_path(project_id)
    if not cache_path.exists():
        return None

    try:
        return json.loads(cache_path.read_text(encoding="utf-8"))
    except json.JSONDecodeError:
        return None


def compute_context_hash(context: dict[str, Any]) -> str:
    """Compute hash of context content.

    Args:
        context: Context dictionary to hash.

    Returns:
        SHA256 hash string.
    """
    # Hash meaningful content, excluding timestamps
    content = {
        k: v
        for k, v in context.items()
        if k not in ("fetched_at", "updated_at", "context_hash")
    }
    content_str = json.dumps(content, sort_keys=True)
    return hashlib.sha256(content_str.encode()).hexdigest()


def is_context_fresh(context: dict[str, Any], max_age_hours: int = 24) -> bool:
    """Check if context is fresh (not stale).

    Args:
        context: Context dictionary with fetched_at timestamp.
        max_age_hours: Maximum age in hours.

    Returns:
        True if context is fresh, False if stale.
    """
    fetched_at = context.get("fetched_at")
    if not fetched_at:
        return False

    try:
        fetched = datetime.fromisoformat(fetched_at.replace("Z", "+00:00"))
        now = datetime.now(timezone.utc)
        age_hours = (now - fetched).total_seconds() / 3600
        return age_hours <= max_age_hours
    except (ValueError, TypeError):
        return False


async def fetch_context_from_api(project_id: str) -> dict[str, Any]:
    """Fetch context from API.

    Args:
        project_id: The project identifier.

    Returns:
        Context dictionary from API.

    Raises:
        RuntimeError: If API call fails.
    """
    from gjalla_precommit.protocol.http_client import fetch_context_async
    from gjalla_precommit.config.settings import Settings

    settings = Settings.load()

    # Prefer local project config for API key and URL over global config
    try:
        from gjalla_precommit.commands._shared import resolve_local_api_key, get_repo_root
        repo_root = get_repo_root()
        local_key, local_url = resolve_local_api_key(repo_root)
        updates = {}
        if local_key:
            updates["api_key"] = local_key
        if local_url:
            updates["api_url"] = local_url
        if updates:
            settings = settings.model_copy(update=updates)
    except Exception:
        pass

    if not settings.api_key:
        raise RuntimeError("API key not configured. Run 'gjalla init' first.")

    return await fetch_context_async(project_id, settings.api_key, settings.api_url)


def refresh_context_if_stale(
    project_id: str, max_age_hours: int = 24
) -> dict[str, Any]:
    """Refresh context if stale, otherwise return cached.

    Args:
        project_id: The project identifier.
        max_age_hours: Maximum age before refresh.

    Returns:
        Fresh context dictionary.
    """
    import asyncio

    cached = load_context(project_id)

    if cached and is_context_fresh(cached, max_age_hours):
        return cached

    # Fetch fresh context from API
    fresh = asyncio.run(fetch_context_from_api(project_id))

    # Add fetch timestamp
    fresh["fetched_at"] = datetime.now(timezone.utc).isoformat()

    # Save to cache
    save_context(project_id, fresh)

    return fresh


# -----------------------------------------------------------------------------
# Schema Version Management
# -----------------------------------------------------------------------------


def get_expected_schema_version() -> int:
    """Get the current expected schema version.

    Returns:
        Current schema version number.
    """
    return CURRENT_SCHEMA_VERSION


def is_schema_current(context: dict[str, Any]) -> bool:
    """Check if context schema is current version.

    Args:
        context: Context dictionary to check.

    Returns:
        True if schema version matches current.
    """
    return context.get("schema_version") == CURRENT_SCHEMA_VERSION


def is_schema_future(context: dict[str, Any]) -> bool:
    """Check if context schema is from a future version.

    Args:
        context: Context dictionary to check.

    Returns:
        True if schema version is higher than current.
    """
    version = context.get("schema_version", 0)
    return version > CURRENT_SCHEMA_VERSION


def should_refresh_context(context: dict[str, Any]) -> bool:
    """Check if context should be refreshed due to schema mismatch.

    Args:
        context: Context dictionary to check.

    Returns:
        True if refresh needed.
    """
    version = context.get("schema_version", 0)
    return version < CURRENT_SCHEMA_VERSION


def migrate_context(context: dict[str, Any]) -> dict[str, Any]:
    """Migrate context to current schema version.

    Args:
        context: Context dictionary to migrate.

    Returns:
        Migrated context dictionary.
    """
    migrated = dict(context)
    current_version = migrated.get("schema_version", 1)

    # Apply migrations sequentially
    if current_version < 2:
        # Migration from v1 to v2: ensure all required fields exist
        migrated.setdefault("capabilities", [])
        migrated.setdefault("architecture", {})
        migrated.setdefault("rules", [])

    # Update to current version
    migrated["schema_version"] = CURRENT_SCHEMA_VERSION

    return migrated


# -----------------------------------------------------------------------------
# Execution Context (existing implementation)
# -----------------------------------------------------------------------------


@dataclass
class Context:
    """Execution context for gjalla operations."""

    root_path: Path = field(default_factory=Path.cwd)
    settings: Settings = field(default_factory=get_settings)
    verbose: bool = False
    dry_run: bool = False
    _data: dict[str, Any] = field(default_factory=dict)

    def get(self, key: str, default: Any = None) -> Any:
        """Get context value."""
        return self._data.get(key, default)

    def set(self, key: str, value: Any) -> None:
        """Set context value."""
        self._data[key] = value

    @property
    def config_path(self) -> Path:
        """Path to configuration file."""
        return self.root_path / ".gjalla.yaml"

    @property
    def is_initialized(self) -> bool:
        """Check if gjalla is initialized in this directory."""
        return self.config_path.exists()


_context: Context | None = None


def get_context() -> Context:
    """Get current context."""
    global _context
    if _context is None:
        _context = Context()
    return _context


def set_context(context: Context) -> None:
    """Set current context."""
    global _context
    _context = context
