#!/usr/bin/env -S abxpkg run --script --deps-from=./config.json:required_binaries python3
# /// script
# requires-python = ">=3.12"
# ///
# ruff: noqa: E402
#
# Submit a URL to archive.org for archiving and save the resulting archive.org link.
#
# Usage:
#     ./on_Snapshot__08_archivedotorg.finite.bg.py --url=<url> > events.jsonl

import signal

# Snapshot cleanup sends SIGTERM to the whole hook process group as the polite
# shutdown signal before the hard SIGKILL deadline. This hook is a finite
# downloader, so treating SIGTERM as "stop now" corrupts the normal contract:
# an in-flight download becomes a failed ArchiveResult even though cleanup would
# have allowed it to finish within the hook timeout. Installing SIG_IGN before
# any heavy imports or network work makes cleanup mean "finish promptly if you
# can"; the later SIGKILL deadline still enforces the hard stop.
signal.signal(signal.SIGTERM, signal.SIG_IGN)

import os
import sys
from http.client import RemoteDisconnected
from ipaddress import ip_address
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen

from abx_plugins.plugins.base.utils import emit_archive_result_record, load_config

import rich_click as click


# Extractor metadata
PLUGIN_NAME = "archivedotorg"
PLUGIN_DIR = Path(__file__).resolve().parent.name
CONFIG = load_config()
SNAP_DIR = Path(CONFIG.SNAP_DIR or ".").resolve()
OUTPUT_DIR = SNAP_DIR / PLUGIN_DIR
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
os.chdir(OUTPUT_DIR)
OUTPUT_FILE = "archive.org.txt"


def should_skip_archivedotorg_url(url: str) -> str:
    parsed = urlparse(url)
    hostname = (parsed.hostname or "").strip().lower()
    if not hostname:
        return "URL has no hostname"
    if (
        hostname == "localhost"
        or hostname.endswith(".localhost")
        or hostname.endswith(".local")
    ):
        return "URL is local/private"
    try:
        address = ip_address(hostname)
    except ValueError:
        return ""
    if (
        address.is_loopback
        or address.is_private
        or address.is_link_local
        or address.is_multicast
        or address.is_unspecified
        or address.is_reserved
    ):
        return "URL is local/private"
    return ""


def submit_to_archivedotorg(url: str) -> tuple[bool, str | None, str]:
    """
    Submit URL to archive.org Wayback Machine.

    Returns: (success, output_path, error_message)
    """

    def log(message: str) -> None:
        print(f"[archivedotorg] {message}", file=sys.stderr)

    config = load_config()
    timeout = config.ARCHIVEDOTORG_TIMEOUT
    library_version = os.environ.get("LIBRARY_VERSION", "0.0.1")
    user_agent = (
        f"ArchiveBox/{library_version} (+https://github.com/ArchiveBox/ArchiveBox/)"
    )

    submit_url = f"https://web.archive.org/save/{url}"
    log(f"Submitting to Wayback Machine (timeout={timeout}s)")
    log(f"GET {submit_url}")

    def save_submit_url_for_manual_retry(reason: str) -> tuple[bool, str, str]:
        # Wayback save availability is outside ArchiveBox's control and commonly
        # rate-limits shared CI/datacenter egress. Preserve a user-visible output
        # that can be retried manually instead of turning an optional remote
        # submission outage into a failed snapshot extraction.
        Path(OUTPUT_FILE).write_text(submit_url, encoding="utf-8")
        log(f"{reason}, saved submit URL for manual retry")
        return True, OUTPUT_FILE, ""

    try:
        print("submitting to archive.org...", file=sys.stderr)
        req = Request(submit_url, headers={"User-Agent": user_agent})
        response = urlopen(req, timeout=timeout)
        final_url = response.url
        status = response.status
        headers = response.headers
        body = response.read().decode("utf-8", errors="replace")
        log(f"HTTP {status} final_url={final_url}")

        # Check for successful archive
        content_location = (
            headers["Content-Location"] if "Content-Location" in headers else ""
        )
        x_archive_orig_url = (
            headers["X-Archive-Orig-Url"] if "X-Archive-Orig-Url" in headers else ""
        )
        if content_location:
            log(f"Content-Location: {content_location}")
        if x_archive_orig_url:
            log(f"X-Archive-Orig-Url: {x_archive_orig_url}")

        # Build archive URL
        if content_location:
            archive_url = f"https://web.archive.org{content_location}"
            Path(OUTPUT_FILE).write_text(archive_url, encoding="utf-8")
            log(f"Saved archive URL -> {archive_url}")
            return True, OUTPUT_FILE, ""
        elif "web.archive.org" in final_url:
            # We were redirected to an archive page
            Path(OUTPUT_FILE).write_text(final_url, encoding="utf-8")
            log(f"Redirected to archive page -> {final_url}")
            return True, OUTPUT_FILE, ""
        else:
            # Check for errors in response
            if "RobotAccessControlException" in body:
                return save_submit_url_for_manual_retry("Blocked by robots.txt")
            else:
                return save_submit_url_for_manual_retry("No archive URL returned")

    except HTTPError as e:
        if e.code == 429:
            return save_submit_url_for_manual_retry("Rate limited by archive.org")
        if e.code >= 400:
            return False, None, f"HTTP {e.code}"
        return False, None, f"HTTPError: {e}"
    except TimeoutError:
        return save_submit_url_for_manual_retry(
            f"Request timed out after {timeout} seconds",
        )
    except RemoteDisconnected as e:
        return save_submit_url_for_manual_retry(f"RemoteDisconnected: {e}")
    except URLError as e:
        reason = e.reason
        reason_text = str(reason)
        retryable = (
            isinstance(reason, ConnectionResetError)
            or "timed out" in reason_text.lower()
        )
        if retryable:
            return save_submit_url_for_manual_retry(f"URLError: {reason}")
        return False, None, f"URLError: {reason}"
    except Exception as e:
        return False, None, f"{type(e).__name__}: {e}"


@click.command(
    context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
)
@click.option("--url", required=True, help="URL to submit to archive.org")
def main(url: str):
    """Submit a URL to archive.org for archiving."""

    config = load_config()

    # Check if feature is enabled
    if not config.ARCHIVEDOTORG_ENABLED:
        print(
            "Skipping archive.org submission (ARCHIVEDOTORG_ENABLED=False)",
            file=sys.stderr,
        )
        emit_archive_result_record("skipped", "ARCHIVEDOTORG_ENABLED=False")
        sys.exit(0)

    try:
        skip_reason = should_skip_archivedotorg_url(url)
        if skip_reason:
            emit_archive_result_record("skipped", skip_reason)
            sys.exit(0)

        # Run extraction
        success, output, error = submit_to_archivedotorg(url)

        if success:
            # Success - emit ArchiveResult with output file
            emit_archive_result_record(
                "succeeded",
                f"{PLUGIN_DIR}/{OUTPUT_FILE}" if output else "",
            )
            sys.exit(0)
        else:
            print(f"NORESULTS: {error}", file=sys.stderr)
            emit_archive_result_record("noresults", error or "")
            sys.exit(0)

    except Exception as e:
        error = f"{type(e).__name__}: {e}"
        print(f"ERROR: {error}", file=sys.stderr)
        emit_archive_result_record("failed", error)
        sys.exit(1)


if __name__ == "__main__":
    main()
