import os
import re
import shlex
import shutil
import sys
from pathlib import Path
from subprocess import PIPE, STDOUT, run

ACTION_PATH = Path(os.environ["GITHUB_ACTION_PATH"])
ENV_PATH = ACTION_PATH / ".black-env"
ENV_BIN = ENV_PATH / ("Scripts" if sys.platform == "win32" else "bin")
OPTIONS = os.getenv("INPUT_OPTIONS", default="")
SRC = os.getenv("INPUT_SRC", default="")
JUPYTER = os.getenv("INPUT_JUPYTER") == "true"
BLACK_ARGS = os.getenv("INPUT_BLACK_ARGS", default="")
VERSION = os.getenv("INPUT_VERSION", default="")
USE_PYPROJECT = os.getenv("INPUT_USE_PYPROJECT") == "true"
OUTPUT_FILE = os.getenv("OUTPUT_FILE", default="")

BLACK_VERSION_RE = re.compile(
    r"^black((?:\s*(?:~=|==|!=|<=|>=|<|>|===)\s*[A-Za-z0-9*+._-]+)"
    r"(?:\s*,\s*(?:~=|==|!=|<=|>=|<|>|===)\s*[A-Za-z0-9*+._-]+)*)\s*$",
    re.IGNORECASE,
)
EXTRAS_RE = re.compile(r"\[.*\]")
EXPORT_SUBST_FAIL_RE = re.compile(r"\$Format:.*\$")


def determine_version_specifier() -> str:
    """Determine the version of Black to install.

    The version can be specified either via the `with.version` input or via the
    pyproject.toml file if `with.use_pyproject` is set to `true`.
    """
    if USE_PYPROJECT and VERSION:
        print(
            "::error::'with.version' and 'with.use_pyproject' inputs are "
            "mutually exclusive.",
            file=sys.stderr,
            flush=True,
        )
        sys.exit(1)
    if USE_PYPROJECT:
        return read_version_specifier_from_pyproject()
    elif VERSION and VERSION[0] in "0123456789":
        return f"=={VERSION}"
    else:
        return VERSION


def read_version_specifier_from_pyproject() -> str:
    if sys.version_info < (3, 11):
        print(
            "::error::'with.use_pyproject' input requires Python 3.11 or later.",
            file=sys.stderr,
            flush=True,
        )
        sys.exit(1)

    import tomllib  # type: ignore[import-not-found,unreachable]

    try:
        with Path("pyproject.toml").open("rb") as fp:
            pyproject = tomllib.load(fp)
    except FileNotFoundError:
        print(
            "::error::'with.use_pyproject' input requires a pyproject.toml file.",
            file=sys.stderr,
            flush=True,
        )
        sys.exit(1)

    version = pyproject.get("tool", {}).get("black", {}).get("required-version")
    if version is not None:
        # Match the two supported usages of `required-version`:
        if "." in version:
            return f"=={version}"
        else:
            return f"~={version}.0"

    arrays = [
        *pyproject.get("dependency-groups", {}).values(),
        pyproject.get("project", {}).get("dependencies"),
        *pyproject.get("project", {}).get("optional-dependencies", {}).values(),
    ]
    for array in arrays:
        version = find_black_version_in_array(array)
        if version is not None:
            break

    if version is None:
        print(
            "::error::'black' dependency missing from pyproject.toml.",
            file=sys.stderr,
            flush=True,
        )
        sys.exit(1)

    return version


def find_black_version_in_array(array: object) -> str | None:
    if not isinstance(array, list):
        return None
    try:
        for item in array:
            # Rudimentary PEP 508 parsing.
            item = item.split(";")[0]
            item = EXTRAS_RE.sub("", item).strip()
            if item == "black":
                print(
                    "::error::Version specifier missing for 'black' dependency in "
                    "pyproject.toml.",
                    file=sys.stderr,
                    flush=True,
                )
                sys.exit(1)
            elif m := BLACK_VERSION_RE.match(item):
                return m.group(1).strip()
    except TypeError:
        pass

    return None


run([sys.executable, "-m", "venv", str(ENV_PATH)], check=True)

version_specifier = determine_version_specifier()
if JUPYTER:
    extra_deps = "[colorama,jupyter]"
else:
    extra_deps = "[colorama]"
if version_specifier:
    req = f"black{extra_deps}{version_specifier}"
else:
    describe_name = ""
    with open(ACTION_PATH / ".git_archival.txt", encoding="utf-8") as fp:
        for line in fp:
            if line.startswith("describe-name: "):
                describe_name = line[len("describe-name: ") :].rstrip()
                break
    if not describe_name:
        print("::error::Failed to detect action version.", file=sys.stderr, flush=True)
        sys.exit(1)
    # expected format is one of:
    # - 23.1.0
    # - 23.1.0-51-g448bba7
    # - $Format:%(describe:tags=true,match=*[0-9]*)$ (if export-subst fails)
    if (
        describe_name.count("-") < 2
        and EXPORT_SUBST_FAIL_RE.match(describe_name) is None
    ):
        # the action's commit matches a tag exactly, install exact version from PyPI
        req = f"black{extra_deps}=={describe_name}"
    else:
        # the action's commit does not match any tag, install from the local git repo
        req = f".{extra_deps}"
print(f"Installing {req}...", flush=True)
pip_proc = run(
    [str(ENV_BIN / "python"), "-m", "pip", "install", req],
    stdout=PIPE,
    stderr=STDOUT,
    encoding="utf-8",
    cwd=ACTION_PATH,
)
if pip_proc.returncode:
    print(pip_proc.stdout)
    print("::error::Failed to install Black.", file=sys.stderr, flush=True)
    sys.exit(pip_proc.returncode)


base_cmd = [str(ENV_BIN / "black")]
if BLACK_ARGS:
    # TODO: remove after a while since this is deprecated in favour of SRC + OPTIONS.
    proc = run(
        [*base_cmd, *shlex.split(BLACK_ARGS)],
        stdout=PIPE,
        stderr=STDOUT,
        encoding="utf-8",
    )
else:
    proc = run(
        [*base_cmd, *shlex.split(OPTIONS), *shlex.split(SRC)],
        stdout=PIPE,
        stderr=STDOUT,
        encoding="utf-8",
    )
shutil.rmtree(ENV_PATH, ignore_errors=True)

# Write output to file if specified
if OUTPUT_FILE:
    try:
        with open(OUTPUT_FILE, "w", encoding="utf-8") as f:
            f.write(proc.stdout)
        print(f"Black output written to {OUTPUT_FILE}")
    except Exception as e:
        print(f"::error::Failed to write output to {OUTPUT_FILE}: {e}", file=sys.stderr)
        sys.exit(1)

print(proc.stdout)
sys.exit(proc.returncode)
