from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Sequence

from .data import DevelopmentCard, Resource
from .game import Game, GameConfig, Phase


class ActionType(Enum):
    PLACE_INITIAL = "place_initial"
    ROLL = "roll"
    BUILD_ROAD = "build_road"
    BUILD_SETTLEMENT = "build_settlement"
    BUILD_CITY = "build_city"
    BUY_DEVELOPMENT = "buy_development"
    PLAY_KNIGHT = "play_knight"
    PLAY_ROAD_BUILDING = "play_road_building"
    PLAY_YEAR_OF_PLENTY = "play_year_of_plenty"
    PLAY_MONOPOLY = "play_monopoly"
    TRADE_BANK = "trade_bank"
    TRADE_PLAYER = "trade_player"
    MOVE_ROBBER = "move_robber"
    DISCARD = "discard"
    END_TURN = "end_turn"


@dataclass
class Action:
    type: ActionType
    payload: Dict[str, object] = field(default_factory=dict)


def parse_resource(value: str | Resource) -> Resource:
    if isinstance(value, Resource):
        return value
    return Resource[value.upper()]


class CatanEnv:
    def __init__(self, config: GameConfig):
        self.base_config = config
        self.game = Game(config)

    def reset(self, seed: Optional[int] = None):
        config = GameConfig(
            player_names=self.base_config.player_names,
            colors=self.base_config.colors,
            seed=seed if seed is not None else self.base_config.seed,
        )
        self.game = Game(config)
        return self.observe()

    def observe(self) -> Dict[str, object]:
        return {
            "game": self.game.observation(),
            "board": self.game.board.serialize(),
        }

    def legal_actions(self) -> List[Action]:
        game = self.game
        player = game.current_player
        actions: List[Action] = []
        if game.phase in (Phase.SETUP_ROUND_ONE, Phase.SETUP_ROUND_TWO):
            actions.extend(self._setup_actions())
            return actions
        if game.pending_discards:
            for name, required in game.pending_discards.items():
                actor = game.player_by_name(name)
                actions.append(
                    Action(
                        ActionType.DISCARD,
                        {
                            "player": name,
                            "required": required,
                            "resources": dict(actor.resources),
                        },
                    )
                )
            return actions
        if game.robber_move_required:
            actions.extend(self._robber_actions())
            return actions
        playable_dev = self._playable_dev_cards()
        actions.extend(playable_dev)
        if not game.has_rolled:
            actions.append(Action(ActionType.ROLL))
            return actions
        actions.extend(self._build_actions())
        if game.dev_deck and player.can_afford("development"):
            actions.append(Action(ActionType.BUY_DEVELOPMENT))
        actions.extend(self._bank_trade_actions())
        actions.extend(self._player_trade_actions())
        actions.append(Action(ActionType.END_TURN))
        return actions

    def _setup_actions(self) -> List[Action]:
        actions: List[Action] = []
        board = self.game.board
        for corner_id, corner in board.corners.items():
            if not board.can_place_settlement(corner_id, True):
                continue
            for edge_id in corner.edges:
                edge = board.edges[edge_id]
                if edge.owner:
                    continue
                payload = {
                    "corner": board.corner_label(corner_id),
                    "road": board.edge_label(edge_id),
                }
                actions.append(Action(ActionType.PLACE_INITIAL, payload))
        return actions

    def _robber_options(self) -> List[Dict[str, object]]:
        board = self.game.board
        options = []
        for hex_id in board.hexes:
            if hex_id == board.robber_hex:
                continue
            victims = []
            for corner_id in board.hexes[hex_id].corners:
                owner = board.corners[corner_id].owner
                if owner and owner != self.game.current_player.name:
                    victims.append(owner)
            options.append({"hex": hex_id, "victims": sorted(set(victims))})
        return options

    def _robber_actions(self) -> List[Action]:
        return [Action(ActionType.MOVE_ROBBER, {"options": self._robber_options()})]

    def _playable_dev_cards(self) -> List[Action]:
        player = self.game.current_player
        cards: List[Action] = []
        available = [
            card
            for card in player.dev_cards
            if card not in player.new_dev_cards
        ]
        robber_options = self._robber_options()
        road_options = self._road_spots()
        for card in available:
            if card == DevelopmentCard.KNIGHT:
                cards.append(
                    Action(ActionType.PLAY_KNIGHT, {"options": robber_options})
                )
            elif card == DevelopmentCard.ROAD_BUILDING:
                cards.append(
                    Action(ActionType.PLAY_ROAD_BUILDING, {"edges": road_options})
                )
            elif card == DevelopmentCard.YEAR_OF_PLENTY:
                cards.append(
                    Action(
                        ActionType.PLAY_YEAR_OF_PLENTY,
                        {"bank": {res.value: amt for res, amt in self.game.bank.items()}},
                    )
                )
            elif card == DevelopmentCard.MONOPOLY:
                cards.append(
                    Action(
                        ActionType.PLAY_MONOPOLY,
                        {"resources": [res.value for res in Resource if res != Resource.DESERT]},
                    )
                )
        return cards

    def _road_spots(self) -> List[int]:
        spots: List[int] = []
        board = self.game.board
        player = self.game.current_player
        for edge_id, edge in board.edges.items():
            if edge.owner:
                continue
            if self.game._road_connection_valid(player, edge):
                spots.append(board.edge_label(edge_id))
        return spots

    def _build_actions(self) -> List[Action]:
        actions: List[Action] = []
        game = self.game
        player = game.current_player
        board = game.board
        if player.can_afford("road"):
            for edge_label in self._road_spots():
                actions.append(Action(ActionType.BUILD_ROAD, {"edge": edge_label}))
        if player.can_afford("settlement") and player.settlements_remaining() > 0:
            for corner_id in board.corners:
                if not board.can_place_settlement(corner_id, True):
                    continue
                if not game._has_adjacent_road(player, corner_id):
                    continue
                actions.append(
                    Action(
                        ActionType.BUILD_SETTLEMENT,
                        {"corner": board.corner_label(corner_id)},
                    )
                )
        if player.can_afford("city") and player.cities_remaining() > 0:
            for corner_id in list(player.settlements):
                actions.append(
                    Action(
                        ActionType.BUILD_CITY,
                        {"corner": board.corner_label(corner_id)},
                    )
                )
        return actions

    def _bank_trade_actions(self) -> List[Action]:
        actions: List[Action] = []
        player = self.game.current_player
        for give in Resource:
            if give == Resource.DESERT:
                continue
            ratio = self.game._bank_trade_ratio(player, give)
            if player.resources[give] < ratio:
                continue
            for receive in Resource:
                if receive == Resource.DESERT:
                    continue
                actions.append(
                    Action(
                        ActionType.TRADE_BANK,
                        {
                            "give": give.value,
                            "receive": receive.value,
                            "amount": 1,
                            "ratio": ratio,
                        },
                    )
                )
        return actions

    def _player_trade_actions(self) -> List[Action]:
        actions: List[Action] = []
        current = self.game.current_player
        for give in Resource:
            if give == Resource.DESERT or current.resources[give] <= 0:
                continue
            for opponent in self.game.players:
                if opponent is current:
                    continue
                for receive in Resource:
                    if (
                        receive == Resource.DESERT
                        or opponent.resources[receive] <= 0
                        or receive == give
                    ):
                        continue
                    actions.append(
                        Action(
                            ActionType.TRADE_PLAYER,
                            {
                                "target": opponent.name,
                                "offer": {give.value: 1},
                                "request": {receive.value: 1},
                            },
                        )
                    )
        return actions

    def step(self, action: Action):
        actor = self.game.current_player.name
        before = self.game.player_by_name(actor).victory_points()
        try:
            self._apply_action(action)
        except ValueError as exc:
            info = {"error": str(exc), "invalid": True}
            return self.observe(), -1.0, False, info
        after = self.game.player_by_name(actor).victory_points()
        reward = after - before
        return self.observe(), reward, self.game.winner is not None, {"winner": self.game.winner}

    def _apply_action(self, action: Action) -> None:
        payload = action.payload or {}
        if action.type == ActionType.PLACE_INITIAL:
            self.game.place_initial_settlement(
                corner_label=int(payload["corner"]),
                road_label=int(payload["road"]),
            )
        elif action.type == ActionType.ROLL:
            self.game.roll_dice()
        elif action.type == ActionType.BUILD_ROAD:
            self.game.build_road(int(payload["edge"]))
        elif action.type == ActionType.BUILD_SETTLEMENT:
            self.game.build_settlement(int(payload["corner"]))
        elif action.type == ActionType.BUILD_CITY:
            self.game.build_city(int(payload["corner"]))
        elif action.type == ActionType.BUY_DEVELOPMENT:
            self.game.buy_development_card()
        elif action.type == ActionType.PLAY_KNIGHT:
            self.game.play_knight(
                target_hex=int(payload["hex"]),
                victim=payload.get("victim"),
            )
        elif action.type == ActionType.PLAY_ROAD_BUILDING:
            edges = [int(e) for e in payload.get("edges", [])]
            self.game.play_road_building(edges)
        elif action.type == ActionType.PLAY_YEAR_OF_PLENTY:
            resources = [parse_resource(r) for r in payload.get("resources", [])]
            self.game.play_year_of_plenty(resources)
        elif action.type == ActionType.PLAY_MONOPOLY:
            resource = parse_resource(payload["resource"])
            self.game.play_monopoly(resource)
        elif action.type == ActionType.TRADE_BANK:
            give = parse_resource(payload["give"])
            receive = parse_resource(payload["receive"])
            amount = int(payload.get("amount", 1))
            self.game.trade_with_bank(give, receive, amount)
        elif action.type == ActionType.TRADE_PLAYER:
            offer = {parse_resource(k): int(v) for k, v in payload.get("offer", {}).items()}
            request = {
                parse_resource(k): int(v)
                for k, v in payload.get("request", {}).items()
            }
            self.game.trade_with_player(payload["target"], offer, request)
        elif action.type == ActionType.MOVE_ROBBER:
            self.game.move_robber(
                int(payload["hex"]), payload.get("victim")
            )
        elif action.type == ActionType.DISCARD:
            cards = {
                parse_resource(k): int(v) for k, v in payload["cards"].items()
            }
            player_name = payload.get("player", self.game.current_player.name)
            self.game.discard_cards(player_name, cards)
        elif action.type == ActionType.END_TURN:
            self.game.end_turn()
        else:
            raise ValueError(f"Unsupported action {action.type}")
