"""Expand tox factor expressions to tox environment list."""

from __future__ import annotations

import re
import sys
import sysconfig
from itertools import chain, groupby, product
from typing import TYPE_CHECKING

from python_discovery import KNOWN_ARCHITECTURES, normalize_isa

if TYPE_CHECKING:
    from collections.abc import Iterator

LATEST_PYTHON_MINOR_MIN: int = 10
LATEST_PYTHON_MINOR_MAX: int = 14


def filter_for_env(value: str, name: str | None) -> str:
    env_factors = (
        set(chain.from_iterable([(i for i, _ in a) for a in find_factor_groups(name)])) if name is not None else set()
    )
    current = set(env_factors)
    current.add(sys.platform)
    parts = sysconfig.get_platform().rsplit("-", 1)
    if len(parts) > 1:
        machine_isa = normalize_isa(parts[-1])
        # Add machine ISA implicitly only when the env name does not already contain
        # an architecture factor; when it does the explicit ISA takes precedence and
        # adding the machine ISA would cause cross-architecture conflicts (#3903).
        if not (env_factors & KNOWN_ARCHITECTURES) and not any(normalize_isa(f) == machine_isa for f in env_factors):
            current.add(machine_isa)
    overall: list[str] = []
    active_continuation = False
    pending_skip = False
    for factors, content in expand_factors(value):
        if factors is None:
            if pending_skip and not active_continuation and not content.endswith("\\"):
                pending_skip = False
                continue
            if content:
                overall.append(content)
            active_continuation = content.endswith("\\") if content else active_continuation
            pending_skip = False
        else:
            matched = any(all((a_name in current) ^ negate for a_name, negate in group) for group in factors)
            if matched:
                overall.append(content)
                active_continuation = content.endswith("\\")
                pending_skip = False
            else:
                pending_skip = content.endswith("\\")
    return "\n".join(overall)


def find_envs(value: str) -> Iterator[str]:
    seen = set()
    for factors, _ in expand_factors(value):
        if factors is not None:
            for group in factors:
                env = explode_factor(group)
                if env not in seen:
                    yield env
                    seen.add(env)


def extend_factors(value: str) -> Iterator[str]:
    for group in find_factor_groups(value):
        yield explode_factor(group)


def explode_factor(group: list[tuple[str, bool]]) -> str:
    return "-".join([name for name, _ in group])


def expand_factors(value: str) -> Iterator[tuple[list[list[tuple[str, bool]]] | None, str]]:
    for line in value.split("\n"):
        factors: list[list[tuple[str, bool]]] | None = None
        marker_search = re.search(
            r"""
            :           # colon separator
            ( \s | $ )  # followed by whitespace or end of string
            """,
            line,
            re.VERBOSE,
        )
        marker_at, content = marker_search.start() if marker_search else -1, line
        if marker_at != -1:
            try:
                factors = list(find_factor_groups(line[:marker_at].strip()))
            except ValueError:
                pass  # when cannot extract factors keep the entire line
            else:
                content = line[marker_at + 1 :].strip()
        yield factors, content


def find_factor_groups(value: str) -> Iterator[list[tuple[str, bool]]]:
    """Transform '{py,!pi}-{a,b},c' to [{'py', 'a'}, {'py', 'b'}, {'pi', 'a'}, {'pi', 'b'}, {'c'}]."""
    value = expand_ranges(value)
    for env in expand_env_with_negation(value):
        yield [name_with_negate(f) for f in env.split("-")]


_FACTOR_RE = re.compile(
    r"""
    (?:
        !?              # optional negation prefix
        [\w.*?]         # first char: word char, dot, or glob wildcards
        [\w.*?-]*       # remaining chars: word chars, dots, glob wildcards, or hyphens
    |
        ^$              # or an empty string
    )
    """,
    re.VERBOSE,
)


def expand_env_with_negation(value: str) -> Iterator[str]:
    """Transform '{py,!pi}-{a,b},c' to ['py-a', 'py-b', '!pi-a', '!pi-b', 'c']."""
    for key, group in groupby(
        re.split(
            r"""
            ( (?: \{ [^}]+ \} )+ )  # one or more brace groups
            |                        # or
            ,                        # comma separator
            """,
            value,
            flags=re.VERBOSE,
        ),
        key=bool,
    ):
        if key:
            group_str = "".join(group).strip()
            elements = re.split(
                r"""
                \{          # opening brace
                ( [^}]+ )   # capture contents
                \}          # closing brace
                """,
                group_str,
                flags=re.VERBOSE,
            )
            parts = [[i.strip() for i in elem.split(",")] for elem in elements]
            for variant in product(*parts):
                variant_str = "".join(variant)
                if not all(_FACTOR_RE.fullmatch(i) for i in variant_str.split("-")):
                    raise ValueError(variant_str)
                yield variant_str


def name_with_negate(factor: str) -> tuple[str, bool]:
    negated = is_negated(factor)
    result = factor[1:] if negated else factor
    return result, negated


def is_negated(factor: str) -> bool:
    return factor.startswith("!")


def expand_ranges(value: str) -> str:
    """Expand ranges in env expressions.

    Supports closed ranges ``{10-13}``, right-open ``{10-}`` (upper bound = :data:`LATEST_PYTHON_MINOR_MAX`), and
    left-open ``{-13}`` (lower bound = :data:`LATEST_PYTHON_MINOR_MIN`).

    """

    def _expand(match: re.Match[str]) -> str:
        src, start_, end_, open_start, open_end = match.groups()
        delimiter = match.group()[-1]  # the ',' or '}' that terminated the match
        if src and start_ and end_:
            start, end = int(start_), int(end_)
            direction = 1 if start < end else -1
            expansion = ",".join(str(x) for x in range(start, end + direction, direction))
        elif open_start:
            expansion = ",".join(str(x) for x in range(int(open_start), LATEST_PYTHON_MINOR_MAX + 1))
        elif open_end:
            expansion = ",".join(str(x) for x in range(LATEST_PYTHON_MINOR_MIN, int(open_end) + 1))
        else:  # a single number, leave untouched
            return match.group()
        return f"{expansion}{delimiter}"

    return re.sub(
        r"""
        (                       # outer capture group
            (?<! [\w.] )        # a range opens a factor, so digits carrying on a name are not one (py313-django4-2)
            (?:
                ( \d+ ) - ( \d+ ) # closed range: start-end
                |
                ( \d+ ) -         # right-open range: start-
            )
            |
            (?<= [{,] ) - ( \d+ )  # left-open range: -end (preceded by { or ,)
            |
            \d+                 # single number
        )
        (?: , | \} )            # followed by comma or closing brace
        """,
        _expand,
        value,
        flags=re.VERBOSE,
    )


__all__ = (
    "LATEST_PYTHON_MINOR_MAX",
    "LATEST_PYTHON_MINOR_MIN",
    "expand_factors",
    "expand_ranges",
    "extend_factors",
    "filter_for_env",
    "find_envs",
)
