"""DSL parser implementation."""

from __future__ import annotations

from db_test_helpers.core._matcher_types import (
    AndMatcher,
    AnyMatcher,
    EqMatcher,
    GtMatcher,
    LtMatcher,
    Matcher,
    NotMatcher,
    OrMatcher,
)

_KNOWN_FUNCTIONS = frozenset({"eq", "lt", "gt", "not", "and", "or", "var", "any"})


def parse(value: str, variables: dict[str, object]) -> Matcher:
    parser = _Parser(value, variables)
    result = parser.parse_expr()
    if parser.pos < len(parser.text):
        raise ValueError(f"Unexpected trailing characters at position {parser.pos}: {value!r}")
    return result


class _Parser:
    def __init__(self, text: str, variables: dict[str, object]) -> None:
        self.text = text
        self.variables = variables
        self.pos = 0

    def parse_expr(self) -> Matcher:
        if not self._at("$"):
            raise ValueError(f"Expected '$' at position {self.pos}: {self.text!r}")
        self.pos += 1

        func_name = self._read_identifier()
        if func_name not in _KNOWN_FUNCTIONS:
            raise ValueError(f"Unknown DSL function: ${func_name}")

        self._expect("(")
        args = self._parse_args(func_name)
        self._expect(")")

        return self._build_matcher(func_name, args)

    def _parse_args(self, func_name: str) -> list[object]:
        if self._at(")"):
            if func_name == "any":
                return []
            raise ValueError(f"${func_name}() requires at least one argument")
        args: list[object] = []
        args.append(self._parse_arg())
        while self._at(","):
            self.pos += 1
            args.append(self._parse_arg())
        return args

    def _parse_arg(self) -> object:
        self._skip_whitespace()
        if self._at("$"):
            return self.parse_expr()
        return self._parse_literal()

    def _parse_literal(self) -> str | int | float:
        self._skip_whitespace()
        if self._at('"'):
            return self._parse_quoted_string()
        return self._parse_bare_token()

    def _parse_quoted_string(self) -> str:
        self.pos += 1
        chars: list[str] = []
        while self.pos < len(self.text):
            ch = self.text[self.pos]
            if ch == "\\":
                self.pos += 1
                if self.pos >= len(self.text):
                    raise ValueError("Unexpected end of string after backslash")
                chars.append(self.text[self.pos])
            elif ch == '"':
                self.pos += 1
                return "".join(chars)
            else:
                chars.append(ch)
            self.pos += 1
        raise ValueError("Unterminated quoted string")

    def _parse_bare_token(self) -> str | int | float:
        start = self.pos
        while self.pos < len(self.text) and self.text[self.pos] not in ',()"':
            self.pos += 1
        token = self.text[start : self.pos].strip()
        if not token:
            raise ValueError(f"Empty token at position {start}")
        return _coerce_literal(token)

    def _read_identifier(self) -> str:
        start = self.pos
        while self.pos < len(self.text) and self.text[self.pos].isalpha():
            self.pos += 1
        return self.text[start : self.pos]

    def _skip_whitespace(self) -> None:
        while self.pos < len(self.text) and self.text[self.pos] == " ":
            self.pos += 1

    def _at(self, ch: str) -> bool:
        return self.pos < len(self.text) and self.text[self.pos] == ch

    def _expect(self, ch: str) -> None:
        if self.pos >= len(self.text) or self.text[self.pos] != ch:
            raise ValueError(f"Expected {ch!r} at position {self.pos}: {self.text!r}")
        self.pos += 1

    def _build_matcher(self, func_name: str, args: list[object]) -> Matcher:
        if func_name == "any":
            if len(args) != 0:
                raise ValueError(f"$any() takes no arguments, got {len(args)}")
            return AnyMatcher()

        if func_name in ("eq", "lt", "gt", "not", "var") and len(args) != 1:
            raise ValueError(f"${func_name}() requires exactly 1 argument, got {len(args)}")
        if func_name in ("and", "or") and len(args) < 2:
            raise ValueError(f"${func_name}() requires at least 2 arguments, got {len(args)}")

        if func_name == "eq":
            return EqMatcher(args[0])
        if func_name == "lt":
            return LtMatcher(_to_number(args[0], "lt"))
        if func_name == "gt":
            return GtMatcher(_to_number(args[0], "gt"))
        if func_name == "not":
            return NotMatcher(_to_matcher(args[0], "not"))
        if func_name == "and":
            return AndMatcher([_to_matcher(a, "and") for a in args])
        if func_name == "or":
            return OrMatcher([_to_matcher(a, "or") for a in args])
        if func_name == "var":
            return self._build_var_matcher(args)
        raise ValueError(f"Unknown DSL function: ${func_name}")

    def _build_var_matcher(self, args: list[object]) -> Matcher:
        if not isinstance(args[0], str):
            raise ValueError("$var() argument must be a string")
        name = args[0]
        if name not in self.variables:
            raise ValueError(f"Undefined variable: {name!r}")
        return EqMatcher(self.variables[name])


def _coerce_literal(token: str) -> str | int | float:
    try:
        return int(token)
    except ValueError:
        pass
    try:
        return float(token)
    except ValueError:
        pass
    return token


def _to_number(arg: object, func: str) -> int | float:
    if isinstance(arg, (int, float)) and not isinstance(arg, bool):
        return arg
    raise ValueError(f"${func}() requires a numeric argument, got {arg!r}")


def _to_matcher(arg: object, func: str) -> Matcher:
    if isinstance(arg, Matcher):
        return arg
    raise ValueError(f"${func}() requires a matcher argument, got {arg!r}")
