"""Website content scraping operator."""

import logging
import re
from dataclasses import dataclass
from typing import Iterable, Optional
from urllib.parse import urljoin, urlparse

import httpx
from bs4 import BeautifulSoup
from langdetect import LangDetectException, detect

from .compliance import ComplianceManager

logger = logging.getLogger(__name__)


@dataclass
class ContentResult:
    """Result of website content extraction."""

    success: bool
    title: Optional[str] = None
    description: Optional[str] = None  # Meta description
    h1_text: Optional[str] = None
    body_snippet: Optional[str] = None  # First ~500 chars of visible text
    language: Optional[str] = None
    is_english: Optional[bool] = None
    source_url: Optional[str] = None
    error: Optional[str] = None


class WebsiteContentOperator:
    """Extract content from a website homepage."""

    FALLBACK_PATHS: tuple[str, ...] = ("/", "/en/", "/index.html", "/home")

    def __init__(
        self,
        timeout: float = 10.0,
        max_content_length: int = 1_000_000,
        compliance: Optional[ComplianceManager] = None,
        compliance_wait_timeout: float = 5.0,
    ):
        self.timeout = timeout
        self.max_content_length = max_content_length
        self.compliance = compliance
        self.compliance_wait_timeout = compliance_wait_timeout

    def extract(self, url: str) -> ContentResult:
        """
        Extract content from website.

        Args:
            url: Full URL to scrape (e.g., 'https://example.com')

        Returns:
            ContentResult with extracted content
        """
        if not url:
            return ContentResult(success=False, error="empty_url")

        candidate_urls = list(self._candidate_urls(url))
        last_error: Optional[str] = None
        for candidate_url in candidate_urls:
            result = self._attempt_extract(candidate_url)
            if result.success:
                return result
            last_error = result.error

            # Do not continue if compliance denied or non-recoverable errors
            if last_error and last_error.startswith("compliance_denied"):
                break

        return ContentResult(success=False, error=last_error or "content_unavailable")

    def _attempt_extract(self, url: str) -> ContentResult:
        """Attempt to fetch and parse a single URL."""
        parsed = urlparse(url)
        acquired = False
        compliance_reason: Optional[str] = None

        if self.compliance:
            policy = self.compliance.check_allowed(url)
            if not policy.allowed:
                compliance_reason = policy.reason or "denied"
                return ContentResult(
                    success=False,
                    error=f"compliance_denied:{compliance_reason}",
                    source_url=url,
                )

            if not self.compliance.wait_if_needed(
                url, max_wait=self.compliance_wait_timeout
            ):
                compliance_reason = "rate_limited"
                return ContentResult(
                    success=False,
                    error="rate_limited",
                    source_url=url,
                )
            acquired = True

        headers = (
            self.compliance.get_headers()
            if self.compliance
            else {
                "User-Agent": "FoundryMatch/1.0 Bot",
                "Accept": "text/html,application/xhtml+xml",
            }
        )

        try:
            with httpx.Client(
                timeout=self.timeout,
                follow_redirects=True,
                verify=False,
            ) as client:
                response = client.get(url, headers=headers)

            if response.status_code != 200:
                status_error = f"status_{response.status_code}"
                return ContentResult(success=False, error=status_error, source_url=url)

            # Check content type
            content_type = response.headers.get("content-type", "").lower()
            if "text/html" not in content_type:
                return ContentResult(success=False, error="not_html", source_url=url)

            # Limit content length
            content = response.text[: self.max_content_length]

            parsed_result = self._parse_html(content)
            if not parsed_result.success:
                parsed_result.source_url = url
                return parsed_result

            # Detect language from aggregated text
            language, is_english = self._detect_language(parsed_result)

            parsed_result.source_url = url
            parsed_result.language = language
            parsed_result.is_english = is_english

            return parsed_result

        except httpx.TimeoutException:
            return ContentResult(success=False, error="timeout", source_url=url)
        except httpx.HTTPError as e:
            return ContentResult(
                success=False, error=f"http_error:{type(e).__name__}", source_url=url
            )
        except Exception as e:
            logger.warning(f"Unexpected error scraping {url}: {e}")
            return ContentResult(
                success=False,
                error=f"unexpected:{type(e).__name__}",
                source_url=url,
            )
        finally:
            if self.compliance and acquired:
                self.compliance.release(url)

    def _candidate_urls(self, url: str) -> Iterable[str]:
        """Generate candidate URLs to try for content extraction."""
        parsed = urlparse(url)
        base = f"{parsed.scheme}://{parsed.netloc}"

        paths: list[str] = []
        if parsed.path:
            paths.append(
                parsed.path if parsed.path.startswith("/") else f"/{parsed.path}"
            )

        for fallback in self.FALLBACK_PATHS:
            if fallback not in paths:
                paths.append(fallback)

        # Preserve ordering but deduplicate
        seen = set()
        for path in paths:
            candidate = urljoin(base, path)
            if candidate not in seen:
                seen.add(candidate)
                yield candidate

    def _parse_html(self, html: str) -> ContentResult:
        """Parse HTML and extract key content."""
        try:
            soup = BeautifulSoup(html, "html.parser")

            # Remove script and style elements
            for element in soup(["script", "style", "nav", "footer", "header"]):
                element.decompose()

            # Extract title
            title = None
            title_tag = soup.find("title")
            if title_tag:
                title = self._clean_text(title_tag.get_text())

            # Extract meta description
            description = None
            meta_desc = (
                soup.find("meta", attrs={"name": "description"})
                or soup.find("meta", attrs={"property": "og:description"})
                or soup.find("meta", attrs={"name": "twitter:description"})
            )
            if meta_desc:
                description = self._clean_text(meta_desc.get("content", ""))

            # Extract first H1
            h1_text = None
            h1_tag = soup.find("h1")
            if h1_tag:
                h1_text = self._clean_text(h1_tag.get_text())

            # Extract visible body text (first ~500 chars)
            body_text = soup.get_text(separator=" ", strip=True)
            body_snippet = self._clean_text(body_text[:500])

            return ContentResult(
                success=True,
                title=title,
                description=description,
                h1_text=h1_text,
                body_snippet=body_snippet,
            )

        except Exception as e:
            logger.warning(f"Error parsing HTML: {e}")
            return ContentResult(success=False, error=f"parse_error:{type(e).__name__}")

    def _clean_text(self, text: str) -> str:
        """Clean and normalize extracted text."""
        if not text:
            return ""
        # Remove extra whitespace
        text = re.sub(r"\s+", " ", text)
        return text.strip()

    def _detect_language(
        self, result: ContentResult
    ) -> tuple[Optional[str], Optional[bool]]:
        """Detect language from extracted content."""
        candidate_text_parts = [
            result.title or "",
            result.description or "",
            result.h1_text or "",
            result.body_snippet or "",
        ]
        combined = " ".join(part for part in candidate_text_parts if part).strip()

        if len(combined) < 20:
            return None, None

        try:
            language = detect(combined)
            return language, language == "en"
        except LangDetectException:
            return None, None
