from __future__ import annotations

import bisect
import re
from argparse import Action, ArgumentParser, ArgumentTypeError, Namespace
from typing import TYPE_CHECKING, Any, NoReturn, Protocol, TypeVar, cast

from tox.tox_env.python.pip.req.util import handle_binary_option

if TYPE_CHECKING:
    from collections.abc import Sequence

_T_contra = TypeVar("_T_contra", contravariant=True)


# stable
class _SupportsWrite(Protocol[_T_contra]):
    def write(self, s: _T_contra, /) -> object: ...


class _OurArgumentParser(ArgumentParser):
    def print_usage(self, file: _SupportsWrite[str] | None = None) -> None:
        pass

    def exit(self, status: int = 0, message: str | None = None) -> NoReturn:  # noqa: ARG002, PLR6301
        message = "" if message is None else message
        msg = message.lstrip(": ").rstrip()
        msg = msg.removeprefix("error: ")
        raise ValueError(msg)


def build_parser() -> ArgumentParser:
    parser = _OurArgumentParser(add_help=False, prog="", allow_abbrev=False)
    _global_options(parser)
    _req_options(parser)
    return parser


def _global_options(parser: ArgumentParser) -> None:
    parser.add_argument("-i", "--index-url", "--pypi-url", dest="index_url", default=None)
    parser.add_argument("--extra-index-url", action=AddUniqueAction)
    parser.add_argument("--no-index", action="store_true", default=False)
    parser.add_argument("-c", "--constraint", action=AddUniqueAction, dest="constraints")
    parser.add_argument("-r", "--requirement", action=AddUniqueAction, dest="requirements")
    parser.add_argument("-e", "--editable", action=AddUniqueAction, dest="editables")
    parser.add_argument("-f", "--find-links", action=AddUniqueAction)
    parser.add_argument("--no-binary", action=BinaryAction, nargs="+")
    parser.add_argument("--only-binary", action=BinaryAction, nargs="+")
    parser.add_argument("--prefer-binary", action="store_true", default=False)
    parser.add_argument("--require-hashes", action="store_true", default=False)
    parser.add_argument("--pre", action="store_true", default=False)
    parser.add_argument("--trusted-host", action=AddSortedUniqueAction)
    parser.add_argument(
        "--use-feature",
        choices=["2020-resolver", "fast-deps"],
        action=AddSortedUniqueAction,
        dest="features_enabled",
    )


def _req_options(parser: ArgumentParser) -> None:
    parser.add_argument("--install-option", action=AddSortedUniqueAction)
    parser.add_argument("--global-option", action=AddSortedUniqueAction)
    parser.add_argument("--hash", action=AddSortedUniqueAction, type=_validate_hash)


_HASH = re.compile(
    r"""
    sha
    (
        256 : [a-f0-9]{64}      # SHA-256 hash
        | 384 : [a-f0-9]{96}    # SHA-384 hash
        | 512 : [a-f0-9]{128}   # SHA-512 hash
    )
    """,
    re.VERBOSE,
)


def _validate_hash(value: str) -> str:
    if not _HASH.fullmatch(value):
        raise ArgumentTypeError(value)
    return value


class AddSortedUniqueAction(Action):
    def __call__(
        self,
        parser: ArgumentParser,  # noqa: ARG002
        namespace: Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,  # noqa: ARG002
    ) -> None:
        if getattr(namespace, self.dest, None) is None:
            setattr(namespace, self.dest, [])
        current = getattr(namespace, self.dest)
        if values is not None and values not in current:
            bisect.insort(current, cast("str", values))


class AddUniqueAction(Action):
    def __call__(
        self,
        parser: ArgumentParser,  # noqa: ARG002
        namespace: Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,  # noqa: ARG002
    ) -> None:
        if getattr(namespace, self.dest, None) is None:
            setattr(namespace, self.dest, [])
        current = getattr(namespace, self.dest)
        if values not in current:
            current.append(values)


class BinaryAction(Action):
    def __call__(
        self,
        parser: ArgumentParser,  # noqa: ARG002
        namespace: Namespace,
        values: str | Sequence[Any] | None,
        option_string: str | None = None,  # noqa: ARG002
    ) -> None:
        if getattr(namespace, "no_binary", None) is None:
            namespace.no_binary = set()
        if getattr(namespace, "only_binary", None) is None:
            namespace.only_binary = set()

        args = (
            (namespace.no_binary, namespace.only_binary)
            if self.dest == "no_binary"
            else (namespace.only_binary, namespace.no_binary)
        )
        assert values is not None  # noqa: S101
        handle_binary_option(values[0], *args)
