from __future__ import annotations

import re
from dataclasses import dataclass, field
from typing import Any


_ACTION_NAME_PATTERN = re.compile(r"^[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*$")


class ACLParseError(ValueError):
    def __init__(
        self,
        message: str,
        line_number: int | None = None,
        column: int | None = None,
    ) -> None:
        self.message = message
        self.line_number = line_number
        self.column = column
        self.code = "parse_error"

        location = []
        if line_number is not None:
            location.append(f"line {line_number}")
        if column is not None:
            location.append(f"column {column}")

        suffix = f" ({', '.join(location)})" if location else ""
        super().__init__(f"{message}{suffix}")


@dataclass(frozen=True)
class VariableRef:
    name: str
    selectors: tuple[str | int, ...] = ()


@dataclass(frozen=True)
class ACLCommand:
    action: str
    params: dict[str, Any]
    assign: str | None = None
    meta: dict[str, str] = field(default_factory=dict)
    line_number: int = 0


def parse_acl(acl_text: str) -> list[ACLCommand]:
    commands: list[ACLCommand] = []
    pending_meta: dict[str, str] = {}
    pending_meta_line: int | None = None
    index = 0
    length = len(acl_text)

    while index < length:
        while index < length and acl_text[index] in (" ", "\t", "\r", "\n"):
            index += 1
        if index >= length:
            break

        line_number = acl_text[:index].count("\n") + 1

        if acl_text[index] == "#":
            index = _skip_to_end_of_line(acl_text, index)
            continue

        if acl_text[index] == "@":
            key, value, index = _parse_metadata(acl_text, index, line_number)
            pending_meta[key] = value
            pending_meta_line = line_number
            continue

        command, index = _parse_command(acl_text, index, line_number, pending_meta)
        commands.append(command)
        pending_meta = {}
        pending_meta_line = None

    if pending_meta:
        raise ACLParseError(
            "metadata must be followed by a command",
            line_number=pending_meta_line,
        )

    return commands


def parse_params(source: str, line_number: int | None = None) -> dict[str, Any]:
    params: dict[str, Any] = {}
    index = 0
    source_length = len(source)

    while True:
        index = _skip_whitespace(source, index)
        if index >= source_length:
            return params

        key, index = _consume_identifier(
            source,
            index,
            line_number,
            "parameter name",
        )
        index = _skip_whitespace(source, index)
        if index >= source_length or source[index] != "=":
            raise ACLParseError("expected '=' after parameter name", line_number, index + 1)

        index += 1
        index = _skip_whitespace(source, index)
        value, index = _parse_value(source, index, line_number)

        if key in params:
            raise ACLParseError(f"duplicate parameter '{key}'", line_number, index + 1)
        params[key] = value

        index = _skip_whitespace(source, index)
        if index >= source_length:
            return params

        if source[index] != ",":
            raise ACLParseError("expected ',' between parameters", line_number, index + 1)
        index += 1


def _parse_metadata(source: str, index: int, line_number: int) -> tuple[str, str, int]:
    index += 1  # skip @
    key, index = _consume_identifier(source, index, line_number, "metadata key")
    index = _skip_inline_whitespace(source, index)
    if index >= len(source) or source[index] != "=":
        raise ACLParseError("expected '=' after metadata key", line_number, index + 1)

    index += 1
    index = _skip_inline_whitespace(source, index)
    value, index = _parse_string(source, index, line_number)
    index = _skip_inline_whitespace(source, index)
    if index < len(source) and source[index] not in ("\n", "\r"):
        raise ACLParseError("unexpected trailing metadata content", line_number, index + 1)

    return key, value, index


def _parse_command(source: str, index: int, line_number: int, meta: dict[str, str]) -> tuple[ACLCommand, int]:
    action, index = _consume_action_name(source, index, line_number)
    index = _skip_whitespace(source, index)

    params_source, index = _extract_parenthesized(source, index, line_number)
    index = _skip_inline_whitespace(source, index)

    assign: str | None = None
    if index < len(source) and source[index:index + 2] == "->":
        index += 2
        index = _skip_inline_whitespace(source, index)
        assign, index = _consume_identifier(source, index, line_number, "assignment name")
        index = _skip_inline_whitespace(source, index)

    if index < len(source) and source[index] not in ("\n", "\r", "#"):
        raise ACLParseError("unexpected trailing command content", line_number, index + 1)

    return ACLCommand(
        action=action,
        params=parse_params(params_source, line_number),
        assign=assign,
        meta=dict(meta),
        line_number=line_number,
    ), index


def _consume_action_name(source: str, index: int, line_number: int) -> tuple[str, int]:
    start = index
    while index < len(source) and (source[index].isalnum() or source[index] in "._"):
        index += 1

    action = source[start:index]
    if not action or not _ACTION_NAME_PATTERN.fullmatch(action):
        raise ACLParseError("invalid action name", line_number, start + 1)

    return action, index


def _extract_parenthesized(source: str, index: int, line_number: int) -> tuple[str, int]:
    if index >= len(source) or source[index] != "(":
        raise ACLParseError("expected '(' after action name", line_number, index + 1)

    index += 1
    depth = 1
    escape = False
    in_string = False
    in_triple = False
    chars: list[str] = []

    while index < len(source):
        char = source[index]
        if in_triple:
            if escape:
                chars.append(char)
                escape = False
            elif char == "\\":
                chars.append(char)
                escape = True
            elif source[index:index + 3] == '"""':
                chars.append('"""')
                index += 3
                in_triple = False
                continue
            else:
                chars.append(char)
            index += 1
            continue

        if in_string:
            chars.append(char)
            if escape:
                escape = False
            elif char == "\\":
                escape = True
            elif char == '"':
                in_string = False
        else:
            if source[index:index + 3] == '"""':
                in_triple = True
                chars.append('"""')
                index += 3
                continue
            elif char == '"':
                in_string = True
                chars.append(char)
            elif char == "(":
                depth += 1
                chars.append(char)
            elif char == ")":
                depth -= 1
                if depth == 0:
                    return "".join(chars), index + 1
                chars.append(char)
            else:
                chars.append(char)
        index += 1

    raise ACLParseError("unclosed parameter list", line_number, index + 1)


def _parse_value(source: str, index: int, line_number: int | None) -> tuple[Any, int]:
    if index >= len(source):
        raise ACLParseError("expected a value", line_number, index + 1)

    char = source[index]
    if char == '"':
        return _parse_string(source, index, line_number)
    if char == "[":
        return _parse_list(source, index, line_number)
    if char == "{":
        return _parse_object(source, index, line_number)
    if char == "-" or char.isdigit():
        return _parse_number(source, index, line_number)
    if char.isalpha() or char == "_":
        word, index = _consume_identifier(source, index, line_number, "value")
        if word == "true":
            return True, index
        if word == "false":
            return False, index
        if word in {"None", "null"}:
            return None, index
        selectors, index = _parse_selectors(source, index, line_number)
        return VariableRef(word, selectors), index

    raise ACLParseError("unsupported value", line_number, index + 1)


def _parse_string(source: str, index: int, line_number: int | None) -> tuple[str, int]:
    if source[index] != '"':
        raise ACLParseError("expected a string value", line_number, index + 1)

    if source[index:index + 3] == '"""':
        return _parse_triple_string(source, index, line_number)

    index += 1
    chars: list[str] = []
    escape = False

    while index < len(source):
        char = source[index]
        if escape:
            if char == "n":
                chars.append("\n")
            elif char == "t":
                chars.append("\t")
            else:
                chars.append(char)
            escape = False
        else:
            if char == "\\":
                escape = True
            elif char == '"':
                return "".join(chars), index + 1
            else:
                chars.append(char)
        index += 1

    raise ACLParseError("unterminated string value", line_number, index + 1)


def _parse_triple_string(source: str, index: int, line_number: int | None) -> tuple[str, int]:
    index += 3
    chars: list[str] = []
    escape = False

    while index < len(source):
        char = source[index]
        if escape:
            if char == "n":
                chars.append("\n")
            elif char == "t":
                chars.append("\t")
            else:
                chars.append(char)
            escape = False
        elif char == "\\":
            escape = True
        elif source[index:index + 3] == '"""':
            return "".join(chars), index + 3
        else:
            chars.append(char)
        index += 1

    raise ACLParseError("unterminated triple-quoted string", line_number, index + 1)


def _parse_number(source: str, index: int, line_number: int | None) -> tuple[int | float, int]:
    start = index
    if source[index] == "-":
        index += 1

    digits_start = index
    while index < len(source) and source[index].isdigit():
        index += 1

    is_float = False
    if index < len(source) and source[index] == ".":
        is_float = True
        index += 1
        while index < len(source) and source[index].isdigit():
            index += 1

    if digits_start == index or (is_float and source[index - 1] == "."):
        raise ACLParseError("invalid numeric literal", line_number, start + 1)

    literal = source[start:index]
    return (float(literal) if is_float else int(literal)), index


def _parse_list(source: str, index: int, line_number: int | None) -> tuple[list[Any], int]:
    if source[index] != "[":
        raise ACLParseError("expected '[' to start list", line_number, index + 1)

    values: list[Any] = []
    index += 1

    while True:
        index = _skip_whitespace(source, index)
        if index >= len(source):
            raise ACLParseError("unterminated list", line_number, index + 1)
        if source[index] == "]":
            return values, index + 1

        value, index = _parse_value(source, index, line_number)
        values.append(value)
        index = _skip_whitespace(source, index)

        if index >= len(source):
            raise ACLParseError("unterminated list", line_number, index + 1)
        if source[index] == "]":
            return values, index + 1
        if source[index] != ",":
            raise ACLParseError("expected ',' between list items", line_number, index + 1)
        index += 1


def _parse_object(source: str, index: int, line_number: int | None) -> tuple[dict[str, Any], int]:
    if source[index] != "{":
        raise ACLParseError("expected '{' to start object", line_number, index + 1)

    values: dict[str, Any] = {}
    index += 1

    while True:
        index = _skip_whitespace(source, index)
        if index >= len(source):
            raise ACLParseError("unterminated object", line_number, index + 1)
        if source[index] == "}":
            return values, index + 1

        key, index = _parse_object_key(source, index, line_number)
        index = _skip_whitespace(source, index)
        if index >= len(source) or source[index] not in {":", "="}:
            raise ACLParseError("expected ':' or '=' after object key", line_number, index + 1)

        index += 1
        index = _skip_whitespace(source, index)
        value, index = _parse_value(source, index, line_number)
        if key in values:
            raise ACLParseError(f"duplicate object key '{key}'", line_number, index + 1)
        values[key] = value

        index = _skip_whitespace(source, index)
        if index >= len(source):
            raise ACLParseError("unterminated object", line_number, index + 1)
        if source[index] == "}":
            return values, index + 1
        if source[index] != ",":
            raise ACLParseError("expected ',' between object items", line_number, index + 1)
        index += 1


def _parse_object_key(source: str, index: int, line_number: int | None) -> tuple[str, int]:
    if source[index] == '"':
        return _parse_string(source, index, line_number)
    return _consume_identifier(source, index, line_number, "object key")


def _parse_selectors(
    source: str,
    index: int,
    line_number: int | None,
) -> tuple[tuple[str | int, ...], int]:
    selectors: list[str | int] = []

    while True:
        next_index = _skip_whitespace(source, index)
        if next_index >= len(source):
            break

        if source[next_index] == ".":
            next_index += 1
            selector, next_index = _consume_identifier(source, next_index, line_number, "field selector")
            selectors.append(selector)
            index = next_index
            continue

        if source[next_index] == "[":
            next_index += 1
            next_index = _skip_whitespace(source, next_index)
            selector, next_index = _parse_selector_value(source, next_index, line_number)
            next_index = _skip_whitespace(source, next_index)
            if next_index >= len(source) or source[next_index] != "]":
                raise ACLParseError("expected ']' after selector", line_number, next_index + 1)
            selectors.append(selector)
            index = next_index + 1
            continue

        break

    return tuple(selectors), index


def _parse_selector_value(
    source: str,
    index: int,
    line_number: int | None,
) -> tuple[str | int, int]:
    if index >= len(source):
        raise ACLParseError("expected selector value", line_number, index + 1)

    if source[index] == '"':
        return _parse_string(source, index, line_number)

    if source[index] == "-" or source[index].isdigit():
        value, index = _parse_number(source, index, line_number)
        if isinstance(value, float):
            raise ACLParseError("list selectors must use integer indexes", line_number, index + 1)
        return value, index

    selector, index = _consume_identifier(source, index, line_number, "selector")
    return selector, index


def _consume_identifier(
    source: str,
    index: int,
    line_number: int | None,
    label: str,
) -> tuple[str, int]:
    if index >= len(source) or not (source[index].isalpha() or source[index] == "_"):
        raise ACLParseError(f"expected {label}", line_number, index + 1)

    start = index
    index += 1
    while index < len(source) and (source[index].isalnum() or source[index] == "_"):
        index += 1
    return source[start:index], index


def _skip_whitespace(source: str, index: int) -> int:
    while index < len(source) and source[index].isspace():
        index += 1
    return index


def _skip_to_end_of_line(source: str, index: int) -> int:
    while index < len(source) and source[index] != "\n":
        index += 1
    return index


def _skip_inline_whitespace(source: str, index: int) -> int:
    while index < len(source) and source[index] in (" ", "\t"):
        index += 1
    return index


parse = parse_acl
