import ast
import sys
from configparser import ConfigParser
from pathlib import Path
from typing import Any, Optional

import tomli

if sys.version_info < (3, 8):
    Constant = ast.Str

    def get_constant(x: ast.Str) -> str:
        return x.s


else:
    Constant = ast.Constant

    def get_constant(x: ast.Constant) -> Any:
        return x.value


class Analyzer(ast.NodeVisitor):
    def __init__(self) -> None:
        self.requires_python: Optional[str] = None

    def visit(self, content: ast.AST) -> None:
        for node in ast.walk(content):
            for child in ast.iter_child_nodes(node):
                child.parent = node  # type: ignore[attr-defined]
        super().visit(content)

    def visit_keyword(self, node: ast.keyword) -> None:
        self.generic_visit(node)
        if node.arg == "python_requires":
            # Must not be nested in an if or other structure
            # This will be Module -> Expr -> Call -> keyword
            if not hasattr(node.parent.parent.parent, "parent") and isinstance(  # type: ignore[attr-defined]
                node.value, Constant
            ):
                self.requires_python = get_constant(node.value)


def setup_py_python_requires(content: str) -> Optional[str]:
    try:
        tree = ast.parse(content)
        analyzer = Analyzer()
        analyzer.visit(tree)
        return analyzer.requires_python or None
    except Exception:
        return None


def get_requires_python_str(package_dir: Path) -> Optional[str]:
    "Return the python requires string from the most canonical source available, or None"

    # Read in from pyproject.toml:project.requires-python
    try:
        with (package_dir / "pyproject.toml").open("rb") as f1:
            info = tomli.load(f1)
        return str(info["project"]["requires-python"])
    except (FileNotFoundError, KeyError, IndexError, TypeError):
        pass

    # Read in from setup.cfg:options.python_requires
    try:
        config = ConfigParser()
        config.read(package_dir / "setup.cfg")
        return str(config["options"]["python_requires"])
    except (FileNotFoundError, KeyError, IndexError, TypeError):
        pass

    try:
        with (package_dir / "setup.py").open() as f2:
            return setup_py_python_requires(f2.read())
    except FileNotFoundError:
        pass

    return None
