"""Abstract base class for HTML fetchers and content analyzer."""

from __future__ import annotations

import re
from abc import ABC, abstractmethod
from typing import ClassVar

from pydantic import BaseModel

from yosoi.models.results import ContentMetadata, FetchResult


class JSDetectionResult(BaseModel, frozen=True):
    """Result of JavaScript framework detection."""

    requires_js: bool
    framework: str | None


class ContentAnalyzer:
    """Analyzes fetched content to detect special cases."""

    @staticmethod
    def analyze(html: str) -> ContentMetadata:
        """Analyze HTML content and return metadata.

        Args:
            html: The HTML of the URL

        Returns:
            The metadata of the HTML from the URL

        """
        metadata = ContentMetadata()
        metadata.content_length = len(html)

        # Convert to lowercase for case-insensitive checks
        html_lower = html.lower()

        # 1. Check if it's RSS/XML
        metadata.is_rss = ContentAnalyzer._detect_rss(html_lower)
        if metadata.is_rss:
            metadata.content_type = 'rss'
            return metadata

        # 2. Detect JavaScript-heavy sites
        js_data = ContentAnalyzer._detect_javascript_heavy(html_lower)
        metadata.requires_js = bool(js_data.requires_js)
        metadata.js_framework = str(js_data.framework) if js_data.framework is not None else None

        return metadata

    @staticmethod
    def _detect_rss(html_lower: str) -> bool:
        """Detect if content is an RSS/Atom feed.

        Args:
            html_lower: The HTML of the URL in all lowercase

        Returns:
            True if the HTML has RSS indicators

        """
        rss_indicators = [
            '<?xml',
            '<rss',
            '<feed',
            '<channel>',
            'xmlns="http://www.w3.org/2005/atom"',
            'xmlns="http://purl.org/rss/1.0/"',
        ]

        # Check first 500 chars (RSS declarations are at the top)
        start = html_lower[:500]

        return any(indicator in start for indicator in rss_indicators)

    @staticmethod
    def _detect_javascript_heavy(html_lower: str) -> JSDetectionResult:
        """Detect JavaScript-heavy sites that need browser rendering.

        Args:
            html_lower: The HTML of the URL in all lowercase

        Returns:
            A dict of if it has JS and if so what framework of JS

        These sites won't work with simple HTML fetching because:
        - Content is rendered client-side
        - No meaningful HTML in initial response
        - Need Playwright/Selenium

        """
        # 1. Check for JS framework signatures
        frameworks = {
            'react': [
                'id="root"',
                'id="app"',
                'data-reactroot',
                'react-root',
                '__react',
            ],
            'vue': [
                'id="app"',
                'v-if=',
                'v-for=',
                'vue.js',
                '__vue',
            ],
            'angular': [
                'ng-app',
                'ng-controller',
                'angular.js',
                'ng-version',
            ],
            'next': [
                '__next',
                '_next/static',
                'next.js',
            ],
            'svelte': [
                'svelte',
                '__svelte',
            ],
        }

        detected_framework = None
        for framework, indicators in frameworks.items():
            if any(indicator in html_lower for indicator in indicators):
                detected_framework = framework
                break

        # 2. Check for minimal content (sign of client-side rendering)
        body_match = re.search(r'<body[^>]*>(.*?)</body>', html_lower, re.DOTALL)
        if body_match:
            body_content = body_match.group(1)
            # Remove scripts, styles, and whitespace
            body_content = re.sub(r'<script[^>]*>.*?</script>', '', body_content, flags=re.DOTALL)
            body_content = re.sub(r'<style[^>]*>.*?</style>', '', body_content, flags=re.DOTALL)
            body_content = body_content.strip()

            # If body has < 100 chars of actual content, it's probably JS-rendered
            minimal_content = len(body_content) < 100
        else:
            minimal_content = False

        # 3. Check for "noscript" warnings
        has_noscript_warning = '<noscript>' in html_lower and (
            'enable javascript' in html_lower or 'requires javascript' in html_lower
        )

        # Determine if JS is required
        requires_js = (detected_framework is not None and minimal_content) or has_noscript_warning

        return JSDetectionResult(requires_js=requires_js, framework=detected_framework)


class HTMLFetcher(ABC):
    """Abstract base class for HTML fetchers.

    Implement this interface to create custom HTML fetchers.
    """

    async def close(self) -> None:  # noqa: B027
        """Release any held resources (e.g. HTTP client connections).

        Override in subclasses that hold persistent connections.
        """

    async def __aenter__(self) -> HTMLFetcher:
        """Enter async context manager."""
        return self

    async def __aexit__(
        self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: object
    ) -> None:
        """Exit async context manager and close resources."""
        await self.close()

    @abstractmethod
    async def fetch(self, url: str) -> FetchResult:
        """Fetch HTML from a URL.

        Args:
            url: URL to fetch

        Returns:
            FetchResult with HTML, metadata, and status

        Raises:
            BotDetectionError: If bot detection is triggered

        """
        pass

    # Body patterns checked on every response
    _BOT_BODY_PATTERNS: ClassVar[dict[str, str]] = {
        'challenge-platform': 'Cloudflare challenge platform',
        'cf-browser-verification': 'Cloudflare browser verification',
        '__cf_chl_jschl_tk__': 'Cloudflare JS challenge token',
        'cf-captcha-container': 'Cloudflare CAPTCHA container',
        'just a moment...': 'Cloudflare "Just a moment" page',
        'checking your browser': 'Cloudflare browser check',
        'attention required! | cloudflare': 'Cloudflare attention page',
        'g-recaptcha': 'Google reCAPTCHA',
        'h-captcha': 'hCaptcha',
        'cf-turnstile': 'Cloudflare Turnstile',
        'access denied</title>': 'Access denied page',
        'you have been blocked': 'Explicit block message',
    }

    # Additional body patterns only checked on >=400 responses
    _BOT_BODY_PATTERNS_4XX: ClassVar[dict[str, str]] = {
        'captcha': 'CAPTCHA required',
        'rate limit': 'Rate limited',
        'too many requests': 'Too many requests',
        'forbidden': 'Forbidden',
    }

    def _enrich_with_header_context(self, found: list[str], headers_lower: dict[str, str]) -> list[str]:
        """Append Cloudflare/rate-limit header details to an indicator list."""
        if retry_after := headers_lower.get('retry-after'):
            found.append(f'Retry-After: {retry_after}')
        if 'cloudflare' in headers_lower.get('server', '').lower():
            found.append('Cloudflare server')
        if cf_ray := headers_lower.get('cf-ray'):
            found.append(f'CF-Ray: {cf_ray}')
        return found

    def _scan_body_patterns(self, html_check: str, patterns: dict[str, str], found: list[str]) -> None:
        """Append matching pattern messages to *found* in-place."""
        for indicator, message in patterns.items():
            if indicator in html_check and message not in found:
                found.append(message)

    def _check_for_bot_detection(
        self, html: str, status_code: int, headers: dict[str, str] | None = None
    ) -> tuple[bool, list[str]]:
        """Check if HTML indicates bot detection.

        Args:
            html: The HTML of the URL
            status_code: The status code of the URL returned
            headers: Optional response headers for additional signal detection

        Returns:
            Tuple of (is_blocked, indicators) where is_blocked is True if bot
            detection triggered, and indicators is a list of detection reasons.
            Returns (False, []) if no blocking detected.

        """
        if not html or len(html) < 100:
            return True, ['HTML too short']

        headers_lower: dict[str, str] = {k.lower(): v for k, v in (headers or {}).items()}

        # Hard-block status codes — enrich with header context and return immediately
        if status_code in [403, 429, 503]:
            return True, self._enrich_with_header_context([f'HTTP {status_code}'], headers_lower)

        # cf-mitigated is a definitive Cloudflare block signal on any status code
        if 'cf-mitigated' in headers_lower:
            return True, self._enrich_with_header_context(['Cloudflare mitigation active'], headers_lower)

        found: list[str] = []
        if 'retry-after' in headers_lower and status_code >= 400:
            found.append('Rate limited (Retry-After header)')

        html_check = html[:3000].lower()
        self._scan_body_patterns(html_check, self._BOT_BODY_PATTERNS, found)

        if status_code == 200:
            return bool(found), found

        if status_code >= 400:
            self._scan_body_patterns(html_check, self._BOT_BODY_PATTERNS_4XX, found)
            return bool(found), found

        return False, []
