# /// script
# dependencies = [
#   "packaging == 26.0",
#   "pyreqwest == 0.11.0",
#   "rich == 14.3.3",
# ]
# ///

# Until https://github.com/astral-sh/uv/issues/6794
# Generated by codex

import re
from pathlib import Path
from typing import NamedTuple

from packaging.version import InvalidVersion, Version
from pyreqwest.exceptions import JSONDecodeError, RequestError
from pyreqwest.simple.sync_request import pyreqwest_get
from rich.console import Console

ROOT = Path(__file__).resolve().parent.parent
PYPROJECT = ROOT / "pyproject.toml"


class Update(NamedTuple):
    package: str
    old: str
    new: str
    location: str


SECTION_RE = re.compile(r"^\s*\[([^]]+)]\s*$")
BUILD_REQUIRES_RE = re.compile(r"^(\s*)requires(\s*=\s*)\[(.*)$")
GROUP_LIST_RE = re.compile(r"^(\s*)([A-Za-z0-9_-]+)(\s*=\s*)\[(.*)$")
QUOTED_RE = re.compile(r"([\"'])([^\"']*)(\1)")
PINNED_RE = re.compile(
    r"^(\s*)([A-Za-z0-9][A-Za-z0-9._-]*)(\s*==\s*)([^\s\"'#,;]+)(\s*)$",
)


def fetch_latest_version(package: str, cache: dict[str, str | None]) -> str | None:
    key = package.lower()
    if key in cache:
        return cache[key]
    url = f"https://pypi.org/pypi/{package}/json"
    try:
        response = pyreqwest_get(url).error_for_status().send()
        data = response.json()
        version = data.get("info", {}).get("version")
    except (RequestError, JSONDecodeError, TypeError, AttributeError):
        version = None
    if isinstance(version, str):
        cache[key] = version
    else:
        cache[key] = None
    return cache[key]


def maybe_bump_dependency(
    dep: str,
    location: str,
    cache: dict[str, str | None],
) -> tuple[str, Update | None]:
    match = PINNED_RE.fullmatch(dep)
    if not match:
        return dep, None
    lead, package, eq, current_version, trail = match.groups()
    latest_version = fetch_latest_version(package, cache)
    if latest_version is None:
        return dep, None
    try:
        current = Version(current_version)
        latest = Version(latest_version)
    except InvalidVersion:
        return dep, None
    if latest <= current:
        return dep, None
    new_dep = f"{lead}{package}{eq}{latest_version}{trail}"
    return new_dep, Update(package, current_version, latest_version, location)


def bump_line(
    line: str,
    location: str,
    cache: dict[str, str | None],
) -> tuple[str, list[Update]]:
    updates: list[Update] = []
    parts: list[str] = []
    last_index = 0
    for quoted in QUOTED_RE.finditer(line):
        start, end = quoted.span()
        parts.append(line[last_index:start])
        quote = quoted.group(1)
        value = quoted.group(2)
        new_value, update = maybe_bump_dependency(value, location, cache)
        if update is not None:
            updates.append(update)
        parts.append(f"{quote}{new_value}{quote}")
        last_index = end
    parts.append(line[last_index:])
    return "".join(parts), updates


def render_updates(updates: list[Update]) -> None:
    console = Console()
    if not updates:
        console.print("[yellow]No dependency updates found.[/yellow]")
        return
    package_width = max(len(update.package) for update in updates)
    old_width = max(len(update.old) for update in updates)
    new_width = max(len(update.new) for update in updates)
    for update in updates:
        package = f"{update.package:<{package_width}}"
        old_version = f"{update.old:>{old_width}}"
        new_version = f"{update.new:<{new_width}}"
        console.print(
            f"[cyan]{package}[/cyan] "
            f"[red]{old_version}[/red] -> [green]{new_version}[/green] "
            f"| [magenta]{update.location}[/magenta]",
        )


def main() -> None:
    text = PYPROJECT.read_text(encoding="utf-8")
    lines = text.splitlines(keepends=True)

    current_section = ""
    inside_target_list = False
    current_location = ""
    cache: dict[str, str | None] = {}
    updates: list[Update] = []
    out_lines: list[str] = []

    for line in lines:
        section_match = SECTION_RE.match(line)
        if section_match:
            current_section = section_match.group(1).strip()
            inside_target_list = False
            current_location = ""
            out_lines.append(line)
            continue

        if not inside_target_list:
            current_location = detect_target_location(current_section, line)
            inside_target_list = bool(current_location)

        if not inside_target_list:
            out_lines.append(line)
            continue

        new_line, line_updates = bump_line(line, current_location, cache)
        out_lines.append(new_line)
        updates.extend(line_updates)
        if "]" in line:
            inside_target_list = False
            current_location = ""

    new_text = "".join(out_lines)
    if new_text != text:
        PYPROJECT.write_text(new_text, encoding="utf-8")

    render_updates(updates)


def detect_target_location(section: str, line: str) -> str:
    if section == "build-system" and BUILD_REQUIRES_RE.match(line):
        return "build-system.requires"
    if section == "dependency-groups":
        group_match = GROUP_LIST_RE.match(line)
        if group_match:
            return f"dependency-groups.{group_match.group(2)}"
    return ""


if __name__ == "__main__":
    main()
