"""
Remote Config Fetcher - Fetch configuration from remote URLs

Fetches configuration from well-known URLs:
- `https://{domain}/.well-known/groknroll`
- Git repository config URLs

Supports caching with configurable TTL.

Example:
    fetcher = RemoteConfigFetcher()
    config = await fetcher.fetch("https://example.com/.well-known/groknroll")

    # Or auto-discover from domain
    config = await fetcher.fetch_from_domain("example.com")
"""

import asyncio
import hashlib
import json
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Optional
from urllib.parse import urlparse


@dataclass
class RemoteConfigSource:
    """
    Represents a remote configuration source

    Attributes:
        url: The URL the config was fetched from
        config: The loaded configuration (if successful)
        fetched_at: Timestamp when config was fetched
        expires_at: Timestamp when cached config expires
        error: Error message (if fetching failed)
    """

    url: str
    config: Optional[dict[str, Any]] = None
    fetched_at: Optional[float] = None
    expires_at: Optional[float] = None
    error: Optional[str] = None

    @property
    def is_valid(self) -> bool:
        """Check if config was successfully fetched"""
        return self.config is not None and self.error is None

    @property
    def is_expired(self) -> bool:
        """Check if cached config has expired"""
        if self.expires_at is None:
            return True
        return time.time() > self.expires_at


@dataclass
class RemoteConfigResult:
    """
    Result of remote config fetching

    Attributes:
        config: The merged configuration
        sources: List of remote sources that were checked
        success: Whether fetching succeeded
        from_cache: Whether config was loaded from cache
        error: Error message (if fetching failed)
    """

    config: dict[str, Any] = field(default_factory=dict)
    sources: list[RemoteConfigSource] = field(default_factory=list)
    success: bool = True
    from_cache: bool = False
    error: Optional[str] = None


class RemoteConfigFetcher:
    """
    Fetcher for remote groknroll configurations

    Supports:
    - Well-known URL discovery (/.well-known/groknroll)
    - Direct URL fetching
    - Caching with configurable TTL
    - Timeout handling

    Example:
        fetcher = RemoteConfigFetcher(cache_ttl=3600)  # 1 hour cache

        # Fetch from well-known URL
        result = await fetcher.fetch_from_domain("example.com")

        # Fetch from direct URL
        result = await fetcher.fetch("https://example.com/groknroll.json")

        if result.success:
            print(result.config)
    """

    # Well-known path for groknroll config
    WELL_KNOWN_PATH = "/.well-known/groknroll"

    # Default cache directory
    CACHE_DIR = Path.home() / ".cache" / "groknroll" / "remote-config"

    # Default settings
    DEFAULT_TIMEOUT = 10.0  # seconds
    DEFAULT_CACHE_TTL = 3600  # 1 hour

    def __init__(
        self,
        timeout: float = DEFAULT_TIMEOUT,
        cache_ttl: int = DEFAULT_CACHE_TTL,
        cache_dir: Optional[Path] = None,
    ):
        """
        Initialize RemoteConfigFetcher

        Args:
            timeout: HTTP request timeout in seconds
            cache_ttl: Cache time-to-live in seconds
            cache_dir: Directory for caching configs
        """
        self.timeout = timeout
        self.cache_ttl = cache_ttl
        self.cache_dir = cache_dir or self.CACHE_DIR
        self._cache: dict[str, RemoteConfigSource] = {}

    async def fetch(self, url: str, skip_cache: bool = False) -> RemoteConfigResult:
        """
        Fetch configuration from a URL

        Args:
            url: URL to fetch config from
            skip_cache: Skip cache and force fetch

        Returns:
            RemoteConfigResult with fetched config
        """
        # Check cache first
        if not skip_cache:
            cached = self._get_cached(url)
            if cached and not cached.is_expired:
                return RemoteConfigResult(
                    config=cached.config or {},
                    sources=[cached],
                    success=True,
                    from_cache=True,
                )

        # Fetch from URL
        source = await self._fetch_url(url)

        # Cache if successful
        if source.is_valid:
            self._cache_source(source)

        return RemoteConfigResult(
            config=source.config or {},
            sources=[source],
            success=source.is_valid,
            from_cache=False,
            error=source.error,
        )

    async def fetch_from_domain(
        self,
        domain: str,
        skip_cache: bool = False,
    ) -> RemoteConfigResult:
        """
        Fetch configuration from a domain's well-known URL

        Tries: https://{domain}/.well-known/groknroll

        Args:
            domain: Domain to fetch config from
            skip_cache: Skip cache and force fetch

        Returns:
            RemoteConfigResult with fetched config
        """
        url = f"https://{domain}{self.WELL_KNOWN_PATH}"
        return await self.fetch(url, skip_cache=skip_cache)

    async def fetch_from_git_remote(
        self,
        remote_url: str,
        skip_cache: bool = False,
    ) -> RemoteConfigResult:
        """
        Fetch configuration from a git remote URL

        Extracts domain from git URL and fetches well-known config.

        Args:
            remote_url: Git remote URL (HTTPS or SSH format)
            skip_cache: Skip cache and force fetch

        Returns:
            RemoteConfigResult with fetched config
        """
        domain = self._extract_domain_from_git_url(remote_url)
        if not domain:
            return RemoteConfigResult(
                success=False,
                error=f"Could not extract domain from git URL: {remote_url}",
            )

        return await self.fetch_from_domain(domain, skip_cache=skip_cache)

    async def _fetch_url(self, url: str) -> RemoteConfigSource:
        """
        Fetch config from a URL

        Args:
            url: URL to fetch

        Returns:
            RemoteConfigSource with result
        """
        source = RemoteConfigSource(url=url)

        try:
            import aiohttp
        except ImportError:
            # Fallback to sync requests
            return self._fetch_url_sync(url)

        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(
                    url,
                    timeout=aiohttp.ClientTimeout(total=self.timeout),
                    headers={
                        "Accept": "application/json",
                        "User-Agent": "groknroll/2.0",
                    },
                ) as response:
                    if response.status == 404:
                        source.error = "Config not found (404)"
                        return source

                    if response.status != 200:
                        source.error = f"HTTP {response.status}"
                        return source

                    content = await response.text()
                    config = self._parse_config(content)

                    source.config = config
                    source.fetched_at = time.time()
                    source.expires_at = time.time() + self.cache_ttl

        except asyncio.TimeoutError:
            source.error = f"Timeout after {self.timeout}s"
        except json.JSONDecodeError as e:
            source.error = f"Invalid JSON: {e}"
        except Exception as e:
            source.error = f"Fetch error: {e}"

        return source

    def _fetch_url_sync(self, url: str) -> RemoteConfigSource:
        """
        Fetch config from a URL synchronously (fallback)

        Args:
            url: URL to fetch

        Returns:
            RemoteConfigSource with result
        """
        source = RemoteConfigSource(url=url)

        try:
            import requests

            response = requests.get(
                url,
                timeout=self.timeout,
                headers={
                    "Accept": "application/json",
                    "User-Agent": "groknroll/2.0",
                },
            )

            if response.status_code == 404:
                source.error = "Config not found (404)"
                return source

            if response.status_code != 200:
                source.error = f"HTTP {response.status_code}"
                return source

            config = self._parse_config(response.text)

            source.config = config
            source.fetched_at = time.time()
            source.expires_at = time.time() + self.cache_ttl

        except json.JSONDecodeError as e:
            source.error = f"Invalid JSON: {e}"
        except Exception as e:
            source.error = f"Fetch error: {e}"

        return source

    def _parse_config(self, content: str) -> dict[str, Any]:
        """
        Parse configuration content

        Supports JSONC (JSON with comments).

        Args:
            content: Configuration content

        Returns:
            Parsed configuration dict

        Raises:
            json.JSONDecodeError: If JSON is invalid
        """
        from groknroll.config.loader import parse_jsonc

        return parse_jsonc(content)

    def _get_cached(self, url: str) -> Optional[RemoteConfigSource]:
        """
        Get cached config for a URL

        Checks both memory cache and disk cache.

        Args:
            url: URL to get cached config for

        Returns:
            Cached RemoteConfigSource or None
        """
        # Check memory cache
        if url in self._cache:
            return self._cache[url]

        # Check disk cache
        cache_file = self._get_cache_file(url)
        if cache_file.exists():
            try:
                with open(cache_file) as f:
                    data = json.load(f)

                source = RemoteConfigSource(
                    url=url,
                    config=data.get("config"),
                    fetched_at=data.get("fetched_at"),
                    expires_at=data.get("expires_at"),
                )

                # Store in memory cache
                self._cache[url] = source

                return source
            except (json.JSONDecodeError, KeyError):
                # Invalid cache file
                cache_file.unlink(missing_ok=True)

        return None

    def _cache_source(self, source: RemoteConfigSource) -> None:
        """
        Cache a config source

        Stores in both memory and disk cache.

        Args:
            source: RemoteConfigSource to cache
        """
        # Memory cache
        self._cache[source.url] = source

        # Disk cache
        cache_file = self._get_cache_file(source.url)
        cache_file.parent.mkdir(parents=True, exist_ok=True)

        data = {
            "url": source.url,
            "config": source.config,
            "fetched_at": source.fetched_at,
            "expires_at": source.expires_at,
        }

        with open(cache_file, "w") as f:
            json.dump(data, f)

    def _get_cache_file(self, url: str) -> Path:
        """
        Get cache file path for a URL

        Args:
            url: URL to get cache file for

        Returns:
            Path to cache file
        """
        url_hash = hashlib.sha256(url.encode()).hexdigest()[:16]
        return self.cache_dir / f"{url_hash}.json"

    def _extract_domain_from_git_url(self, url: str) -> Optional[str]:
        """
        Extract domain from a git URL

        Supports:
        - https://github.com/user/repo.git
        - git@github.com:user/repo.git
        - ssh://git@github.com/user/repo.git

        Args:
            url: Git remote URL

        Returns:
            Domain or None if extraction failed
        """
        # HTTPS URL
        if url.startswith(("https://", "http://")):
            parsed = urlparse(url)
            return parsed.netloc

        # SSH URL (git@domain:user/repo)
        if url.startswith("git@"):
            # Extract domain before the colon
            domain = url.split("@")[1].split(":")[0]
            return domain

        # SSH URL (ssh://git@domain/user/repo)
        if url.startswith("ssh://"):
            parsed = urlparse(url)
            # Remove username if present
            netloc = parsed.netloc
            if "@" in netloc:
                netloc = netloc.split("@")[1]
            return netloc

        return None

    def clear_cache(self) -> int:
        """
        Clear all cached configs

        Returns:
            Number of cache entries cleared
        """
        count = len(self._cache)
        self._cache.clear()

        # Clear disk cache
        if self.cache_dir.exists():
            for cache_file in self.cache_dir.glob("*.json"):
                cache_file.unlink()
                count += 1

        return count


# Convenience functions


async def fetch_remote_config(url: str) -> dict[str, Any]:
    """
    Fetch remote configuration from a URL (convenience function)

    Args:
        url: URL to fetch config from

    Returns:
        Configuration dict

    Raises:
        ValueError: If fetching fails
    """
    fetcher = RemoteConfigFetcher()
    result = await fetcher.fetch(url)

    if not result.success:
        raise ValueError(f"Failed to fetch config: {result.error}")

    return result.config


async def fetch_domain_config(domain: str) -> dict[str, Any]:
    """
    Fetch configuration from a domain's well-known URL (convenience function)

    Args:
        domain: Domain to fetch config from

    Returns:
        Configuration dict

    Raises:
        ValueError: If fetching fails
    """
    fetcher = RemoteConfigFetcher()
    result = await fetcher.fetch_from_domain(domain)

    if not result.success:
        raise ValueError(f"Failed to fetch config from {domain}: {result.error}")

    return result.config


def fetch_remote_config_sync(url: str) -> dict[str, Any]:
    """
    Fetch remote configuration synchronously (convenience function)

    Args:
        url: URL to fetch config from

    Returns:
        Configuration dict

    Raises:
        ValueError: If fetching fails
    """
    fetcher = RemoteConfigFetcher()
    source = fetcher._fetch_url_sync(url)

    if not source.is_valid:
        raise ValueError(f"Failed to fetch config: {source.error}")

    return source.config or {}
