import dataclasses
from collections.abc import Mapping, Sequence
from typing import Any, Protocol

import bashlex
import bashlex.errors

from cibuildwheel import bashlex_eval


class EnvironmentParseError(Exception):
    pass


def split_env_items(env_string: str) -> list[str]:
    """Splits space-separated variable assignments into a list of individual assignments.

    >>> split_env_items('VAR=abc')
    ['VAR=abc']
    >>> split_env_items('VAR="a string" THING=3')
    ['VAR="a string"', 'THING=3']
    >>> split_env_items('VAR="a string" THING=\\'single "quotes"\\'')
    ['VAR="a string"', 'THING=\\'single "quotes"\\'']
    >>> split_env_items('VAR="dont \\\\"forget\\\\" about backslashes"')
    ['VAR="dont \\\\"forget\\\\" about backslashes"']
    >>> split_env_items('PATH="$PATH;/opt/cibw_test_path"')
    ['PATH="$PATH;/opt/cibw_test_path"']
    >>> split_env_items('PATH2="something with spaces"')
    ['PATH2="something with spaces"']
    """
    if not env_string:
        return []

    try:
        command_node = bashlex.parsesingle(env_string)
    except bashlex.errors.ParsingError as e:
        raise EnvironmentParseError(env_string) from e

    result = []

    for word_node in command_node.parts:
        part_string = env_string[word_node.pos[0] : word_node.pos[1]]
        result.append(part_string)

    return result


class EnvironmentAssignment(Protocol):
    name: str

    def evaluated_value(
        self,
        *,
        environment: Mapping[str, str],
        executor: bashlex_eval.EnvironmentExecutor | None = None,
    ) -> str:
        """Returns the value of this assignment, as evaluated in the environment"""


class EnvironmentAssignmentRaw:
    """
    An environment variable - a simple name/value pair
    """

    def __init__(self, name: str, value: str):
        self.name = name
        self.value = value

    def __repr__(self) -> str:
        return f"{self.name}={self.value}"

    def evaluated_value(self, **_: Any) -> str:
        return self.value


@dataclasses.dataclass
class EnvironmentAssignmentBash:
    """
    An environment variable, in bash syntax. The value can use bash constructs
    like "$OTHER_VAR" and "$(command arg1 arg2)".
    """

    name: str
    value: str

    def __init__(self, assignment: str):
        name, equals, value = assignment.partition("=")
        if not equals:
            raise EnvironmentParseError(assignment)
        self.name = name
        self.value = value

    def evaluated_value(
        self,
        environment: Mapping[str, str],
        executor: bashlex_eval.EnvironmentExecutor | None = None,
    ) -> str:
        return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)

    def __repr__(self) -> str:
        return f"{self.name}={self.value}"


@dataclasses.dataclass(kw_only=True)
class ParsedEnvironment:
    assignments: list[EnvironmentAssignment]

    def __init__(self, assignments: Sequence[EnvironmentAssignment]) -> None:
        self.assignments = list(assignments)

    def as_dictionary(
        self,
        prev_environment: Mapping[str, str],
        executor: bashlex_eval.EnvironmentExecutor | None = None,
    ) -> dict[str, str]:
        environment = {**prev_environment}

        for assignment in self.assignments:
            value = assignment.evaluated_value(environment=environment, executor=executor)
            environment[assignment.name] = value

        return environment

    def add(self, name: str, value: str, prepend: bool = False) -> None:
        assignment = EnvironmentAssignmentRaw(name=name, value=value)
        if prepend:
            self.assignments.insert(0, assignment)
        else:
            self.assignments.append(assignment)

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"

    def options_summary(self) -> list[EnvironmentAssignment]:
        return self.assignments


def parse_environment(env_string: str) -> ParsedEnvironment:
    env_items = split_env_items(env_string)
    assignments = [EnvironmentAssignmentBash(item) for item in env_items]
    return ParsedEnvironment(assignments=assignments)
