import random
import re
from dataclasses import dataclass


MAX_DICE_COUNT = 100
MAX_DICE_FACES = 1000
MAX_TERM_COUNT = 20
MAX_TOTAL_ROLLS = 200
MAX_EXPRESSION_LENGTH = 200
MAX_MESSAGE_LENGTH = 1200

_TOKEN_RE = re.compile(r"([+-]?)(\d*d(?:%|[fF]|\d+)|\d+)")
_DICE_RE = re.compile(r"(?i)(\d*)d(%|f|\d+)")

# 常见跑团表达式示例：3d6、d20+5、2d8+1d4+3、d%、4dF


class RollError(ValueError):
    pass


@dataclass(slots=True)
class RollTermResult:
    sign: int
    source: str
    detail: str
    value: int


@dataclass(slots=True)
class RollResult:
    expression: str
    total: int
    terms: list[RollTermResult]

    def format(self) -> str:
        parts = [f"{term.source}={term.detail}" for term in self.terms]
        detail = " ".join(parts)
        text = f"{self.expression} = {self.total}"
        if detail:
            text += f"\n{detail}"
        if len(text) > MAX_MESSAGE_LENGTH:
            raise RollError("结果过长，请减少骰子数量或简化表达式")
        return text


def _parse_single_term(raw: str, sign: int, rng: random.Random) -> RollTermResult:
    dice_match = _DICE_RE.fullmatch(raw)
    if dice_match:
        count_text, faces_text = dice_match.groups()
        count = int(count_text) if count_text else 1
        if count <= 0:
            raise RollError("骰子个数必须大于 0")
        if count > MAX_DICE_COUNT:
            raise RollError(f"单项最多只能掷 {MAX_DICE_COUNT} 个骰子")

        if faces_text == "%":
            faces = 100
            rolls = [rng.randint(1, 100) for _ in range(count)]
        elif faces_text.lower() == "f":
            rolls = [rng.choice((-1, 0, 1)) for _ in range(count)]
            total = sum(rolls) * sign
            signed = "+" if sign > 0 else "-"
            return RollTermResult(
                sign=sign,
                source=f"{signed}{count}dF",
                detail="[" + ", ".join(f"{v:+d}" for v in rolls) + "]",
                value=total,
            )
        else:
            faces = int(faces_text)
            if faces <= 0:
                raise RollError("骰子面数必须大于 0")
            if faces > MAX_DICE_FACES:
                raise RollError(f"骰子面数不能超过 {MAX_DICE_FACES}")
            rolls = [rng.randint(1, faces) for _ in range(count)]

        total = sum(rolls) * sign
        signed = "+" if sign > 0 else "-"
        return RollTermResult(
            sign=sign,
            source=f"{signed}{count}d{faces_text.upper()}",
            detail="[" + ", ".join(map(str, rolls)) + "]",
            value=total,
        )

    value = int(raw) * sign
    signed = "+" if sign > 0 else "-"
    return RollTermResult(sign=sign, source=f"{signed}{raw}", detail=str(abs(value)), value=value)


def roll_expression(expr: str, rng: random.Random | None = None) -> RollResult:
    expr = expr.strip().replace(" ", "")
    if not expr:
        raise RollError("请提供要掷的表达式，例如 roll 3d6 或 roll d20+5")
    if len(expr) > MAX_EXPRESSION_LENGTH:
        raise RollError("表达式太长了")

    matches = list(_TOKEN_RE.finditer(expr))
    if not matches:
        raise RollError("无法解析表达式，请使用如 3d6、d20+5、2d8+1d4+3、4dF 这样的格式")

    rebuilt = "".join(m.group(0) for m in matches)
    if rebuilt != expr:
        raise RollError("表达式中含有无法识别的内容")
    if len(matches) > MAX_TERM_COUNT:
        raise RollError(f"表达式项数不能超过 {MAX_TERM_COUNT}")

    total_rolls = 0
    for m in matches:
        token = m.group(2)
        dice_match = _DICE_RE.fullmatch(token)
        if dice_match:
            count_text, faces_text = dice_match.groups()
            count = int(count_text) if count_text else 1
            if count <= 0:
                raise RollError("骰子个数必须大于 0")
            if count > MAX_DICE_COUNT:
                raise RollError(f"单项最多只能掷 {MAX_DICE_COUNT} 个骰子")
            if faces_text not in {"%", "f", "F"}:
                faces = int(faces_text)
                if faces <= 0:
                    raise RollError("骰子面数必须大于 0")
                if faces > MAX_DICE_FACES:
                    raise RollError(f"骰子面数不能超过 {MAX_DICE_FACES}")
            total_rolls += count
    if total_rolls > MAX_TOTAL_ROLLS:
        raise RollError(f"一次最多只能实际掷 {MAX_TOTAL_ROLLS} 个骰子")

    rng = rng or random.Random()
    terms: list[RollTermResult] = []
    total = 0
    for idx, match in enumerate(matches):
        sign_text, raw = match.groups()
        sign = -1 if sign_text == "-" else 1
        if idx == 0 and sign_text == "":
            sign = 1
        term = _parse_single_term(raw, sign, rng)
        terms.append(term)
        total += term.value

    return RollResult(expression=expr, total=total, terms=terms)
