import asyncio
import base64
import logging
import re
from pathlib import Path
from typing import Callable, Set

import httpx

from auditor.core.factory import ValidatorFactory

logger = logging.getLogger(__name__)


class AsyncKeyAuditor:
    """
    Asynchronous LLMLeaks for scanning GitHub repositories.
    
    This class implements a non-blocking I/O approach using asyncio
    and httpx to efficiently scan multiple files and validate keys
    against various API providers.
    
    Attributes:
        github_token: Personal access token for GitHub API
        max_concurrent: Maximum number of concurrent requests
        found_keys: Set of already discovered keys (deduplication)
        
    Example:
        auditor = AsyncKeyAuditor("ghp_xxx", max_concurrent=5)
        await auditor.run(
            query="API_KEY",
            pages=5,
            output_file="results.txt"
        )
    """
    
    GITHUB_API_BASE = "https://api.github.com"
    REQUEST_DELAY = 0.7  # Seconds between requests for rate limiting
    RATE_LIMIT_COOLDOWN = 30  # Seconds to wait when rate limited
    
    def __init__(
        self,
        github_token: str,
        max_concurrent: int = 5,
        on_key_found: Callable[[str, str, bool], None] | None = None,
        on_page_start: Callable[[int], None] | None = None
    ):
        """
        Initialize the auditor.
        
        Args:
            github_token: GitHub Personal Access Token
            max_concurrent: Maximum concurrent requests (default: 5)
            on_key_found: Optional callback when a key is found
                         Signature: (provider, key, is_valid) -> None
            on_page_start: Optional callback when processing a page starts
                          Signature: (page_number) -> None
        """
        self.headers = {
            "Authorization": f"token {github_token}",
            "Accept": "application/vnd.github.v3+json",
            "User-Agent": "LLMLeaks/1.0"
        }
        self.found_keys: Set[str] = set()
        self.semaphore = asyncio.Semaphore(max_concurrent)
        self.on_key_found = on_key_found
        self.on_page_start = on_page_start
        self._valid_count = 0
        self._scanned_count = 0
    
    @property
    def stats(self) -> dict:
        """Get current scan statistics."""
        return {
            "scanned": self._scanned_count,
            "valid": self._valid_count,
            "unique_keys": len(self.found_keys)
        }
    
    async def _safe_request(
        self,
        client: httpx.AsyncClient,
        url: str
    ) -> httpx.Response | None:
        """
        Make a rate-limited HTTP request.
        
        Args:
            client: The HTTP client to use
            url: The URL to request
            
        Returns:
            Response object or None on error
        """
        async with self.semaphore:
            try:
                await asyncio.sleep(self.REQUEST_DELAY)
                return await client.get(url)
            except httpx.HTTPError as e:
                logger.debug(f"Request error for {url}: {e}")
                return None
    
    async def process_file(
        self,
        client: httpx.AsyncClient,
        file_url: str,
        output_file: str
    ) -> None:
        """
        Process a single file from GitHub and scan for API keys.
        
        Args:
            client: HTTP client instance
            file_url: GitHub API URL for the file
            output_file: Path to write valid keys
        """
        response = await self._safe_request(client, file_url)
        
        if not response or response.status_code != 200:
            return
        
        try:
            raw_content = response.json().get("content", "")
            content = base64.b64decode(raw_content).decode("utf-8", errors="ignore")
            self._scanned_count += 1
            
            # Scan for all supported providers
            for provider, (pattern, validator) in ValidatorFactory.get_config().items():
                for key in re.findall(pattern, content):
                    await self._process_key(
                        client, provider, key, validator, output_file
                    )
                    
        except (ValueError, KeyError) as e:
            logger.debug(f"Error processing file {file_url}: {e}")
    
    async def _process_key(
        self,
        client: httpx.AsyncClient,
        provider: str,
        key: str,
        validator,
        output_file: str
    ) -> None:
        """Process a single discovered key."""
        if key in self.found_keys:
            return
        
        self.found_keys.add(key)
        is_valid = await validator.validate(client, key)
        
        if self.on_key_found:
            self.on_key_found(provider, key, is_valid)
        
        if is_valid:
            self._valid_count += 1
            logger.debug(f"[VALID] {provider.upper()}: {key[:15]}...")
            self._write_result(output_file, provider, key)
        else:
            logger.debug(f"[INVALID] {provider.upper()}: {key[:10]}...")
    
    def _write_result(self, output_file: str, provider: str, key: str) -> None:
        """Write a valid key to the output file."""
        output_path = Path(output_file)
        output_path.parent.mkdir(parents=True, exist_ok=True)
        
        with open(output_path, "a", encoding="utf-8") as f:
            f.write(f"[{provider.upper()}] {key}\n")
    
    async def run(
        self,
        query: str,
        pages: int,
        output_file: str,
        per_page: int = 50
    ) -> dict:
        """
        Execute the audit scan.
        
        Args:
            query: GitHub code search query
            pages: Number of result pages to process
            output_file: Path for writing valid keys
            per_page: Results per page (max 100)
            
        Returns:
            Dictionary containing scan statistics
        """
        async with httpx.AsyncClient(
            headers=self.headers,
            timeout=20.0
        ) as client:
            logger.debug(f"Starting scan for query: '{query}'")
            
            for page in range(1, pages + 1):
                if self.on_page_start:
                    self.on_page_start(page)
                logger.debug(f"Processing page {page}/{pages}")
                
                search_url = (
                    f"{self.GITHUB_API_BASE}/search/code"
                    f"?q={query}&page={page}&per_page={per_page}"
                )
                
                response = await self._safe_request(client, search_url)
                
                if response and response.status_code == 403:
                    logger.debug("Rate limit hit. Cooling down...")
                    await asyncio.sleep(self.RATE_LIMIT_COOLDOWN)
                    continue
                
                if not response:
                    logger.debug(f"No response for page {page}")
                    continue
                
                items = response.json().get("items", [])
                if not items:
                    logger.debug("No more results found")
                    break
                
                tasks = [
                    self.process_file(client, item["url"], output_file)
                    for item in items
                ]
                await asyncio.gather(*tasks)
            
            logger.debug(
                f"Scan complete. Found {self._valid_count} valid keys "
                f"out of {len(self.found_keys)} unique keys"
            )
            
            return self.stats
