"""Core broken link crawling logic."""

import asyncio
import aiohttp
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional, Tuple
from urllib.parse import urljoin, urlparse

from bs4 import BeautifulSoup

REQUEST_TIMEOUT = 30
USER_AGENT = "Mozilla/5.0 (compatible; BrokenLinkChecker/1.0)"


@dataclass
class BrokenLink:
    source_url: str
    broken_url: str
    anchor_text: str
    surrounding_context: str
    status_code: int
    error_message: str
    found_at: str
    source_label: str = ""
    contact_emails: List[str] = field(default_factory=list)


@dataclass
class CrawlResult:
    url: str
    label: str
    total_links: int
    broken_links: List[BrokenLink]
    crawl_time: float
    contact_emails: List[str] = field(default_factory=list)


class BrokenLinkFinder:
    def __init__(self, delay: float = 1.0, timeout: int = REQUEST_TIMEOUT):
        self.delay = delay
        self.timeout = timeout
        self.session: Optional[aiohttp.ClientSession] = None

    async def __aenter__(self):
        timeout = aiohttp.ClientTimeout(total=self.timeout)
        self.session = aiohttp.ClientSession(
            timeout=timeout,
            headers={"User-Agent": USER_AGENT},
        )
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        if self.session:
            await self.session.close()

    async def fetch_page(self, url: str) -> Tuple[Optional[str], int, str]:
        """Fetch a page and return (html, status_code, error_message)."""
        try:
            async with self.session.get(url, allow_redirects=True) as response:
                if response.status == 200:
                    html = await response.text()
                    return html, response.status, ""
                else:
                    return None, response.status, f"HTTP {response.status}"
        except asyncio.TimeoutError:
            return None, 0, "Timeout"
        except aiohttp.ClientError as e:
            return None, 0, str(e)
        except Exception as e:
            return None, 0, f"Error: {str(e)}"

    async def check_link(self, url: str) -> Tuple[int, str]:
        """Check if a link is broken. Returns (status_code, error_message)."""
        try:
            async with self.session.head(url, allow_redirects=True) as response:
                # Some servers don't support HEAD, fall back to GET for 405
                if response.status == 405:
                    async with self.session.get(url, allow_redirects=True) as get_resp:
                        return get_resp.status, ""
                return response.status, ""
        except asyncio.TimeoutError:
            return 0, "Timeout"
        except aiohttp.ClientError as e:
            return 0, str(e)
        except Exception as e:
            return 0, f"Error: {str(e)}"

    def extract_links(self, html: str, base_url: str) -> List[dict]:
        """Extract all outbound links from HTML with context."""
        soup = BeautifulSoup(html, "html.parser")
        links = []
        base_domain = urlparse(base_url).netloc

        for anchor in soup.find_all("a", href=True):
            href = anchor["href"]

            if href.startswith(("#", "javascript:", "mailto:", "tel:")):
                continue

            full_url = urljoin(base_url, href)
            parsed = urlparse(full_url)

            if parsed.scheme not in ("http", "https"):
                continue

            # Skip internal links (same domain)
            if parsed.netloc == base_domain:
                continue

            anchor_text = anchor.get_text(strip=True)[:200]
            parent = anchor.parent
            context = parent.get_text(strip=True)[:300] if parent else ""

            links.append(
                {"url": full_url, "anchor_text": anchor_text, "context": context}
            )

        return links

    async def crawl_page(
        self, url: str, label: str = "", contact_emails: Optional[List[str]] = None
    ) -> CrawlResult:
        """Crawl a single page and find broken outbound links."""
        start_time = time.time()
        broken_links = []

        html, status, error = await self.fetch_page(url)

        if not html:
            return CrawlResult(
                url=url,
                label=label,
                total_links=0,
                broken_links=[],
                crawl_time=time.time() - start_time,
            )

        links = self.extract_links(html, url)

        for i, link_data in enumerate(links):
            if i > 0:
                await asyncio.sleep(self.delay)

            status_code, error_msg = await self.check_link(link_data["url"])
            is_broken = status_code >= 400 or status_code == 0

            if is_broken:
                broken_link = BrokenLink(
                    source_url=url,
                    broken_url=link_data["url"],
                    anchor_text=link_data["anchor_text"],
                    surrounding_context=link_data["context"],
                    status_code=status_code,
                    error_message=error_msg,
                    found_at=datetime.now().isoformat(),
                    source_label=label,
                    contact_emails=contact_emails or [],
                )
                broken_links.append(broken_link)

        return CrawlResult(
            url=url,
            label=label,
            total_links=len(links),
            broken_links=broken_links,
            crawl_time=time.time() - start_time,
            contact_emails=contact_emails or [],
        )
