"""
Magic: The Gathering Rules Engine

This module provides the core rules engine for validating and simulating
Magic: The Gathering gameplay. It uses the hardcoded keyword database
to ensure rule compliance during gameplay.

Usage:
    from mtg_rules_engine.engine import RulesEngine
    
    engine = RulesEngine()
    
    # Validate a card's abilities
    card = {
        "name": "Garruk the Wreathshaper",
        "text": "Flying, trample. When Garruk enters, create a 1/1 green Elf creature token.",
        "type": "CREATURE",
        "power_toughness": (6, 6),
        "color": ["GREEN"],
    }
    errors = engine.validate_card(card)
    print(f"Validation errors: {errors}")
    
    # Validate a game action
    action = {
        "type": "attack",
        "attacker": "Garruk the Wreathshaper",
        "attacker_power": 6,
        "target": "Opponent's creature",
        "blocking_creatures": [{"name": "Garruk the Wreathshaper", "power": 6, "toughness": 6, "abilities": ["flying", "trample"]}],
    }
    errors = engine.validate_action(action)
    print(f"Action validation errors: {errors}")
"""

from typing import Any, Dict, List, Optional, Set, Tuple, Union

from .keywords import (
    ABILITY_WORDS,
    KEYWORD_ACTIONS,
    KEYWORD_ABILITIES,
    get_all_keywords,
    get_keyword_info,
    is_valid_keyword,
    get_keyword_type,
)

from .validator import KeywordValidator


class RulesError(Exception):
    """Base exception for rules validation errors."""
    pass


class RulesError:
    """Represents a rules validation error."""
    def __init__(self, message: str, rule: Optional[str] = None, 
                 keyword: Optional[str] = None, 
                 severity: str = "error"):
        self.message = message
        self.rule = rule
        self.keyword = keyword
        self.severity = severity  # "error", "warning", "info"
    
    def __str__(self):
        return f"{self.message}"


class RulesEngine:
    """
    Core rules engine for Magic: The Gathering.
    
    This engine validates cards, game actions, and game states
    using the hardcoded keyword database.
    """
    
    def __init__(self):
        """Initialize the rules engine with the keyword validator."""
        self.validator = KeywordValidator()
        self._all_keywords: Set[str] = get_all_keywords()
        
    # =========================================================================
    # CARD VALIDATION
    # =========================================================================
    
    def validate_card(self, card: Dict[str, Any]) -> List[Dict[str, str]]:
        """
        Validate a card's properties against the rules.
        
        Args:
            card: Dictionary containing card properties:
                - name (str): Card name
                - text (str): Rules text
                - type (str): Card type (CREATURE, INSTANT, SORCERY, ENCHANTMENT, LAND)
                - color (List[str]): Card colors
                - power_toughness (Tuple[int, int] or None): Power/toughness for creatures
                - mana_cost (str or None): Mana cost
                - abilities (List[str] or None): List of keywords
                - supertypes (List[str] or None): Supertypes (LEGENDARY, etc.)
                - subtypes (List[str] or None): Subtypes
                - other (Dict[str, Any]): Other properties
                
        Returns:
            List of validation errors (empty if no errors)
        """
        errors: List[Dict[str, str]] = []
        
        # Validate card name
        name = card.get("name", "")
        if not name:
            errors.append({"message": "Card must have a name", "rule": "201"})
        
        # Validate card type
        card_type = card.get("type", "")
        valid_types = {"CREATURE", "INSTANT", "SORCERY", "ENCHANTMENT", 
                      "LAND", "ARTIFACT", "PLANEWALKER", "BATTLE", "DUNGEON",
                      "CONSPIRACY", "SCHEME", "PHENOMENON", "ADVENTURER", "OMEN",
                      "BATTLE", "CARDFACE"}
        if card_type and card_type not in valid_types:
            errors.append({
                "message": f"Invalid card type: {card_type}",
                "rule": "205",
                "valid_types": list(valid_types)
            })
        
        # Validate creature requirements
        if card_type == "CREATURE":
            self._validate_creature(card, errors)
        
        # Validate spell requirements
        if card_type in ("INSTANT", "SORCERY"):
            self._validate_spell(card, errors)
        
        # Validate enchantment requirements
        if card_type == "ENCHANTMENT":
            self._validate_enchantment(card, errors)
        
        # Validate land requirements
        if card_type == "LAND":
            self._validate_land(card, errors)
        
        # Validate artifact requirements
        if card_type == "ARTIFACT":
            self._validate_artifact(card, errors)
        
        # Validate planeswalker requirements
        if card_type == "PLANEWALKER":
            self._validate_planeswalker(card, errors)
        
        # Validate keyword usage in text
        text = card.get("text", "")
        keyword_errors = self.validator.validate_card_text(text)
        errors.extend(keyword_errors)
        
        return errors
    
    def _validate_creature(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate creature-specific rules."""
        power_toughness = card.get("power_toughness")
        if power_toughness:
            power, toughness = power_toughness
            if power < -9:
                errors.append({"message": "Power cannot be less than -9", "rule": "208"})
            if toughness < 0:
                errors.append({"message": "Toughness cannot be negative", "rule": "208"})
            if power < 0 and toughness < 0:
                errors.append({"message": "A creature can't have both negative power and toughness", "rule": "208"})
        
        # Validate basic creature rules
        abilities = card.get("abilities", [])
        if abilities:
            for ability in abilities:
                if is_valid_keyword(ability):
                    # Check if ability is appropriate for the card type
                    ability_info = get_keyword_info(ability)
                    if ability_info:
                        keyword_type = ability_info.get("category", "unknown")
                        if keyword_type == "keyword_action":
                            # Some actions may not be appropriate as abilities
                            pass  # We'll handle this in more detail
                        elif keyword_type == "characteristic_defining":
                            # Characteristic-defining abilities need special handling
                            pass
    
    def _validate_spell(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate spell-specific rules."""
        mana_cost = card.get("mana_cost")
        if mana_cost is None:
            errors.append({"message": "Spell must have a mana cost or alternative cost", "rule": "202"})
        
        # Validate spell text for keywords
        text = card.get("text", "")
        keywords = self.validator.find_keywords_in_text(text)
        if keywords:
            for keyword in keywords:
                info = get_keyword_info(keyword)
                if info:
                    keyword_type = info.get("category", "unknown")
                    if keyword_type == "keyword_action":
                        # Check if the action is appropriate for a spell
                        pass
                    elif keyword_type == "keyword_ability":
                        # Spells can have spell abilities
                        pass
    
    def _validate_enchantment(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate enchantment-specific rules."""
        text = card.get("text", "")
        keywords = self.validator.find_keywords_in_text(text)
        
        for keyword in keywords:
            info = get_keyword_info(keyword)
            if info:
                keyword_type = info.get("category", "unknown")
                if keyword_type == "keyword_ability":
                    ability_type = info.get("type", "unknown")
                    if ability_type in ("static", "triggered", "activated"):
                        pass  # Enchantments can have these ability types
    
    def _validate_land(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate land-specific rules."""
        subtypes = card.get("subtypes", [])
        if "basic" in subtypes:
            # Basic lands can only have certain basic land types
            valid_basic_types = {"PLAINS", "ISLAND", "SWAMP", "MOUNTAIN", "FOREST"}
            if not subtypes or not any(st in valid_basic_types for st in subtypes):
                errors.append({
                    "message": "Basic land must have a valid basic land type",
                    "rule": "305",
                    "valid_types": list(valid_basic_types)
                })
    
    def _validate_artifact(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate artifact-specific rules."""
        pass  # Artifacts can have almost any ability
    
    def _validate_planeswalker(self, card: Dict[str, Any], errors: List[Dict[str, str]]):
        """Validate planeswalker-specific rules."""
        loyalty = card.get("loyalty")
        if loyalty is not None:
            if loyalty < 0 or loyalty > 27:
                errors.append({"message": "Loyalty must be between 0 and 27", "rule": "306"})
    
    # =========================================================================
    # GAME ACTION VALIDATION
    # =========================================================================
    
    def validate_action(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """
        Validate a game action for rule compliance.
        
        Args:
            action: Dictionary describing the game action:
                - type (str): Action type (attack, block, cast, activate, etc.)
                - details (Dict[str, Any]): Action-specific details
                
        Returns:
            List of validation errors (empty if no errors)
        """
        errors: List[Dict[str, str]] = []
        action_type = action.get("type", "").lower()
        
        if action_type == "attack":
            errors.extend(self._validate_attack(action))
        elif action_type == "block":
            errors.extend(self._validate_block(action))
        elif action_type == "cast":
            errors.extend(self._validate_cast(action))
        elif action_type == "activate":
            errors.extend(self._validate_activate(action))
        elif action_type == "sacrifice":
            errors.extend(self._validate_sacrifice(action))
        elif action_type == "destroy":
            errors.extend(self._validate_destroy(action))
        elif action_type == "exile":
            errors.extend(self._validate_exile(action))
        elif action_type == "discard":
            errors.extend(self._validate_discard(action))
        elif action_type == "transform":
            errors.extend(self._validate_transform(action))
        elif action_type == "convert":
            errors.extend(self._validate_convert(action))
        elif action_type == "tap":
            errors.extend(self._validate_tap(action))
        elif action_type == "untap":
            errors.extend(self._validate_untap(action))
        else:
            errors.append({
                "message": f"Unknown action type: {action_type}",
                "rule": "100",
            })
        
        return errors
    
    def _validate_attack(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate an attack action."""
        errors = []
        attacker = action.get("attacker", {})
        attacker_name = attacker.get("name", "")
        attacker_toughness = attacker.get("toughness", 0)
        attacker_power = attacker.get("power", 0)
        attacker_abilities = attacker.get("abilities", [])
        
        # Check if attacker is a creature
        if attacker.get("type") != "CREATURE":
            errors.append({"message": f"{attacker_name} is not a creature", "rule": "508"})
        
        # Check for defender ability
        if "defender" in attacker_abilities:
            errors.append({"message": f"{attacker_name} has defender and can't attack", "rule": "702.3"})
        
        # Check for landwalk
        landwalk = [a for a in attacker_abilities if a.startswith("landwalk")]
        if landwalk:
            defending_player = action.get("defending_player", {})
            controlling_land = defending_player.get("controlling_land", [])
            landwalk_type = landwalk[0]  # e.g., "islandwalk"
            land_type = landwalk_type.split("walk")[0] if "walk" in landwalk_type else landwalk_type
            if landwalk_type == "nonbasic landwalk":
                # Need nonbasic land
                has_nonbasic = any(l.get("is_basic") == False for l in controlling_land)
                if not has_nonbasic:
                    errors.append({"message": f"Need a nonbasic land to attack", "rule": "702.14"})
            elif landwalk_type == "snowwalk":
                # Need snow land
                has_snow = any(l.get("is_snow") for l in controlling_land)
                if not has_snow:
                    errors.append({"message": f"Need a snow land to attack", "rule": "702.14"})
            elif landwalk_type == "islandwalk":
                has_island = any(l.get("land_type") == "ISLAND" for l in controlling_land)
                if not has_island:
                    errors.append({"message": f"Need an Island to attack", "rule": "702.14"})
            elif landwalk_type == "mountainwalk":
                has_mountain = any(l.get("land_type") == "MOUNTAIN" for l in controlling_land)
                if not has_mountain:
                    errors.append({"message": f"Need a Mountain to attack", "rule": "702.14"})
            elif landwalk_type == "plainswalk":
                has_plains = any(l.get("land_type") == "PLAINS" for l in controlling_land)
                if not has_plains:
                    errors.append({"message": f"Need a Plains to attack", "rule": "702.14"})
            elif landwalk_type == "swampwalk":
                has_swamp = any(l.get("land_type") == "SWAMP" for l in controlling_land)
                if not has_swamp:
                    errors.append({"message": f"Need a Swamp to attack", "rule": "702.14"})
            elif landwalk_type == "forestwalk":
                has_forest = any(l.get("land_type") == "FOREST" for l in controlling_land)
                if not has_forest:
                    errors.append({"message": f"Need a Forest to attack", "rule": "702.14"})
        
        # Check for flying
        if "flying" in attacker_abilities:
            blockers = action.get("blocking_creatures", [])
            can_block = any(
                "flying" in b.get("abilities", []) or "reach" in b.get("abilities", [])
                for b in blockers
            )
            if blockers and not can_block:
                errors.append({
                    "message": f"Cannot attack because {attacker_name} has flying and can't be blocked",
                    "rule": "702.9"
                })
        
        # Check for trample
        if "trample" in attacker_abilities:
            # Trample can attack even if blocked
            pass
        
        return errors
    
    def _validate_block(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a block action."""
        errors = []
        blocker = action.get("blocker", {})
        attacker = action.get("attacker", {})
        
        blocker_abilities = blocker.get("abilities", [])
        attacker_abilities = attacker.get("abilities", [])
        
        # Check if blocker is a creature
        if blocker.get("type") != "CREATURE":
            errors.append({"message": f"{blocker.get('name', 'Creature')} is not a creature", "rule": "509"})
        
        # Check for defender
        if "defender" in blocker_abilities:
            errors.append({"message": f"{blocker.get('name', 'Creature')} has defender and can't block", "rule": "702.3"})
        
        # Check for flying
        if "flying" in attacker_abilities:
            can_block = any(
                "flying" in b.get("abilities", []) or "reach" in b.get("abilities", [])
                for b in [blocker]
            )
            if not can_block:
                errors.append({
                    "message": f"{attacker.get('name', 'Creature')} has flying and can't be blocked",
                    "rule": "702.9"
                })
        
        # Check for shadow
        if "shadow" in attacker_abilities:
            can_block = any(
                "shadow" in b.get("abilities", []) for b in [blocker]
            )
            if not can_block:
                errors.append({
                    "message": f"{attacker.get('name', 'Creature')} has shadow and can't be blocked",
                    "rule": "702.28"
                })
        
        # Check for menace
        if "menace" in attacker_abilities:
            can_block = len(action.get("blocking_creatures", [blocker])) >= 2
            if not can_block:
                errors.append({
                    "message": f"{attacker.get('name', 'Creature')} has menace and needs 2 blockers",
                    "rule": "702.111"
                })
        
        # Check for fear
        if "fear" in attacker_abilities:
            can_block = any(
                b.get("color", []) and any(c in b.get("color", []) for c in ["BLACK"])
                or "artifact" in b.get("type", [])
                for b in [blocker]
            )
            if not can_block:
                errors.append({
                    "message": f"{attacker.get('name', 'Creature')} has fear and can't be blocked",
                    "rule": "702.36"
                })
        
        return errors
    
    def _validate_cast(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a spell cast action."""
        errors = []
        spell = action.get("spell", {})
        
        # Check if spell is a valid card type
        spell_type = spell.get("type", "")
        valid_spell_types = {"INSTANT", "SORCERY", "ENCHANTMENT", "CREATURE", "LAND", "ARTIFACT", "PLANEWALKER"}
        if spell_type not in valid_spell_types:
            errors.append({"message": f"Invalid spell type: {spell_type}", "rule": "205"})
        
        # Check timing restrictions
        spell_type = spell.get("type", "")
        current_phase = action.get("current_phase", "")
        
        if spell_type == "SORCERY":
            # Sorceries can only be cast during the main phase
            valid_phases = {"PRECOMBAT_MAIN", "POSTCOMBAT_MAIN"}
            if current_phase not in valid_phases:
                errors.append({
                    "message": f"Sorceries can only be cast during the main phase, not {current_phase}",
                    "rule": "601.1"
                })
        
        if spell_type == "INSTANT":
            # Instants can be cast anytime
            pass
        
        # Check for flash
        if "flash" in spell.get("abilities", []):
            # Flash spells can be cast any time you could cast an instant
            pass
        
        # Check for haste (if creature spell)
        if spell_type == "CREATURE" and "haste" in spell.get("abilities", []):
            # Haste creatures can attack immediately
            pass
        
        return errors
    
    def _validate_activate(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate an ability activation action."""
        errors = []
        ability = action.get("ability", {})
        
        # Check for exhaustion
        if "exhaust" in ability.get("variants", []) or "exhaust" in ability.get("type", ""):
            pass  # Exhaust is checked during activation
        
        # Check for tap cost
        has_tap_cost = "{T}" in ability.get("cost", "") or "T" in ability.get("cost", "")
        if has_tap_cost:
            is_tapped = action.get("source_tapped", False)
            if is_tapped:
                errors.append({
                    "message": "Source is tapped and can't pay {T} cost",
                    "rule": "118.2"
                })
        
        # Check for sorcery restriction
        if "sorcery" in ability.get("variant", "").lower():
            current_phase = action.get("current_phase", "")
            valid_phases = {"PRECOMBAT_MAIN", "POSTCOMBAT_MAIN"}
            if current_phase not in valid_phases:
                errors.append({
                    "message": f"Sorcery ability can only be activated during the main phase",
                    "rule": "602"
                })
        
        return errors
    
    def _validate_sacrifice(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a sacrifice action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is a permanent
        if target.get("type") not in {"CREATURE", "ENCHANTMENT", "ARTIFACT", 
                                     "LAND", "PLANEWALKER", "BATTLE"}:
            errors.append({"message": "Can only sacrifice permanents", "rule": "701.21"})
        
        # Check for indestructible
        if "indestructible" in target.get("abilities", []):
            errors.append({
                "message": f"{target.get('name', 'Permanent')} has indestructible and can't be sacrificed",
                "rule": "702.12"
            })
        
        return errors
    
    def _validate_destroy(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a destroy action."""
        errors = []
        target = action.get("target", {})
        
        # Check for indestructible
        if "indestructible" in target.get("abilities", []):
            errors.append({
                "message": f"{target.get('name', 'Permanent')} has indestructible and can't be destroyed",
                "rule": "702.12"
            })
        
        return errors
    
    def _validate_exile(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate an exile action."""
        errors = []
        target = action.get("target", {})
        
        # Exile can be used on any object
        pass  # Exile is generally allowed
        
        return errors
    
    def _validate_discard(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a discard action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is in hand
        if target.get("zone") != "HAND":
            errors.append({"message": "Can only discard cards from hand", "rule": "701.9"})
        
        return errors
    
    def _validate_transform(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a transform action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is a double-faced card
        if not target.get("is_double_faced"):
            errors.append({"message": "Can only transform double-faced cards", "rule": "701.27"})
        
        return errors
    
    def _validate_convert(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a convert action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is a double-faced card
        if not target.get("is_double_faced"):
            errors.append({"message": "Can only convert double-faced cards", "rule": "701.28"})
        
        return errors
    
    def _validate_tap(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate a tap action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is tapped
        if target.get("tapped", False):
            errors.append({"message": f"{target.get('name', 'Permanent')} is already tapped", "rule": "701.26"})
        
        return errors
    
    def _validate_untap(self, action: Dict[str, Any]) -> List[Dict[str, str]]:
        """Validate an untap action."""
        errors = []
        target = action.get("target", {})
        
        # Check if target is not tapped
        if not target.get("tapped", False):
            errors.append({"message": f"{target.get('name', 'Permanent')} is already untapped", "rule": "701.26"})
        
        return errors
    
    # =========================================================================
    # GAME STATE VALIDATION
    # =========================================================================
    
    def validate_game_state(self, state: Dict[str, Any]) -> List[Dict[str, str]]:
        """
        Validate a complete game state.
        
        Args:
            state: Dictionary describing the current game state
            
        Returns:
            List of validation errors (empty if no errors)
        """
        errors: List[Dict[str, str]] = []
        
        # Validate players
        players = state.get("players", [])
        if len(players) < 2:
            errors.append({"message": "Game must have at least 2 players", "rule": "100.1"})
        
        # Validate each player's resources
        for player in players:
            hand = player.get("hand", [])
            if len(hand) > 10:
                errors.append({
                    "message": f"Player {player.get('name', 'Player')} has too many cards in hand",
                    "rule": "119.3"
                })
            
            # Validate creatures on battlefield
            creatures = player.get("creatures", [])
            for creature in creatures:
                # Check for legendary uniqueness
                name = creature.get("name", "")
                legendary = creature.get("supertypes", [])
                if "LEGENDARY" in legendary:
                    other_legendaries = [c for c in creatures if c.get("name") == name and c.get("id") != creature.get("id")]
                    if other_legendaries:
                        errors.append({
                            "message": f"Player {player.get('name', 'Player')} controls multiple legendary {name}",
                            "rule": "704.5j"
                        })
        
        # Validate zones
        zones = state.get("zones", {})
        if "stack" in zones:
            if not isinstance(zones["stack"], list):
                errors.append({"message": "Stack must be a list", "rule": "405"})
        
        return errors
    
    # =========================================================================
    # UTILITY METHODS
    # =========================================================================
    
    def get_rule_text(self, rule: str) -> str:
        """
        Get the full text of a specific rule.
        
        Args:
            rule: Rule number (e.g., "702.9")
            
        Returns:
            Rule text or empty string if not found
        """
        # In a full implementation, this would fetch from a rules database
        return f"Rule {rule}: See keywords.py for details"
    
    def get_keyword_info_summary(self, keyword: str) -> Dict[str, Any]:
        """
        Get a summary of a keyword's information.
        
        Args:
            keyword: The keyword to look up
            
        Returns:
            Summary dictionary
        """
        info = get_keyword_info(keyword)
        if info is None:
            return {"keyword": keyword, "status": "not_found"}
        
        return {
            "keyword": keyword,
            "category": info.get("category", "unknown"),
            "rule": info.get("rule", "N/A"),
            "definition": info.get("definition", "N/A"),
            "variants": info.get("variants", []),
        }
    
    def search_keywords(self, query: str) -> List[Dict[str, Any]]:
        """
        Search for keywords matching a query.
        
        Args:
            query: Search query
            
        Returns:
            List of matching keywords with info
        """
        results = []
        query_lower = query.lower()
        
        for keyword in self._all_keywords:
            if query_lower in keyword.lower():
                info = get_keyword_info(keyword)
                if info:
                    results.append({
                        "keyword": keyword,
                        "category": info.get("category", "unknown"),
                        "rule": info.get("rule", "N/A"),
                        "definition": info.get("definition", "")[:200],  # Truncated
                    })
        
        return results
