"""
Sophi Parser
Builds an AST (Abstract Syntax Tree) from the token list produced by the lexer.

Grammar (simplified BNF):

  swap_program  ::= 'swap' '{' swap_body '}'
  swap_body     ::= expires_decl? party_decl+
  expires_decl  ::= 'expires' ('block' | 'timestamp') '(' NUMBER ')'
  party_decl    ::= IDENT side_decl side_decl
  side_decl     ::= ('wants' | 'gives') value 'partial'?
  value         ::= plain_value | blind_value
  plain_value   ::= NUMBER IDENT
  blind_value   ::= 'blind' '(' HASH ')'
"""

from dataclasses import dataclass, field
from typing import List, Optional
from .lexer import Token, tokenise, LexError

# ── AST nodes ──────────────────────────────────────────────────────────────────

@dataclass
class PlainValue:
    amount: float
    asset: str

@dataclass
class BlindValue:
    hash: str           # hex string, 64 chars

@dataclass
class Side:
    direction: str      # 'wants' | 'gives'
    value: object       # PlainValue | BlindValue
    partial: bool = False

@dataclass
class PartyDecl:
    name: str
    wants: Side
    gives: Side
    line: int

@dataclass
class ExpiresDecl:
    kind: str           # 'block' | 'timestamp'
    value: int

@dataclass
class SwapProgram:
    expires: Optional[ExpiresDecl]
    parties: List[PartyDecl]

# ── Parse errors ───────────────────────────────────────────────────────────────

class ParseError(Exception):
    pass

# ── Parser ─────────────────────────────────────────────────────────────────────

class Parser:
    def __init__(self, tokens: List[Token]):
        self.tokens = tokens
        self.pos = 0

    def peek(self) -> Token:
        return self.tokens[self.pos]

    def advance(self) -> Token:
        tok = self.tokens[self.pos]
        self.pos += 1
        return tok

    def expect(self, type_: str, value: str = None) -> Token:
        tok = self.advance()
        if tok.type != type_:
            raise ParseError(
                f"Line {tok.line}: expected {type_!r} but got {tok.type!r} ({tok.value!r})"
            )
        if value and tok.value != value:
            raise ParseError(
                f"Line {tok.line}: expected {value!r} but got {tok.value!r}"
            )
        return tok

    def match(self, type_: str, value: str = None) -> bool:
        tok = self.peek()
        if tok.type != type_:
            return False
        if value and tok.value != value:
            return False
        return True

    def parse(self) -> SwapProgram:
        self.expect('KEYWORD', 'swap')
        self.expect('LBRACE')
        program = self._parse_body()
        self.expect('RBRACE')
        self.expect('EOF')
        return program

    def _parse_body(self) -> SwapProgram:
        expires = None

        # Optional expires declaration
        if self.match('KEYWORD', 'expires'):
            expires = self._parse_expires()

        # Collect all (name, side) lines then group by name
        # Syntax: each line is   IDENT ('wants'|'gives') value ['partial']
        raw: list = []   # list of (name, line_no, Side)

        while not self.match('RBRACE') and not self.match('EOF'):
            name_tok = self.expect('IDENT')
            side = self._parse_side()
            raw.append((name_tok.value, name_tok.line, side))

        # Group by party name preserving order
        seen: dict = {}
        order: list = []
        for name, line_no, side in raw:
            if name not in seen:
                seen[name] = {'line': line_no, 'sides': {}}
                order.append(name)
            if side.direction in seen[name]['sides']:
                raise ParseError(
                    f"Line {line_no}: {name!r} has two '{side.direction}' declarations."
                )
            seen[name]['sides'][side.direction] = side

        parties = []
        for name in order:
            entry = seen[name]
            sides = entry['sides']
            if 'wants' not in sides or 'gives' not in sides:
                raise ParseError(
                    f"Line {entry['line']}: {name!r} must have both 'wants' and 'gives'."
                )
            parties.append(PartyDecl(
                name=name,
                wants=sides['wants'],
                gives=sides['gives'],
                line=entry['line'],
            ))

        if len(parties) < 2:
            raise ParseError("A swap needs at least two parties.")

        return SwapProgram(expires=expires, parties=parties)

    def _parse_expires(self) -> ExpiresDecl:
        self.advance()  # consume 'expires'
        kind_tok = self.advance()
        if kind_tok.value not in ('block', 'timestamp'):
            raise ParseError(f"Line {kind_tok.line}: expected 'block' or 'timestamp', got {kind_tok.value!r}")
        self.expect('LPAREN')
        num_tok = self.expect('NUMBER')
        self.expect('RPAREN')
        return ExpiresDecl(kind=kind_tok.value, value=int(float(num_tok.value)))

    def _parse_side(self) -> Side:
        dir_tok = self.advance()
        if dir_tok.type != 'KEYWORD' or dir_tok.value not in ('wants', 'gives'):
            raise ParseError(
                f"Line {dir_tok.line}: expected 'wants' or 'gives', got {dir_tok.value!r}"
            )
        direction = dir_tok.value
        value = self._parse_value()
        partial = False
        if self.match('KEYWORD', 'partial'):
            self.advance()
            partial = True
        return Side(direction=direction, value=value, partial=partial)

    def _parse_value(self) -> object:
        tok = self.peek()

        # blind(HASH)
        if tok.type == 'KEYWORD' and tok.value == 'blind':
            self.advance()
            self.expect('LPAREN')
            hash_tok = self.expect('HASH')
            self.expect('RPAREN')
            return BlindValue(hash=hash_tok.value.lstrip('0x').lower())

        # NUMBER ASSET
        if tok.type == 'NUMBER':
            num_tok = self.advance()
            asset_tok = self.advance()
            if asset_tok.type not in ('IDENT', 'KEYWORD'):
                raise ParseError(
                    f"Line {asset_tok.line}: expected asset name after amount, got {asset_tok.value!r}"
                )
            return PlainValue(amount=float(num_tok.value), asset=asset_tok.value.upper())

        raise ParseError(f"Line {tok.line}: expected a value (amount or blind hash), got {tok.value!r}")


# ── Public entry point ─────────────────────────────────────────────────────────

def parse(source: str) -> SwapProgram:
    tokens = tokenise(source)
    return Parser(tokens).parse()
