"""Async httpx web scraper with retries, rotating user agents, and fallback."""
from __future__ import annotations
import logging
import random
import re
from typing import Optional

import httpx
from bs4 import BeautifulSoup

from models import ScrapeResult

logger = logging.getLogger(__name__)

USER_AGENTS = [
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36",
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0",
]

REMOVE_TAGS = {"script", "style", "nav", "footer", "header", "noscript", "iframe", "svg"}


class AsyncScraper:
    """Asynchronous web scraper with retries, rotating UAs, and fallback HTTP clients."""

    def __init__(self, timeout_ms: int = 20_000):
        self.timeout = timeout_ms / 1000.0

    async def scrape(self, url: str) -> ScrapeResult:
        """Scrape a URL with retries and fallbacks."""
        errors = []

        # Strategy 1: Standard httpx client
        for attempt in range(3):
            try:
                result = await self._try_scrape(url, attempt)
                if result.text_content and len(result.text_content) > 50:
                    return result
                if result.error:
                    errors.append(f"Attempt {attempt+1}: {result.error}")
            except Exception as e:
                errors.append(f"Attempt {attempt+1}: {str(e)}")

        # Strategy 2: Minimal headers (like curl)
        for attempt in range(2):
            try:
                result = await self._try_minimal(url)
                if result.text_content and len(result.text_content) > 50:
                    return result
                if result.error:
                    errors.append(f"Minimal {attempt+1}: {result.error}")
            except Exception as e:
                errors.append(f"Minimal {attempt+1}: {str(e)}")

        return ScrapeResult(url=url, title="", text_content="", text_length=0,
                           error="; ".join(errors[-3:]) or "All scrape attempts failed")

    async def _try_scrape(self, url: str, attempt: int) -> ScrapeResult:
        ua = USER_AGENTS[attempt % len(USER_AGENTS)]
        headers = {
            "User-Agent": ua,
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
            "Accept-Language": "en-US,en;q=0.9,nl;q=0.8",
            "Accept-Encoding": "gzip, deflate, br",
            "DNT": "1",
            "Connection": "keep-alive",
            "Upgrade-Insecure-Requests": "1",
            "Sec-Fetch-Dest": "document",
            "Sec-Fetch-Mode": "navigate",
            "Sec-Fetch-Site": "none",
            "Sec-Fetch-User": "?1",
            "Cache-Control": "max-age=0",
        }
        async with httpx.AsyncClient(
            timeout=self.timeout,
            follow_redirects=True,
            headers=headers,
            http2=True,
        ) as client:
            response = await client.get(url)
            return self._parse_response(response, url)

    async def _try_minimal(self, url: str) -> ScrapeResult:
        headers = {
            "User-Agent": "curl/8.4.0",
            "Accept": "*/*",
        }
        async with httpx.AsyncClient(
            timeout=self.timeout,
            follow_redirects=True,
            headers=headers,
            http2=False,
        ) as client:
            response = await client.get(url)
            return self._parse_response(response, url)

    def _parse_response(self, response, url: str) -> ScrapeResult:
        result = ScrapeResult(url=url, title="", text_content="", text_length=0)
        result.status_code = response.status_code

        if response.status_code >= 500:
            result.error = f"HTTP {response.status_code}"
            return result
        if response.status_code >= 400 and response.status_code < 500:
            if response.status_code in (403, 429):
                result.error = f"HTTP {response.status_code} (blocked/rate-limited)"
            else:
                result.error = f"HTTP {response.status_code}"
            return result

        try:
            html = response.text
        except Exception:
            result.error = "Could not decode response"
            return result

        if not html or len(html) < 100:
            result.error = "Empty response"
            return result

        soup = BeautifulSoup(html, "html.parser")
        result.title = soup.title.string.strip() if soup.title else ""

        # Remove non-content elements
        for tag in soup(REMOVE_TAGS):
            tag.decompose()

        body = soup.find("body")
        text = body.get_text(separator="\n") if body else soup.get_text(separator="\n")

        # Also extract text from JSON-LD structured data
        for script in soup.find_all("script", type="application/ld+json"):
            try:
                import json
                ld = json.loads(script.string or "")
                if isinstance(ld, dict):
                    desc = ld.get("description", "")
                    if desc:
                        text += "\n" + desc
            except Exception:
                pass

        result.text_content = self._clean_text(text)
        result.text_length = len(result.text_content)
        return result

    @staticmethod
    def _clean_text(text: str) -> str:
        if not text:
            return ""
        text = re.sub(r"[ \t]+", " ", text)
        text = re.sub(r"\n{3,}", "\n\n", text)
        lines = [ln.strip() for ln in text.split("\n")]
        lines = [ln for ln in lines if len(ln) > 2]
        return "\n".join(lines).strip()
