"""Compliance and crawling policy enforcement."""

import logging
import threading
import time
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional
from urllib.parse import urlparse
from urllib.robotparser import RobotFileParser

logger = logging.getLogger(__name__)


@dataclass
class CrawlPolicy:
    """Crawl policy for a domain."""

    allowed: bool
    crawl_delay: float  # seconds
    reason: Optional[str] = None


class ComplianceManager:
    """Manage crawling compliance (robots.txt, rate limits, deny lists)."""

    # Domains to never crawl (login, SSO, social, etc.)
    DENY_LIST = {
        # Auth/SSO
        "login.microsoftonline.com",
        "accounts.google.com",
        "auth.okta.com",
        "sso.",  # Prefix match
        "login.",
        "signin.",
        "account.",
        # Social/link aggregators
        "linktr.ee",
        "linktree.com",
        "beacons.ai",
        "twitter.com",
        "facebook.com",
        "linkedin.com",
        "instagram.com",
        # Parking/placeholders
        "godaddy.com",
        "sedo.com",
        "afternic.com",
        "dan.com",
        "parkingcrew.net",
        # Generic
        "example.com",
        "example.org",
    }

    # Paths to never crawl
    DENY_PATHS = {
        "/login",
        "/signin",
        "/auth",
        "/account",
        "/dashboard",
        "/admin",
        "/wp-admin",
        "/checkout",
        "/cart",
        "/api/",
    }

    def __init__(
        self,
        user_agent: str = "FoundryMatchEnricher/1.0 (+https://foundrymatch.com/bot)",
        global_rate_limit: float = 0.1,  # 10 req/sec
        per_domain_rate_limit: float = 2.0,  # 1 req per 2 sec per domain
        max_concurrent_per_domain: int = 2,
    ):
        self.user_agent = user_agent
        self.global_rate_limit = global_rate_limit
        self.per_domain_rate_limit = per_domain_rate_limit
        self.max_concurrent_per_domain = max_concurrent_per_domain

        # Rate limiting state
        self.last_request_time = 0.0
        self.domain_last_request: defaultdict[str, float] = defaultdict(float)
        self.domain_active_requests: defaultdict[str, int] = defaultdict(int)

        # robots.txt cache (domain -> RobotFileParser)
        self.robots_cache: dict[str, RobotFileParser] = {}
        self._lock = threading.Lock()

    def check_allowed(self, url: str) -> CrawlPolicy:
        """
        Check if URL is allowed to be crawled.

        Args:
            url: Full URL to check

        Returns:
            CrawlPolicy with allowed status and reason
        """
        parsed = urlparse(url)
        domain = parsed.netloc.lower()
        path = parsed.path

        # Check deny list (domain)
        for denied in self.DENY_LIST:
            if denied.endswith("."):
                # Prefix match
                if domain.startswith(denied):
                    return CrawlPolicy(
                        allowed=False, crawl_delay=0, reason=f"denied_domain: {denied}"
                    )
            else:
                # Exact or suffix match
                if domain == denied or domain.endswith(f".{denied}"):
                    return CrawlPolicy(
                        allowed=False, crawl_delay=0, reason=f"denied_domain: {denied}"
                    )

        # Check deny list (path)
        for denied_path in self.DENY_PATHS:
            if path.startswith(denied_path):
                return CrawlPolicy(
                    allowed=False, crawl_delay=0, reason=f"denied_path: {denied_path}"
                )

        # Check robots.txt
        robots = self._get_robots_txt(domain)
        if robots and not robots.can_fetch(self.user_agent, url):
            return CrawlPolicy(
                allowed=False, crawl_delay=0, reason="robots.txt_disallow"
            )

        # Get crawl delay from robots.txt or use default
        crawl_delay = self.per_domain_rate_limit
        if robots:
            robots_delay = robots.crawl_delay(self.user_agent)
            if robots_delay:
                crawl_delay = max(crawl_delay, float(robots_delay))

        return CrawlPolicy(allowed=True, crawl_delay=crawl_delay)

    def acquire(self, url: str) -> bool:
        """
        Try to acquire permission to crawl URL (respects rate limits).

        Args:
            url: Full URL to crawl

        Returns:
            True if permission granted, False if should wait
        """
        parsed = urlparse(url)
        domain = parsed.netloc.lower()

        current_time = time.time()

        with self._lock:
            # Check global rate limit
            if current_time - self.last_request_time < self.global_rate_limit:
                return False

            # Check per-domain rate limit
            if (
                current_time - self.domain_last_request[domain]
                < self.per_domain_rate_limit
            ):
                return False

            # Check concurrent requests per domain
            if self.domain_active_requests[domain] >= self.max_concurrent_per_domain:
                return False

            # Grant permission
            self.last_request_time = current_time
            self.domain_last_request[domain] = current_time
            self.domain_active_requests[domain] += 1

        return True

    def release(self, url: str):
        """Release a crawl slot after request completes."""
        parsed = urlparse(url)
        domain = parsed.netloc.lower()
        with self._lock:
            self.domain_active_requests[domain] = max(
                0, self.domain_active_requests[domain] - 1
            )

    def wait_if_needed(self, url: str, max_wait: float = 5.0):
        """
        Wait until URL can be crawled (blocking).

        Args:
            url: URL to crawl
            max_wait: Maximum time to wait (seconds)

        Returns:
            True if acquired, False if timeout
        """
        start = time.time()
        while time.time() - start < max_wait:
            if self.acquire(url):
                return True
            time.sleep(0.1)
        return False

    def _get_robots_txt(self, domain: str) -> Optional[RobotFileParser]:
        """Fetch and cache robots.txt for domain."""
        with self._lock:
            if domain in self.robots_cache:
                return self.robots_cache[domain]

        try:
            robots = RobotFileParser()
            robots.set_url(f"https://{domain}/robots.txt")
            robots.read()
            with self._lock:
                self.robots_cache[domain] = robots
            return robots
        except Exception as e:
            logger.debug(f"Failed to fetch robots.txt for {domain}: {e}")
            # Cache None to avoid repeated failures
            with self._lock:
                self.robots_cache[domain] = None
            return None

    def get_headers(self) -> dict:
        """Get HTTP headers for requests."""
        return {
            "User-Agent": self.user_agent,
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9",
            "Accept-Encoding": "gzip, deflate",
            "DNT": "1",
            "Connection": "keep-alive",
            "Upgrade-Insecure-Requests": "1",
        }
