"""
Dark Web Search - Search dark web via multiple search engines

Provides multi-threaded search across 16 dark web search engines via Tor proxy.

Features:
- Multi-threaded concurrent searching
- Automatic deduplication
- Integrated threat blocklist filtering
- Safe search mode to avoid dangerous sites

Reliability Features (REL-001):
- Specific exception handling instead of bare except
- Proper error logging
- Resource cleanup on failure
"""

import logging
import re
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeoutError, as_completed
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

import requests.exceptions
from bs4 import BeautifulSoup

from groknroll.osint.tor_manager import TorManager


# REL-001: Configure logger for error tracking
logger = logging.getLogger(__name__)


# VAL-001: Input validation for search queries
MAX_QUERY_LENGTH = 500
MIN_QUERY_LENGTH = 1
FORBIDDEN_CHARS = set('\x00\r\n')  # Null bytes and newlines


class QueryValidationError(ValueError):
    """Raised when search query validation fails."""
    pass


def validate_search_query(query: str) -> str:
    """
    Validate and sanitize a search query.

    VAL-001: Input validation for search operations.

    Args:
        query: The search query to validate

    Returns:
        Sanitized query string

    Raises:
        QueryValidationError: If query is invalid
    """
    if not query:
        raise QueryValidationError("Search query cannot be empty")

    # Strip whitespace
    query = query.strip()

    # Check length
    if len(query) < MIN_QUERY_LENGTH:
        raise QueryValidationError(f"Query too short (min {MIN_QUERY_LENGTH} characters)")

    if len(query) > MAX_QUERY_LENGTH:
        raise QueryValidationError(f"Query too long (max {MAX_QUERY_LENGTH} characters)")

    # Check for forbidden characters
    found_forbidden = set(query) & FORBIDDEN_CHARS
    if found_forbidden:
        raise QueryValidationError(
            f"Query contains forbidden characters: {repr(found_forbidden)}"
        )

    # Basic sanitization: collapse multiple spaces
    query = " ".join(query.split())

    return query

# Import blocklist (optional - graceful fallback if not available)
try:
    from groknroll.osint.blocklist import OSINTBlocklist, get_blocklist

    BLOCKLIST_AVAILABLE = True
except ImportError:
    BLOCKLIST_AVAILABLE = False

# Default dark web search engines with query URL templates
SEARCH_ENGINES = {
    "ahmia": "http://juhanurmihxlp77nkq76byazcldy2hlmovfu2epvl5ankdibsot4csyd.onion/search/?q={query}",
    "onionland": "http://3bbad7fauom4d6sgppalyqddsqbf5u5p56b5k5uk2zxsy3d6ey2jobad.onion/search?q={query}",
    "torgle": "http://iy3544gmoeclh5de6gez2256v6pjh4omhpqdh2wpeeppjtvqmjhkfwad.onion/torgle/?query={query}",
    "amnesia": "http://amnesia7u5odx5xbwtpnqk3edybgud5bmiagu75bnqx2crntw5kry7ad.onion/search?query={query}",
    "kaizer": "http://kaizerwfvp5gxu6cppibp7jhcqptavq3iqef66wbxenh6a2fklibdvid.onion/search?q={query}",
    "anima": "http://anima4ffe27xmakwnseih3ic2y7y3l6e7fucwk4oerdn4odf7k74tbid.onion/search?q={query}",
    "tornado": "http://tornadoxn3viscgz647shlysdy7ea5zqzwda7hierekeuokh5eh5b3qd.onion/search?q={query}",
    "tornet": "http://tornetupfu7gcgidt33ftnungxzyfq2pygui5qdoyss34xbgx2qruzid.onion/search?q={query}",
    "torland": "http://torlbmqwtudkorme6prgfpmsnile7ug2zm4u3ejpcncxuhpu4k2j4kyd.onion/index.php?a=search&q={query}",
    "findtor": "http://findtorroveq5wdnipkaojfpqulxnkhblymc7aramjzajcvpptd4rjqd.onion/search?q={query}",
    "excavator": "http://2fd6cemt4gmccflhm6imvdfvli3nf7zn6rfrwpsy7uhxrgbypvwf5fad.onion/search?query={query}",
    "onionway": "http://oniwayzz74cv2puhsgx4dpjwieww4wdphsydqvf5q7eyz4myjvyw26ad.onion/search.php?s={query}",
    "tor66": "http://tor66sewebgixwhcqfnp5inzp5x5uohhdy3kvtnyfxc2e5mxiuh34iid.onion/search?q={query}",
    "oss": "http://3fzh7yuupdfyjhwt3ugzqqof6ulbcl27ecev33knxe3u7goi3vfn2qqd.onion/oss/index.php?search={query}",
    "torgol": "http://torgolnpeouim56dykfob6jh5r2ps2j73enc42s2um4ufob3ny4fcdyd.onion/?q={query}",
    "deepsearches": "http://searchgf7gdtauh7bhnbyed4ivxqmuoat3nm6zfrg3ymkq6mtnpye3ad.onion/search?q={query}",
}


@dataclass
class SearchResult:
    """Represents a single search result from dark web engines."""

    url: str
    title: str
    source_engine: str
    found_at: datetime = field(default_factory=datetime.utcnow)
    relevance_score: float = 0.0

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "url": self.url,
            "title": self.title,
            "source_engine": self.source_engine,
            "found_at": self.found_at.isoformat(),
            "relevance_score": self.relevance_score,
        }


class DarkWebSearch:
    """
    Search dark web via multiple search engines through Tor proxy.

    Provides multi-threaded searching across 16 dark web search engines
    with automatic deduplication, result ranking, and threat filtering.

    Features:
        - Multi-threaded concurrent searching
        - Automatic result deduplication
        - Integrated blocklist for dangerous site filtering
        - Safe search mode to avoid malware/exploit sites

    Usage:
        tor = TorManager()
        search = DarkWebSearch(tor)
        results = search.search("query terms")

        # With safe mode (filters dangerous sites)
        safe_results = search.search("query terms", safe_mode=True)
    """

    def __init__(
        self,
        tor_manager: TorManager,
        max_workers: int = 5,
        timeout: int = 45,
        enable_blocklist: bool = True,
    ):
        """
        Initialize dark web search.

        Args:
            tor_manager: TorManager instance for proxy connections
            max_workers: Maximum concurrent search threads
            timeout: Request timeout per engine in seconds
            enable_blocklist: Enable threat blocklist filtering
        """
        self.tor_manager = tor_manager
        self.max_workers = max_workers
        self.timeout = timeout
        self.engines = SEARCH_ENGINES.copy()

        # Initialize blocklist
        self._blocklist = None
        if enable_blocklist and BLOCKLIST_AVAILABLE:
            self._blocklist = get_blocklist()
        self._blocked_results: list[dict] = []

    def search(
        self,
        query: str,
        engines: Optional[list[str]] = None,
        limit: int = 100,
        safe_mode: bool = True,
    ) -> list[SearchResult]:
        """
        Search across multiple dark web engines.

        VAL-001: Validates and sanitizes query input.

        Args:
            query: Search query string
            engines: List of engine names to use (default: all)
            limit: Maximum results to return
            safe_mode: Filter out dangerous/malicious sites (default: True)

        Returns:
            List of deduplicated SearchResult objects

        Raises:
            QueryValidationError: If query is invalid
        """
        # VAL-001: Validate and sanitize query
        query = validate_search_query(query)

        # Reset blocked results tracking
        self._blocked_results = []

        # Select engines to use
        if engines:
            selected_engines = {k: v for k, v in self.engines.items() if k in engines}
        else:
            selected_engines = self.engines

        results: list[SearchResult] = []

        # Search engines concurrently
        # REL-001: Improved exception handling with specific exception types
        with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
            futures = {
                executor.submit(self._search_engine, engine_name, url_template, query): engine_name
                for engine_name, url_template in selected_engines.items()
            }

            for future in as_completed(futures):
                engine_name = futures[future]
                try:
                    engine_results = future.result(timeout=self.timeout + 10)
                    results.extend(engine_results)
                except FuturesTimeoutError:
                    logger.warning(f"Search engine '{engine_name}' timed out")
                except requests.exceptions.ConnectionError as e:
                    logger.warning(f"Connection error for engine '{engine_name}': {e}")
                except requests.exceptions.Timeout as e:
                    logger.warning(f"Request timeout for engine '{engine_name}': {e}")
                except requests.exceptions.RequestException as e:
                    logger.warning(f"Request error for engine '{engine_name}': {e}")
                except ValueError as e:
                    logger.warning(f"Parsing error for engine '{engine_name}': {e}")
                except Exception as e:
                    # REL-001: Log unexpected errors but continue with other engines
                    logger.error(f"Unexpected error for engine '{engine_name}': {type(e).__name__}: {e}")

        # Deduplicate results
        unique_results = self._deduplicate(results)

        # Apply blocklist filtering in safe mode
        if safe_mode and self._blocklist:
            unique_results = self._filter_dangerous(unique_results)

        return unique_results[:limit]

    def _filter_dangerous(self, results: list[SearchResult]) -> list[SearchResult]:
        """
        Filter out dangerous/malicious sites using blocklist.

        Args:
            results: Search results to filter

        Returns:
            Safe results with dangerous sites removed
        """
        if not self._blocklist:
            return results

        safe_results = []

        for result in results:
            blocked = self._blocklist.check_url(result.url)
            if blocked:
                # Track blocked result
                self._blocked_results.append({
                    "url": result.url,
                    "title": result.title,
                    "category": blocked.category.value,
                    "severity": blocked.severity.value,
                    "description": blocked.description,
                    "threat_actor": blocked.threat_actor,
                })
            else:
                safe_results.append(result)

        return safe_results

    def get_blocked_results(self) -> list[dict]:
        """
        Get list of results that were blocked in the last search.

        Returns:
            List of blocked result info dicts
        """
        return self._blocked_results.copy()

    def search_unsafe(
        self,
        query: str,
        engines: Optional[list[str]] = None,
        limit: int = 100,
    ) -> list[SearchResult]:
        """
        Search WITHOUT blocklist filtering.

        WARNING: Results may include malware, exploit kits, and other
        dangerous sites. Use with extreme caution.

        Args:
            query: Search query string
            engines: List of engine names to use
            limit: Maximum results

        Returns:
            Unfiltered search results
        """
        return self.search(query, engines, limit, safe_mode=False)

    def search_single_engine(self, query: str, engine: str) -> list[SearchResult]:
        """
        Search a specific dark web engine.

        Args:
            query: Search query string
            engine: Engine name (e.g., 'ahmia', 'tor66')

        Returns:
            List of SearchResult objects from that engine

        Raises:
            ValueError: If engine name is not recognized
        """
        if engine not in self.engines:
            raise ValueError(f"Unknown engine: {engine}. Available: {list(self.engines.keys())}")

        return self._search_engine(engine, self.engines[engine], query)

    def _search_engine(
        self,
        engine_name: str,
        url_template: str,
        query: str,
    ) -> list[SearchResult]:
        """
        Search a single engine and parse results.

        Args:
            engine_name: Name of the search engine
            url_template: URL template with {query} placeholder
            query: Search query string

        Returns:
            List of SearchResult objects
        """
        url = url_template.format(query=query)
        session = self.tor_manager.get_session(timeout=self.timeout)
        headers = self.tor_manager.get_headers()

        # REL-001: Improved exception handling with specific types
        try:
            response = session.get(url, headers=headers, timeout=self.timeout)
            if response.status_code != 200:
                logger.debug(f"Engine {engine_name} returned status {response.status_code}")
                return []

            soup = BeautifulSoup(response.text, "html.parser")
            results = []

            # Generic parsing for standard search engine layouts
            for a in soup.find_all("a"):
                try:
                    href = a.get("href", "")
                    title = a.get_text(strip=True)

                    # Extract onion links
                    onion_links = re.findall(r"https?://[a-z0-9.]+\.onion[^\s\"'<>]*", href)
                    if onion_links:
                        link = onion_links[0]
                        # Filter out self-referential and search links
                        if "search" not in link.lower() and len(title) > 3:
                            results.append(
                                SearchResult(
                                    url=link,
                                    title=title[:200],  # Truncate long titles
                                    source_engine=engine_name,
                                )
                            )
                except (AttributeError, TypeError) as e:
                    # REL-001: Specific exceptions for HTML parsing issues
                    logger.debug(f"Error parsing link in {engine_name}: {e}")
                    continue

            return results

        except requests.exceptions.Timeout:
            logger.debug(f"Timeout searching {engine_name}")
            return []
        except requests.exceptions.ConnectionError as e:
            logger.debug(f"Connection error for {engine_name}: {e}")
            return []
        except requests.exceptions.RequestException as e:
            logger.debug(f"Request error for {engine_name}: {e}")
            return []
        except Exception as e:
            # REL-001: Log unexpected exceptions
            logger.warning(f"Unexpected error searching {engine_name}: {type(e).__name__}: {e}")
            return []

    def _deduplicate(self, results: list[SearchResult]) -> list[SearchResult]:
        """
        Remove duplicate results based on URL.

        Args:
            results: List of SearchResult objects

        Returns:
            Deduplicated list preserving first occurrence
        """
        seen_urls: set[str] = set()
        unique_results: list[SearchResult] = []

        for result in results:
            # Normalize URL by removing trailing slashes
            clean_url = result.url.rstrip("/")
            if clean_url not in seen_urls:
                seen_urls.add(clean_url)
                unique_results.append(result)

        return unique_results

    def get_available_engines(self) -> list[str]:
        """Get list of available search engine names."""
        return list(self.engines.keys())

    def add_engine(self, name: str, url_template: str) -> None:
        """
        Add a custom search engine.

        Args:
            name: Engine name identifier
            url_template: URL template with {query} placeholder
        """
        if "{query}" not in url_template:
            raise ValueError("URL template must contain {query} placeholder")
        self.engines[name] = url_template

    def remove_engine(self, name: str) -> bool:
        """
        Remove a search engine.

        Args:
            name: Engine name to remove

        Returns:
            True if removed, False if not found
        """
        if name in self.engines:
            del self.engines[name]
            return True
        return False
