"""Input parser — tokenizes commands, matches verbs, resolves objects."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from h2g2.engine.game_object import GameObject, Direction
    from h2g2.engine.state import GameState

# Words stripped from input (articles, filler)
BUZZ_WORDS = {
    "a", "an", "the", "is", "are", "am", "and", "of", "then",
    "all", "but", "except", "please", "here", "some", "more",
}

# Direction word mappings
DIRECTION_WORDS: dict[str, str] = {
    "north": "NORTH", "n": "NORTH", "fore": "NORTH", "f": "NORTH",
    "south": "SOUTH", "s": "SOUTH", "aft": "SOUTH",
    "east": "EAST", "e": "EAST", "starboard": "EAST", "sb": "EAST",
    "west": "WEST", "w": "WEST", "port": "WEST", "p": "WEST",
    "up": "UP", "u": "UP",
    "down": "DOWN", "d": "DOWN",
    "northeast": "NE", "ne": "NE",
    "northwest": "NW", "nw": "NW",
    "southeast": "SE", "se": "SE",
    "southwest": "SW", "sw": "SW",
    "in": "IN", "inside": "IN", "into": "IN", "enter": "IN",
    "out": "OUT", "outside": "OUT", "exit": "OUT", "leave": "OUT",
}

# Prepositions that separate object clauses
PREPOSITIONS = {
    "in", "into", "inside", "on", "onto", "with", "using",
    "at", "to", "toward", "from", "off", "through", "thru",
    "under", "below", "beneath", "behind", "before", "near",
    "around", "about", "over", "across", "against",
}

# Verb synonyms
VERB_SYNONYMS: dict[str, str] = {
    "grab": "take", "catch": "take", "get": "take", "hold": "take",
    "carry": "take", "pick": "take",
    "release": "drop", "discard": "drop",
    "go": "walk", "move": "walk", "run": "walk",
    "l": "look", "describe": "look",
    "x": "examine", "inspect": "examine", "check": "examine",
    "i": "inventory", "possessions": "inventory",
    "q": "quit", "bye": "quit",
    "g": "again",
    "hit": "attack", "kill": "attack", "fight": "attack",
    "yell": "shout", "scream": "shout",
    "lie": "lie down",
    "stand": "stand up", "rise": "stand up",
    "wear": "put on",
    "remove": "take off",
    "drink": "drink",
    "eat": "eat",
    "read": "read",
    "open": "open",
    "close": "close", "shut": "close",
    "push": "push", "press": "push",
    "pull": "pull",
    "turn": "turn",
    "switch": "turn",
    "give": "give",
    "show": "show",
    "tell": "tell",
    "ask": "ask",
    "wait": "wait", "z": "wait",
    "save": "save",
    "restore": "restore",
    "score": "score",
    "verbose": "verbose",
    "brief": "brief",
    "superbrief": "superbrief",
    "diagnose": "diagnose",
    "restart": "restart",
    "swim": "swim",
    "sleep": "sleep",
    "wake": "wake",
    "enjoy": "enjoy",
    "listen": "listen",
    "smell": "smell",
    "taste": "taste",
    "touch": "touch", "feel": "touch",
    "pour": "pour",
    "fill": "fill",
    "empty": "empty",
    "answer": "answer",
    "call": "call",
    "count": "count",
    "drown": "drown",
    "pray": "pray",
    "brush": "brush",
    "swallow": "swallow",
    "consult": "consult",
    "say": "say",
    "carve": "carve", "engrave": "carve", "write": "carve", "inscribe": "carve",
    "plug": "plug",
    "unplug": "unplug", "disconnect": "unplug",
    "connect": "plug",
    "block": "block",
    "rub": "rub",
    "repair": "repair", "fix": "repair", "mend": "repair",
    "follow": "follow",
    "kick": "kick",
    "knock": "knock",
    "type": "type",
    "panic": "panic",
    "relax": "relax",
    "hang": "hang",
}


@dataclass
class ParseResult:
    verb: str
    direct_obj: GameObject | None = None
    indirect_obj: GameObject | None = None
    direction: str | None = None  # For movement commands
    prep: str | None = None  # Preposition between objects
    raw_input: str = ""
    is_negated: bool = False
    # Unresolved noun text (for disambiguation)
    direct_noun: str | None = None
    indirect_noun: str | None = None
    direct_adj: str | None = None
    indirect_adj: str | None = None


class Parser:
    """Parses player input into structured commands."""

    def __init__(self) -> None:
        self.last_result: ParseResult | None = None

    def parse(self, raw: str, state: "GameState") -> ParseResult | None:
        """Parse a raw input string into a ParseResult, or None if unparsable."""
        raw = raw.strip()
        if not raw:
            return None

        tokens = raw.lower().split()

        # Handle negation
        is_negated = False
        if tokens[0] in ("dont", "don't"):
            is_negated = True
            tokens = tokens[1:]
            if not tokens:
                return None

        # Strip buzz words from beginning (but keep them between clauses)
        while tokens and tokens[0] in BUZZ_WORDS:
            tokens.pop(0)
        if not tokens:
            return None

        # Check for bare direction command
        if len(tokens) == 1 and tokens[0] in DIRECTION_WORDS:
            return ParseResult(
                verb="walk",
                direction=DIRECTION_WORDS[tokens[0]],
                raw_input=raw,
                is_negated=is_negated,
            )

        # Check for "go <direction>"
        if len(tokens) >= 2 and tokens[0] in ("go", "walk", "run", "move"):
            if tokens[1] in DIRECTION_WORDS:
                return ParseResult(
                    verb="walk",
                    direction=DIRECTION_WORDS[tokens[1]],
                    raw_input=raw,
                    is_negated=is_negated,
                )

        # Extract verb word (don't resolve synonyms yet — check compounds first)
        verb_word = tokens.pop(0)

        # Handle compound verbs using the RAW verb word
        verb = verb_word  # temporary, before synonym resolution
        if verb_word == "get" and tokens:
            if tokens[0] == "out":
                verb = "get out"
                tokens.pop(0)
                if tokens and tokens[0] == "of":
                    tokens.pop(0)
            elif tokens[0] == "in":
                verb = "get in"
                tokens.pop(0)
            elif tokens[0] == "up":
                verb = "stand up"
                tokens.pop(0)
            else:
                verb = VERB_SYNONYMS.get(verb_word, verb_word)
        elif verb_word == "turn" and tokens:
            if tokens[0] == "on":
                verb = "turn on"
                tokens.pop(0)
            elif tokens[0] == "off":
                verb = "turn off"
                tokens.pop(0)
            else:
                verb = VERB_SYNONYMS.get(verb_word, verb_word)
        elif verb_word == "pick" and tokens and tokens[0] == "up":
            verb = "take"
            tokens.pop(0)
        elif verb_word == "put" and tokens and tokens[0] == "on":
            verb = "put on"
            tokens.pop(0)
        elif verb_word == "lie" and tokens and tokens[0] == "down":
            verb = "lie down"
            tokens.pop(0)
        elif verb_word == "stand" and tokens and tokens[0] == "up":
            verb = "stand up"
            tokens.pop(0)
        elif verb_word == "look" and tokens:
            if tokens[0] == "at":
                verb = "examine"
                tokens.pop(0)
            elif tokens[0] in ("in", "inside"):
                verb = "look in"
                tokens.pop(0)
            elif tokens[0] == "under":
                verb = "look under"
                tokens.pop(0)
            else:
                verb = VERB_SYNONYMS.get(verb_word, verb_word)
        elif verb_word == "hang" and tokens and tokens[0] == "up":
            verb = "hang up"
            tokens.pop(0)
        elif verb_word == "consult":
            verb = "consult"
            # "consult X about Y" -> verb=consult, direct_obj=X, prep=about, indirect_obj=Y
            # The about preposition is already in PREPOSITIONS so it will split naturally
        else:
            verb = VERB_SYNONYMS.get(verb_word, verb_word)

        # Handle post-synonym compounds
        if verb == "take" and tokens and tokens[0] == "off":
            verb = "take off"
            tokens.pop(0)

        # Handle AGAIN
        if verb == "again" and self.last_result:
            return self.last_result

        # Handle IT/THEM pronoun
        for i, t in enumerate(tokens):
            if t in ("it", "them", "him", "her") and state.p_it_object:
                # Will resolve during object resolution
                pass

        # Strip remaining buzz words
        tokens = [t for t in tokens if t not in BUZZ_WORDS]

        # No object needed for some verbs
        if not tokens:
            result = ParseResult(
                verb=verb, raw_input=raw, is_negated=is_negated,
            )
            self.last_result = result
            return result

        # Split on preposition to get direct/indirect object phrases
        prep = None
        direct_tokens: list[str] = []
        indirect_tokens: list[str] = []
        in_indirect = False

        for token in tokens:
            if token in PREPOSITIONS and not in_indirect and direct_tokens:
                prep = token
                in_indirect = True
            elif in_indirect:
                indirect_tokens.append(token)
            else:
                direct_tokens.append(token)

        # Handle "in front of" as a special case
        if prep == "in" and indirect_tokens and indirect_tokens[0] == "front":
            indirect_tokens.pop(0)
            if indirect_tokens and indirect_tokens[0] == "of":
                indirect_tokens.pop(0)
            prep = "in front of"

        # Resolve nouns
        d_adj, d_noun = self._extract_noun(direct_tokens)
        i_adj, i_noun = self._extract_noun(indirect_tokens)

        # Try to resolve objects
        d_obj = self._resolve(d_noun, d_adj, state) if d_noun else None
        i_obj = self._resolve(i_noun, i_adj, state) if i_noun else None

        result = ParseResult(
            verb=verb,
            direct_obj=d_obj,
            indirect_obj=i_obj,
            prep=prep,
            raw_input=raw,
            is_negated=is_negated,
            direct_noun=d_noun,
            indirect_noun=i_noun,
            direct_adj=d_adj,
            indirect_adj=i_adj,
        )
        self.last_result = result
        return result

    def _extract_noun(self, tokens: list[str]) -> tuple[str | None, str | None]:
        """Extract adjective and noun from a token list.

        Returns (adjective, noun). The last word is the noun,
        preceding words are adjectives.
        """
        if not tokens:
            return None, None
        if len(tokens) == 1:
            return None, tokens[0]
        # Last token is noun, rest are adjectives
        return tokens[0], tokens[-1]

    def _resolve(
        self, noun: str | None, adj: str | None, state: "GameState"
    ) -> "GameObject | None":
        """Resolve a noun+adjective to a game object."""
        if noun is None:
            return None

        # Handle pronouns
        if noun in ("it", "them", "him", "her"):
            return state.p_it_object

        if state.here is None or state.protagonist is None:
            return None

        matches = state.world.find_by_name(
            noun, adj, state.here, state.protagonist,
        )

        if len(matches) == 1:
            return matches[0]
        elif len(matches) > 1:
            # Disambiguation — for now, return the first match
            # TODO: Ask "Which one?" for true disambiguation
            return matches[0]
        return None
