#!/usr/bin/env python3
from __future__ import annotations

import argparse
import ast
import json
import re
import subprocess
import sys
from pathlib import Path


def parse_setup_py(path: Path) -> dict[str, str | None]:
    tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
    setup_call = None
    for node in ast.walk(tree):
        if isinstance(node, ast.Call):
            func = node.func
            if isinstance(func, ast.Name) and func.id == "setup":
                setup_call = node
                break
            if isinstance(func, ast.Attribute) and func.attr == "setup":
                setup_call = node
                break
    if setup_call is None:
        raise ValueError(f"No setup() call found in {path}")

    metadata: dict[str, str | None] = {
        "name": None,
        "version": None,
        "python_requires": None,
    }
    for keyword in setup_call.keywords:
        if keyword.arg not in metadata:
            continue
        value = keyword.value
        if isinstance(value, ast.Constant) and isinstance(value.value, str):
            metadata[keyword.arg] = value.value
    return metadata


def fallback_setup_metadata(path: Path) -> dict[str, str | None]:
    text = path.read_text(encoding="utf-8")
    metadata: dict[str, str | None] = {
        "name": None,
        "version": None,
        "python_requires": None,
    }

    for key, pattern in {
        "name": r"""name\s*=\s*["']([^"']+)["']""",
        "version": r"""version\s*=\s*["']([^"']+)["']""",
        "python_requires": r"""python_requires\s*=\s*["']([^"']+)["']""",
    }.items():
        match = re.search(pattern, text)
        if match:
            metadata[key] = match.group(1)

    if metadata["name"] is None or metadata["version"] is None:
        for flag, key in [("--name", "name"), ("--version", "version")]:
            if metadata[key] is not None:
                continue
            result = subprocess.run(
                [sys.executable, str(path), flag],
                text=True,
                capture_output=True,
                check=False,
            )
            if result.returncode == 0 and result.stdout.strip():
                metadata[key] = result.stdout.strip().splitlines()[-1]

    return metadata


def parse_pyproject(path: Path) -> dict[str, str | None]:
    try:
        import tomllib
    except ModuleNotFoundError as exc:  # pragma: no cover
        raise RuntimeError("Python 3.11+ is required to parse pyproject.toml") from exc

    data = tomllib.loads(path.read_text(encoding="utf-8"))
    project = data.get("project", {})
    return {
        "name": project.get("name"),
        "version": project.get("version"),
        "python_requires": project.get("requires-python"),
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--setup-py", default="setup.py")
    parser.add_argument("--pyproject", default="pyproject.toml")
    args = parser.parse_args()

    setup_py = Path(args.setup_py)
    pyproject = Path(args.pyproject)

    if pyproject.exists():
        metadata = parse_pyproject(pyproject)
        if (not metadata.get("name") or not metadata.get("version")) and setup_py.exists():
            metadata = parse_setup_py(setup_py)
            if not metadata.get("name") or not metadata.get("version"):
                metadata = fallback_setup_metadata(setup_py)
    elif setup_py.exists():
        metadata = parse_setup_py(setup_py)
        if not metadata.get("name") or not metadata.get("version"):
            metadata = fallback_setup_metadata(setup_py)
    else:
        raise FileNotFoundError("Neither setup.py nor pyproject.toml was found")

    if not metadata.get("name") or not metadata.get("version"):
        raise ValueError(f"Incomplete package metadata: {metadata}")

    json.dump(metadata, sys.stdout, indent=2, sort_keys=True)
    sys.stdout.write("\n")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
