import argparse
import getpass
import ipaddress
import json
import logging
import os
import time
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import unquote, urljoin, urlsplit

from scrapling.fetchers import DynamicSession, FetcherSession

TARGET_URL = "https://news.ycombinator.com/"
IP_URL = "https://api.ipify.org?format=json"
PAGE_DELAY_SECONDS = 30
HTTP_TIMEOUT_SECONDS = 30
BROWSER_TIMEOUT_MS = 30000


class GuideError(Exception):
    pass


def read_proxy():
    raw = os.environ.get("PROXY_URL") or getpass.getpass("Proxy URL (hidden): ")
    try:
        parts = urlsplit(raw)
        valid = (parts.scheme in ("http", "https") and parts.hostname
                 and parts.port and parts.username and parts.password
                 and parts.path in ("", "/") and not parts.query and not parts.fragment)
    except ValueError:
        valid = False
    if not valid:
        raise GuideError("Use the complete HTTP proxy URL from the generator")
    return {"server": f"{parts.scheme}://{parts.hostname}:{parts.port}",
            "username": unquote(parts.username), "password": unquote(parts.password)}


def check_status(response):
    if response.status != 200:
        raise GuideError(f"HTTP {response.status}; stop and check the diagnostic table")


def extract_stories(response):
    check_status(response)
    rows = []
    for item in response.css("tr.athing"):
        story_id = item.attrib.get("id", "")
        title = item.css(".titleline > a::text").get()
        href = item.css(".titleline > a::attr(href)").get()
        if not story_id.isdigit() or not title or not href:
            raise GuideError("A story is missing its ID, title, or link")
        score = response.css(f"#score_{story_id}::text").get()
        points = int(score.split()[0]) if score else None
        url = urljoin(TARGET_URL, href)
        if urlsplit(url).scheme not in ("http", "https"):
            raise GuideError("A story link is not an HTTP or HTTPS URL")
        rows.append({"id": int(story_id), "title": title,
                     "url": url, "points": points})
    if not rows:
        raise GuideError("No stories found; inspect the response and selectors")
    return rows


def collect(session, mode, pages, use_proxy):
    def fetch(url):
        if mode == "http":
            response = session.get(url)
        else:
            response = session.fetch(url, google_search=False)
        check_status(response)
        return response

    exit_ip = None
    if use_proxy:
        response = fetch(IP_URL)
        payload = (response.json() if mode == "http"
                   else json.loads(response.css("body")[0].get_all_text()))
        exit_ip = str(ipaddress.ip_address(payload["ip"]))
    rows, seen = [], set()
    url = TARGET_URL
    for page_number in range(pages):
        if page_number:
            time.sleep(PAGE_DELAY_SECONDS)
        response = fetch(url)
        for row in extract_stories(response):
            if row["id"] not in seen:
                rows.append(row)
                seen.add(row["id"])
        if page_number + 1 < pages:
            next_path = response.css("a.morelink::attr(href)").get()
            if not next_path:
                raise GuideError("Next-page link missing; no complete run saved")
            url = urljoin(url, next_path)
            if urlsplit(url).netloc != urlsplit(TARGET_URL).netloc:
                raise GuideError("Next-page link leaves Hacker News; stopped")
    return {"captured_at": datetime.now(timezone.utc).isoformat(),
            "mode": mode, "pages": pages, "exit_ip": exit_ip, "stories": rows}


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("mode", choices=("http", "browser"))
    parser.add_argument("--pages", type=int, choices=(1, 2), default=1)
    parser.add_argument("--proxy", action="store_true")
    args = parser.parse_args()
    route = "proxy" if args.proxy else "direct"
    output = Path(f"hn-{args.mode}-{route}-{args.pages}.json")
    if output.exists():
        raise GuideError(f"Move {output.name} before running again")
    proxy = read_proxy() if args.proxy else None
    if args.mode == "http":
        settings = {"timeout": HTTP_TIMEOUT_SECONDS, "retries": 1}
        if proxy:
            settings.update(proxy=proxy["server"],
                            proxy_auth=(proxy["username"], proxy["password"]))
        with FetcherSession(**settings) as session:
            result = collect(session, args.mode, args.pages, args.proxy)
    else:
        settings = {"headless": True, "retries": 1, "timeout": BROWSER_TIMEOUT_MS}
        if proxy:
            settings["proxy"] = proxy
        with DynamicSession(**settings) as session:
            result = collect(session, args.mode, args.pages, args.proxy)
    with output.open("x", encoding="utf-8") as stream:
        json.dump(result, stream, ensure_ascii=False, indent=2)
    print(f"Saved {len(result['stories'])} unique stories to {output.name}")
    print(f"Pages: {result['pages']}; route: {'proxy' if args.proxy else 'direct'}")
    if result["exit_ip"]:
        print(f"Exit IP: {result['exit_ip']}")


if __name__ == "__main__":
    # Library errors can contain connection details; never print raw exceptions.
    logging.disable(logging.CRITICAL)
    try:
        main()
    except GuideError as error:
        raise SystemExit(str(error)) from None
    except (Exception, KeyboardInterrupt) as error:
        print(f"Stopped ({type(error).__name__}); no successful result reported.")
        raise SystemExit("Check proxy credentials, timeouts, and the diagnostic table.") from None
