from __future__ import annotations

import ipaddress
import socket
from urllib.parse import urljoin, urlparse

import httpx
from bs4 import BeautifulSoup

MAX_JOB_PAGE_BYTES = 2 * 1024 * 1024
MAX_REDIRECTS = 5


class JobPageError(ValueError):
    pass


def _validate_public_url(url: str) -> None:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"} or not parsed.hostname:
        raise JobPageError("Job URL must use http or https.")
    if parsed.username or parsed.password:
        raise JobPageError("Credentials are not allowed in job URLs.")

    default_port = 443 if parsed.scheme == "https" else 80
    try:
        addresses = socket.getaddrinfo(
            parsed.hostname, parsed.port or default_port, type=socket.SOCK_STREAM
        )
    except socket.gaierror as exc:
        raise JobPageError(f"Could not resolve job URL host: {parsed.hostname}") from exc

    for address in addresses:
        ip = ipaddress.ip_address(address[4][0])
        if not ip.is_global:
            raise JobPageError("Job URL resolves to a private or non-public address.")


def html_to_text(html: str) -> str:
    soup = BeautifulSoup(html, "html.parser")
    for element in soup(["script", "style", "noscript", "svg"]):
        element.decompose()
    text = "\n".join(line.strip() for line in soup.get_text("\n").splitlines() if line.strip())
    if not text:
        raise JobPageError("The job page did not contain readable text.")
    return text


def fetch_job_page(url: str) -> str:
    current = url
    headers = {"User-Agent": "ResumeAgent/0.1 (+local CLI)"}
    with httpx.Client(timeout=15, headers=headers, follow_redirects=False) as client:
        for _ in range(MAX_REDIRECTS + 1):
            _validate_public_url(current)
            with client.stream("GET", current) as response:
                if response.is_redirect:
                    location = response.headers.get("location")
                    if not location:
                        raise JobPageError("Job page returned an invalid redirect.")
                    current = urljoin(current, location)
                    continue
                response.raise_for_status()
                content_type = response.headers.get("content-type", "")
                if "text/html" not in content_type and "text/plain" not in content_type:
                    raise JobPageError("Job URL did not return HTML or plain text.")
                body = bytearray()
                for chunk in response.iter_bytes():
                    body.extend(chunk)
                    if len(body) > MAX_JOB_PAGE_BYTES:
                        raise JobPageError("Job page is larger than the 2 MB safety limit.")
                return html_to_text(body.decode(response.encoding or "utf-8", errors="replace"))
    raise JobPageError("Job URL redirected too many times.")
