"""PromptCanvas Python SDK Client."""

import time
import asyncio
from typing import Dict, List, Optional, Union, Any
from urllib.parse import urlencode, quote

import httpx

from .cache import LRUCache, AsyncLRUCache
from .errors import (
    AuthenticationError,
    NetworkError,
    RateLimitError,
    TimeoutError,
    map_api_error,
)
from .models import (
    Prompt,
    PromptMeta,
    SlugSuggestion,
    CacheStats,
    BatchResult,
    ListResult,
)
from .variables import VariableResolver

DEFAULT_BASE_URL = "https://lpyabethlowrowqvrlqv.supabase.co/functions/v1/sdk"
DEFAULT_TIMEOUT = 10.0
DEFAULT_MAX_RETRIES = 3
DEFAULT_CACHE_TTL = 300.0  # 5 minutes
DEFAULT_CACHE_MAX_SIZE = 1000

VariableValue = Union[str, int, float, bool]


class PromptCanvas:
    """
    PromptCanvas SDK Client.
    
    High-performance client for fetching prompts with built-in caching,
    prefetching, and variable resolution.
    
    Supports both sync and async operations:
    - Sync: `pc.get("slug")`, `pc.get_many(["a", "b"])`
    - Async: `await pc.aget("slug")`, `await pc.aget_many(["a", "b"])`
    
    Example:
        >>> pc = PromptCanvas(api_key="pc_live_xxx")
        >>> 
        >>> # Sync usage
        >>> prompt = pc.get("system-prompt")
        >>> print(prompt.content)
        >>> 
        >>> # With variables
        >>> prompt = pc.get("welcome", variables={"name": "John"})
        >>> 
        >>> # Async usage
        >>> prompt = await pc.aget("system-prompt")
    """

    def __init__(
        self,
        api_key: str,
        base_url: str = DEFAULT_BASE_URL,
        timeout: float = DEFAULT_TIMEOUT,
        cache_ttl: float = DEFAULT_CACHE_TTL,
        cache_max_size: int = DEFAULT_CACHE_MAX_SIZE,
        stale_while_revalidate: bool = True,
        max_retries: int = DEFAULT_MAX_RETRIES,
        prefetch: Optional[List[str]] = None,
        debug: bool = False,
    ):
        """
        Initialize PromptCanvas client.
        
        Args:
            api_key: Your PromptCanvas API key (required)
            base_url: API base URL (default: PromptCanvas cloud)
            timeout: Request timeout in seconds (default: 10)
            cache_ttl: Cache time-to-live in seconds (default: 300)
            cache_max_size: Maximum cached prompts (default: 1000)
            stale_while_revalidate: Return stale cache while refreshing (default: True)
            max_retries: Max retry attempts for failed requests (default: 3)
            prefetch: List of slugs to prefetch on init (optional)
            debug: Enable debug logging (default: False)
        """
        if not api_key:
            raise AuthenticationError("API key is required")

        self._api_key = api_key
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout
        self._max_retries = max_retries
        self._stale_while_revalidate = stale_while_revalidate
        self._debug = debug

        # Initialize cache
        self._cache = LRUCache(max_size=cache_max_size, ttl_seconds=cache_ttl)
        self._async_cache = AsyncLRUCache(max_size=cache_max_size, ttl_seconds=cache_ttl)

        # HTTP clients (lazy initialized)
        self._sync_client: Optional[httpx.Client] = None
        self._async_client: Optional[httpx.AsyncClient] = None

        # Prefetch prompts
        if prefetch:
            try:
                self.warmup(prefetch)
            except Exception as e:
                self._log("warn", f"Prefetch failed: {e}")

    def _get_sync_client(self) -> httpx.Client:
        """Get or create sync HTTP client."""
        if self._sync_client is None:
            self._sync_client = httpx.Client(
                timeout=self._timeout,
                headers={
                    "x-api-key": self._api_key,
                    "Content-Type": "application/json",
                },
            )
        return self._sync_client

    def _get_async_client(self) -> httpx.AsyncClient:
        """Get or create async HTTP client."""
        if self._async_client is None:
            self._async_client = httpx.AsyncClient(
                timeout=self._timeout,
                headers={
                    "x-api-key": self._api_key,
                    "Content-Type": "application/json",
                },
            )
        return self._async_client

    def _log(self, level: str, message: str) -> None:
        """Log a message if debug is enabled."""
        if self._debug or level != "debug":
            if level == "debug" and not self._debug:
                return
            print(f"[PromptCanvas] {level.upper()}: {message}")

    # ==================== SYNC METHODS ====================

    def get(
        self,
        slug: str,
        variables: Optional[Dict[str, VariableValue]] = None,
        skip_cache: bool = False,
    ) -> Prompt:
        """
        Fetch a single prompt by slug (sync).
        
        Args:
            slug: The prompt slug to fetch
            variables: Variables to interpolate into the template
            skip_cache: Force fresh fetch from server
            
        Returns:
            Prompt object with resolved variables
            
        Raises:
            PromptNotFoundError: If prompt doesn't exist
            NoProductionVersionError: If prompt has no production version
        """
        # Check cache first
        if not skip_cache:
            cached = self._cache.get(slug)
            if cached and not self._cache.is_stale(cached):
                return self._resolve_variables(cached.prompt, variables)

            if cached and self._stale_while_revalidate:
                if self._cache.is_within_grace_period(cached):
                    # Return stale, refresh in background (sync can't do background)
                    return self._resolve_variables(cached.prompt, variables)

        # Fetch from server
        prompt = self._fetch_and_cache_sync(slug)
        return self._resolve_variables(prompt, variables)

    def get_many(
        self,
        slugs: List[str],
        variables: Optional[Dict[str, VariableValue]] = None,
        skip_cache: bool = False,
    ) -> BatchResult:
        """
        Batch fetch multiple prompts (sync).
        
        Args:
            slugs: List of prompt slugs to fetch
            variables: Variables to apply to all prompts
            skip_cache: Force fresh fetch for all
            
        Returns:
            BatchResult with prompts list and errors list
        """
        if not slugs:
            return BatchResult(prompts=[], errors=[])

        cached_prompts: List[Prompt] = []
        to_fetch: List[str] = []

        if not skip_cache:
            for slug in slugs:
                cached = self._cache.get(slug)
                if cached and not self._cache.is_stale(cached):
                    cached_prompts.append(cached.prompt)
                else:
                    to_fetch.append(slug)
        else:
            to_fetch = list(slugs)

        # Fetch missing
        fetched = BatchResult(prompts=[], errors=[])
        if to_fetch:
            fetched = self._fetch_batch_sync(to_fetch)
            for prompt in fetched.prompts:
                self._cache.set(prompt.slug, prompt)

        # Combine and resolve
        all_prompts = [
            self._resolve_variables(p, variables)
            for p in cached_prompts + fetched.prompts
        ]

        return BatchResult(prompts=all_prompts, errors=fetched.errors)

    def suggest(self, query: str, limit: int = 20) -> List[SlugSuggestion]:
        """
        Get autocomplete suggestions for slug input (sync).
        
        Args:
            query: Partial slug to search for
            limit: Maximum suggestions (default: 20, max: 50)
            
        Returns:
            List of matching slug suggestions
        """
        limit = min(limit, 50)
        response = self._request_sync(
            f"/prompts/suggest?q={quote(query)}&limit={limit}"
        )
        return [SlugSuggestion.from_dict(s) for s in response.get("data", [])]

    def list(
        self,
        category: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> ListResult:
        """
        List all available prompts (sync).
        
        Args:
            category: Filter by category
            limit: Maximum results (default: 100)
            offset: Pagination offset
            
        Returns:
            ListResult with prompts and total count
        """
        params = {"limit": min(limit, 100), "offset": offset}
        if category:
            params["category"] = category

        url = f"/prompts?{urlencode(params)}"
        response = self._request_sync(url)
        data = response.get("data", {})

        return ListResult(
            prompts=[PromptMeta.from_dict(p) for p in data.get("prompts", [])],
            total=data.get("total", 0),
        )

    def warmup(self, slugs: List[str]) -> None:
        """Pre-populate cache with specific prompts (sync)."""
        if not slugs:
            return
        self._log("debug", f"Warming up cache with {len(slugs)} prompts")
        self.get_many(slugs, skip_cache=True)

    def invalidate(self, slug: str) -> None:
        """Remove a prompt from cache."""
        self._cache.delete(slug)

    def clear_cache(self) -> None:
        """Clear all cached prompts."""
        self._cache.clear()

    def get_cache_stats(self) -> CacheStats:
        """Get cache statistics."""
        return self._cache.get_stats()

    def _fetch_and_cache_sync(self, slug: str) -> Prompt:
        """Fetch a prompt and store in cache (sync)."""
        response = self._request_sync(f"/prompts/{quote(slug)}")
        if not response.get("success"):
            raise map_api_error(response.get("error", {}))

        prompt = Prompt.from_dict(response.get("data", {}))
        self._cache.set(slug, prompt)
        return prompt

    def _fetch_batch_sync(self, slugs: List[str]) -> BatchResult:
        """Batch fetch prompts (sync)."""
        slugs_param = ",".join(quote(s) for s in slugs)
        response = self._request_sync(f"/prompts/batch?slugs={slugs_param}")

        if not response.get("success"):
            raise map_api_error(response.get("error", {}))

        data = response.get("data", {})
        return BatchResult(
            prompts=[Prompt.from_dict(p) for p in data.get("prompts", [])],
            errors=data.get("errors", []),
        )

    def _request_sync(self, path: str) -> dict:
        """Make a sync HTTP request with retries."""
        client = self._get_sync_client()
        url = f"{self._base_url}{path}"
        last_error: Optional[Exception] = None

        for attempt in range(self._max_retries + 1):
            try:
                response = client.get(url)

                # Check rate limiting
                remaining = int(response.headers.get("X-RateLimit-Remaining", "1000"))
                if remaining < 100:
                    self._log("warn", f"Rate limit warning: {remaining} remaining")

                if response.status_code == 429:
                    retry_after = int(response.headers.get("Retry-After", "60"))
                    raise RateLimitError(retry_after * 1000, remaining)

                return response.json()

            except RateLimitError:
                raise
            except httpx.TimeoutException:
                raise TimeoutError(int(self._timeout * 1000))
            except Exception as e:
                last_error = e
                if attempt < self._max_retries:
                    delay = min(0.1 * (2 ** attempt), 5.0)
                    self._log("debug", f"Retry {attempt + 1}/{self._max_retries} in {delay}s")
                    time.sleep(delay)

        raise NetworkError(
            f"Request failed after {self._max_retries + 1} attempts",
            last_error,
        )

    # ==================== ASYNC METHODS ====================

    async def aget(
        self,
        slug: str,
        variables: Optional[Dict[str, VariableValue]] = None,
        skip_cache: bool = False,
    ) -> Prompt:
        """
        Fetch a single prompt by slug (async).
        
        Args:
            slug: The prompt slug to fetch
            variables: Variables to interpolate into the template
            skip_cache: Force fresh fetch from server
            
        Returns:
            Prompt object with resolved variables
        """
        if not skip_cache:
            cached = await self._async_cache.aget(slug)
            if cached and not self._async_cache.is_stale(cached):
                return self._resolve_variables(cached.prompt, variables)

            if cached and self._stale_while_revalidate:
                if self._async_cache.is_within_grace_period(cached):
                    # Start background refresh
                    asyncio.create_task(self._afetch_and_cache(slug))
                    return self._resolve_variables(cached.prompt, variables)

        prompt = await self._afetch_and_cache(slug)
        return self._resolve_variables(prompt, variables)

    async def aget_many(
        self,
        slugs: List[str],
        variables: Optional[Dict[str, VariableValue]] = None,
        skip_cache: bool = False,
    ) -> BatchResult:
        """
        Batch fetch multiple prompts (async).
        
        Args:
            slugs: List of prompt slugs to fetch
            variables: Variables to apply to all prompts
            skip_cache: Force fresh fetch for all
            
        Returns:
            BatchResult with prompts list and errors list
        """
        if not slugs:
            return BatchResult(prompts=[], errors=[])

        cached_prompts: List[Prompt] = []
        to_fetch: List[str] = []

        if not skip_cache:
            for slug in slugs:
                cached = await self._async_cache.aget(slug)
                if cached and not self._async_cache.is_stale(cached):
                    cached_prompts.append(cached.prompt)
                else:
                    to_fetch.append(slug)
        else:
            to_fetch = list(slugs)

        fetched = BatchResult(prompts=[], errors=[])
        if to_fetch:
            fetched = await self._afetch_batch(to_fetch)
            for prompt in fetched.prompts:
                await self._async_cache.aset(prompt.slug, prompt)

        all_prompts = [
            self._resolve_variables(p, variables)
            for p in cached_prompts + fetched.prompts
        ]

        return BatchResult(prompts=all_prompts, errors=fetched.errors)

    async def asuggest(self, query: str, limit: int = 20) -> List[SlugSuggestion]:
        """Get autocomplete suggestions (async)."""
        limit = min(limit, 50)
        response = await self._arequest(f"/prompts/suggest?q={quote(query)}&limit={limit}")
        return [SlugSuggestion.from_dict(s) for s in response.get("data", [])]

    async def alist(
        self,
        category: Optional[str] = None,
        limit: int = 100,
        offset: int = 0,
    ) -> ListResult:
        """List all available prompts (async)."""
        params = {"limit": min(limit, 100), "offset": offset}
        if category:
            params["category"] = category

        url = f"/prompts?{urlencode(params)}"
        response = await self._arequest(url)
        data = response.get("data", {})

        return ListResult(
            prompts=[PromptMeta.from_dict(p) for p in data.get("prompts", [])],
            total=data.get("total", 0),
        )

    async def awarmup(self, slugs: List[str]) -> None:
        """Pre-populate cache with specific prompts (async)."""
        if not slugs:
            return
        self._log("debug", f"Warming up cache with {len(slugs)} prompts")
        await self.aget_many(slugs, skip_cache=True)

    async def ainvalidate(self, slug: str) -> None:
        """Remove a prompt from cache (async)."""
        await self._async_cache.adelete(slug)

    async def aclear_cache(self) -> None:
        """Clear all cached prompts (async)."""
        await self._async_cache.aclear()

    async def _afetch_and_cache(self, slug: str) -> Prompt:
        """Fetch a prompt and store in cache (async)."""
        response = await self._arequest(f"/prompts/{quote(slug)}")
        if not response.get("success"):
            raise map_api_error(response.get("error", {}))

        prompt = Prompt.from_dict(response.get("data", {}))
        await self._async_cache.aset(slug, prompt)
        return prompt

    async def _afetch_batch(self, slugs: List[str]) -> BatchResult:
        """Batch fetch prompts (async)."""
        slugs_param = ",".join(quote(s) for s in slugs)
        response = await self._arequest(f"/prompts/batch?slugs={slugs_param}")

        if not response.get("success"):
            raise map_api_error(response.get("error", {}))

        data = response.get("data", {})
        return BatchResult(
            prompts=[Prompt.from_dict(p) for p in data.get("prompts", [])],
            errors=data.get("errors", []),
        )

    async def _arequest(self, path: str) -> dict:
        """Make an async HTTP request with retries."""
        client = self._get_async_client()
        url = f"{self._base_url}{path}"
        last_error: Optional[Exception] = None

        for attempt in range(self._max_retries + 1):
            try:
                response = await client.get(url)

                remaining = int(response.headers.get("X-RateLimit-Remaining", "1000"))
                if remaining < 100:
                    self._log("warn", f"Rate limit warning: {remaining} remaining")

                if response.status_code == 429:
                    retry_after = int(response.headers.get("Retry-After", "60"))
                    raise RateLimitError(retry_after * 1000, remaining)

                return response.json()

            except RateLimitError:
                raise
            except httpx.TimeoutException:
                raise TimeoutError(int(self._timeout * 1000))
            except Exception as e:
                last_error = e
                if attempt < self._max_retries:
                    delay = min(0.1 * (2 ** attempt), 5.0)
                    self._log("debug", f"Retry {attempt + 1}/{self._max_retries} in {delay}s")
                    await asyncio.sleep(delay)

        raise NetworkError(
            f"Request failed after {self._max_retries + 1} attempts",
            last_error,
        )

    # ==================== UTILITIES ====================

    def _resolve_variables(
        self,
        prompt: Prompt,
        variables: Optional[Dict[str, VariableValue]],
    ) -> Prompt:
        """Resolve variables in prompt content."""
        if not variables:
            return prompt

        resolved_content = VariableResolver.resolve(prompt.content, variables)
        return Prompt(
            slug=prompt.slug,
            name=prompt.name,
            content=resolved_content,
            version=prompt.version,
            stage=prompt.stage,
            variables=prompt.variables,
            category=prompt.category,
            tags=prompt.tags,
            updated_at=prompt.updated_at,
            etag=prompt.etag,
        )

    def extract_variables(self, template: str) -> List[str]:
        """Extract variable names from a template."""
        return VariableResolver.extract(template)

    def close(self) -> None:
        """Close HTTP clients."""
        if self._sync_client:
            self._sync_client.close()
            self._sync_client = None

    async def aclose(self) -> None:
        """Close async HTTP client."""
        if self._async_client:
            await self._async_client.aclose()
            self._async_client = None

    def __enter__(self) -> "PromptCanvas":
        return self

    def __exit__(self, *args: Any) -> None:
        self.close()

    async def __aenter__(self) -> "PromptCanvas":
        return self

    async def __aexit__(self, *args: Any) -> None:
        await self.aclose()
