from __future__ import annotations

import random
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple

from .board import Board, Corner, Edge, create_board
from .data import (
    BUILD_COSTS,
    CornerCoord,
    DevelopmentCard,
    EdgeID,
    PortKind,
    Resource,
    VICTORY_POINTS_TO_WIN,
    DEV_CARD_DISTRIBUTION,
)
from .player import Player


class Phase(Enum):
    SETUP_ROUND_ONE = "setup_round_one"
    SETUP_ROUND_TWO = "setup_round_two"
    MAIN = "main"


@dataclass
class GameConfig:
    player_names: Sequence[str]
    colors: Optional[Sequence[str]] = None
    seed: Optional[int] = None

    def __post_init__(self) -> None:
        if not (3 <= len(self.player_names) <= 4):
            raise ValueError("Catan base game supports exactly 3-4 players.")


@dataclass
class TurnLog:
    player: str
    action: str
    details: Dict[str, object] = field(default_factory=dict)


def initial_bank() -> Dict[Resource, int]:
    bank = {resource: 19 for resource in Resource if resource != Resource.DESERT}
    return bank


def build_dev_deck(rng: random.Random) -> List[DevelopmentCard]:
    deck: List[DevelopmentCard] = []
    for card, amount in DEV_CARD_DISTRIBUTION.items():
        deck.extend([card] * amount)
    rng.shuffle(deck)
    return deck


class Game:
    def __init__(self, config: GameConfig):
        self.config = config
        self.rng = random.Random(config.seed)
        self.board = create_board(seed=config.seed)
        self.players: List[Player] = []
        self._init_players()
        self.bank = initial_bank()
        self.dev_deck = build_dev_deck(self.rng)
        self.phase = Phase.SETUP_ROUND_ONE
        self.setup_round_index = 0
        self.setup_progress = 0
        self.turn_order_rounds = [
            list(range(len(self.players))),
            list(reversed(range(len(self.players)))),
        ]
        self.current_player_index = self.turn_order_rounds[0][0]
        self.has_rolled = False
        self.pending_discards: Dict[str, int] = {}
        self.robber_move_required = False
        self.last_roll: Optional[int] = None
        self.history: List[TurnLog] = []
        self.winner: Optional[str] = None
        self.dev_card_played = False

    def _init_players(self) -> None:
        default_colors = ["red", "blue", "orange", "white"]
        colors = list(self.config.colors or default_colors)
        if len(colors) < len(self.config.player_names):
            colors.extend(["player"] * (len(self.config.player_names) - len(colors)))
        for name, color in zip(self.config.player_names, colors):
            self.players.append(Player(name=name, color=color))

    @property
    def current_player(self) -> Player:
        return self.players[self.current_player_index]

    def player_by_name(self, name: str) -> Player:
        for player in self.players:
            if player.name == name:
                return player
        raise KeyError(name)

    def place_initial_settlement(self, corner_label: int, road_label: int) -> None:
        if self.phase not in (Phase.SETUP_ROUND_ONE, Phase.SETUP_ROUND_TWO):
            raise ValueError("Initial placement only during setup.")
        player = self.current_player
        corner_id = self.board.corner_from_label(corner_label)
        road_id = self.board.edge_from_label(road_label)
        self._build_settlement(player, corner_id, require_connection=False)
        self._build_road(
            player,
            road_id,
            must_touch_corner=corner_id,
        )
        if self.phase == Phase.SETUP_ROUND_TWO:
            self._grant_initial_resources(player, corner_id)
        self._advance_setup_turn()

    def _advance_setup_turn(self) -> None:
        sequence = self.turn_order_rounds[self.setup_round_index]
        self.setup_progress += 1
        if self.setup_progress >= len(sequence):
            if self.phase == Phase.SETUP_ROUND_ONE:
                self.phase = Phase.SETUP_ROUND_TWO
                self.setup_round_index = 1
                self.setup_progress = 0
                self.current_player_index = self.turn_order_rounds[1][0]
            else:
                self.phase = Phase.MAIN
                self.current_player_index = 0
                self.setup_progress = 0
                self.has_rolled = False
            return
        self.current_player_index = sequence[self.setup_progress]

    def roll_dice(self) -> int:
        if self.phase != Phase.MAIN:
            raise ValueError("Cannot roll dice during setup.")
        if self.has_rolled:
            raise ValueError("Dice already rolled.")
        roll = self.rng.randint(1, 6) + self.rng.randint(1, 6)
        self.last_roll = roll
        if roll == 7:
            self._trigger_robber()
        else:
            self._distribute_resources(roll)
        self.has_rolled = True
        self.history.append(
            TurnLog(player=self.current_player.name, action="roll", details={"value": roll})
        )
        return roll

    def _trigger_robber(self) -> None:
        self.pending_discards = {}
        for player in self.players:
            total = player.total_resources()
            if total > 7:
                self.pending_discards[player.name] = total // 2
        self.robber_move_required = True

    def discard_cards(self, player_name: str, losses: Dict[Resource, int]) -> None:
        if player_name not in self.pending_discards:
            raise ValueError("Player is not required to discard.")
        required = self.pending_discards[player_name]
        if sum(losses.values()) != required:
            raise ValueError("Incorrect discard amount.")
        player = self.player_by_name(player_name)
        for resource, amount in losses.items():
            if player.resources[resource] < amount:
                raise ValueError("Player lacks resources to discard.")
        for resource, amount in losses.items():
            player.resources[resource] -= amount
            self.bank[resource] += amount
        del self.pending_discards[player_name]

    def move_robber(self, target_hex: int, victim: Optional[str] = None) -> None:
        if not self.robber_move_required or self.pending_discards:
            raise ValueError("Cannot move robber yet.")
        if target_hex == self.board.robber_hex:
            raise ValueError("Robber must move to a new hex.")
        if target_hex not in self.board.hexes:
            raise ValueError("Invalid hex.")
        tile = self.board.hexes[target_hex]
        adjacent_owners = {
            self.board.corners[corner_id].owner
            for corner_id in tile.corners
            if self.board.corners[corner_id].owner
            and self.board.corners[corner_id].owner != self.current_player.name
        }
        if adjacent_owners:
            if not victim:
                raise ValueError("Must choose a victim to steal from.")
            if victim not in adjacent_owners:
                raise ValueError("Victim has no adjacent buildings.")
            victim_player = self.player_by_name(victim)
            stolen = self._steal_random_resource(victim_player)
            if stolen:
                self.current_player.receive_resource(stolen, 1)
                self.history.append(
                    TurnLog(
                        player=self.current_player.name,
                        action="steal",
                        details={"victim": victim_player.name, "resource": stolen.value},
                    )
                )
        elif victim:
            raise ValueError("No valid victim on the chosen hex.")
        self.board.hexes[self.board.robber_hex].has_robber = False
        self.board.robber_hex = target_hex
        tile.has_robber = True
        self.robber_move_required = False

    def _steal_random_resource(self, victim: Player) -> Optional[Resource]:
        cards = [res for res, count in victim.resources.items() for _ in range(count)]
        if not cards:
            return None
        resource = self.rng.choice(cards)
        victim.resources[resource] -= 1
        return resource

    def _distribute_resources(self, roll: int) -> None:
        demand: Dict[Resource, int] = {res: 0 for res in self.bank}
        payouts: Dict[str, Dict[Resource, int]] = {
            player.name: {res: 0 for res in self.bank} for player in self.players
        }
        for tile in self.board.hexes.values():
            if tile.number_token != roll or tile.has_robber:
                continue
            for corner_id in tile.corners:
                corner = self.board.corners[corner_id]
                if not corner.owner:
                    continue
                player = self.player_by_name(corner.owner)
                amount = 1
                if corner.building == "city":
                    amount = 2
                resource = tile.resource
                if resource == Resource.DESERT:
                    continue
                payouts[player.name][resource] += amount
                demand[resource] += amount
        for resource, amount in demand.items():
            if amount == 0:
                continue
            beneficiaries = [
                player for player in self.players if payouts[player.name][resource] > 0
            ]
            if not beneficiaries:
                continue
            available = self.bank[resource]
            if available <= 0:
                continue
            if amount > available:
                if len(beneficiaries) == 1:
                    player = beneficiaries[0]
                    gain = min(available, payouts[player.name][resource])
                    if gain:
                        player.receive_resource(resource, gain)
                        self.bank[resource] -= gain
                continue
            for player in beneficiaries:
                gain = payouts[player.name][resource]
                if gain:
                    player.receive_resource(resource, gain)
                    self.bank[resource] -= gain

    def build_settlement(self, corner_label: int) -> None:
        player = self.current_player
        corner_id = self.board.corner_from_label(corner_label)
        self._ensure_action_allowed()
        if not player.can_afford("settlement"):
            raise ValueError("Insufficient resources for settlement.")
        self._build_settlement(player, corner_id, require_connection=True)
        player.pay_cost("settlement")
        for resource, cost in BUILD_COSTS["settlement"].items():
            self.bank[resource] += cost
        self.history.append(
            TurnLog(
                player=player.name,
                action="build_settlement",
                details={"corner": corner_label},
            )
        )
        self._check_winner()

    def _ensure_action_allowed(self, require_roll: bool = True) -> None:
        if self.phase != Phase.MAIN:
            raise ValueError("Action only allowed after setup.")
        if require_roll and not self.has_rolled:
            raise ValueError("Must roll dice first.")
        if self.robber_move_required or self.pending_discards:
            raise ValueError("Resolve robber first.")

    def _build_settlement(
        self,
        player: Player,
        corner_id: CornerCoord,
        require_connection: bool,
    ) -> None:
        corner = self.board.corners[corner_id]
        if not self.board.can_place_settlement(
            corner_id, check_distance=True
        ):
            raise ValueError("Corner occupied or violates distance rule.")
        if require_connection and not self._has_adjacent_road(player, corner_id):
            raise ValueError("Settlement must connect to player's road.")
        if player.settlements_remaining() <= 0:
            raise ValueError("No settlements remaining.")
        corner.owner = player.name
        corner.building = "settlement"
        player.settlements.add(corner_id)
        self._update_longest_road()

    def _has_adjacent_road(self, player: Player, corner_id: CornerCoord) -> bool:
        corner = self.board.corners[corner_id]
        for edge_id in corner.edges:
            if edge_id in player.roads:
                return True
        return False

    def build_city(self, corner_label: int) -> None:
        player = self.current_player
        corner_id = self.board.corner_from_label(corner_label)
        self._ensure_action_allowed()
        if not player.can_afford("city"):
            raise ValueError("Insufficient resources for city.")
        corner = self.board.corners[corner_id]
        if corner.owner != player.name or corner.building != "settlement":
            raise ValueError("Must upgrade your settlement.")
        if player.cities_remaining() <= 0:
            raise ValueError("No cities remaining.")
        player.pay_cost("city")
        for resource, amount in BUILD_COSTS["city"].items():
            self.bank[resource] += amount
        corner.building = "city"
        player.settlements.remove(corner_id)
        player.cities.add(corner_id)
        self.history.append(
            TurnLog(player=player.name, action="build_city", details={"corner": corner_label})
        )
        self._check_winner()

    def build_road(self, edge_label: int) -> None:
        player = self.current_player
        edge_id = self.board.edge_from_label(edge_label)
        self._ensure_action_allowed()
        if not player.can_afford("road"):
            raise ValueError("Insufficient resources for road.")
        self._build_road(player, edge_id)
        player.pay_cost("road")
        for resource, amount in BUILD_COSTS["road"].items():
            self.bank[resource] += amount
        self.history.append(
            TurnLog(player=player.name, action="build_road", details={"edge": edge_label})
        )
        self._update_longest_road()

    def _build_road(
        self,
        player: Player,
        edge_id: EdgeID,
        must_touch_corner: Optional[CornerCoord] = None,
    ) -> None:
        edge = self.board.edges[edge_id]
        if edge.owner:
            raise ValueError("Edge already occupied.")
        if player.roads_remaining() <= 0:
            raise ValueError("No roads remaining.")
        if must_touch_corner and must_touch_corner not in edge.corners:
            raise ValueError("Road must touch the specified settlement.")
        if not self._road_connection_valid(player, edge):
            raise ValueError("Road must connect to player's network.")
        edge.owner = player.name
        player.roads.add(edge_id)
        self._update_longest_road()

    def _road_connection_valid(self, player: Player, edge: Edge) -> bool:
        for corner_id in edge.corners:
            corner = self.board.corners[corner_id]
            if corner.owner == player.name:
                return True
            if corner.owner and corner.owner != player.name:
                continue
            for neighbor_edge in corner.edges:
                neighbor = self.board.edges.get(neighbor_edge)
                if neighbor and neighbor.owner == player.name:
                    return True
        return not player.roads  # allow first road if none yet

    def buy_development_card(self) -> DevelopmentCard:
        player = self.current_player
        self._ensure_action_allowed()
        if not self.dev_deck:
            raise ValueError("No development cards remaining.")
        if not player.can_afford("development"):
            raise ValueError("Cannot afford development card.")
        player.pay_cost("development")
        for resource, amount in BUILD_COSTS["development"].items():
            self.bank[resource] += amount
        card = self.dev_deck.pop()
        player.dev_cards.append(card)
        player.new_dev_cards.append(card)
        if card == DevelopmentCard.VICTORY_POINT:
            player.hidden_points += 1
            self._check_winner()
        self.history.append(
            TurnLog(player=player.name, action="buy_development", details={"card": card.value})
        )
        return card

    def play_knight(self, target_hex: int, victim: Optional[str]) -> None:
        player = self.current_player
        self._ensure_action_allowed(require_roll=False)
        self._play_dev_card(player, DevelopmentCard.KNIGHT)
        self.robber_move_required = True
        self.move_robber(target_hex, victim)
        player.played_knights += 1
        self._update_largest_army()

    def _play_dev_card(self, player: Player, card: DevelopmentCard) -> None:
        if card not in player.dev_cards:
            raise ValueError("Card not in inventory.")
        if card in player.new_dev_cards:
            raise ValueError("Cannot play development card the turn it was purchased.")
        if card != DevelopmentCard.VICTORY_POINT:
            if self.dev_card_played:
                raise ValueError("Only one development card per turn.")
            self.dev_card_played = True
        player.dev_cards.remove(card)

    def play_road_building(self, edge_labels: Sequence[int]) -> None:
        player = self.current_player
        self._ensure_action_allowed(require_roll=False)
        if len(edge_labels) != 2:
            raise ValueError("Road building places exactly two roads.")
        self._play_dev_card(player, DevelopmentCard.ROAD_BUILDING)
        for label in edge_labels:
            edge_id = self.board.edge_from_label(label)
            self._build_road(player, edge_id)

    def play_year_of_plenty(self, resources: Sequence[Resource]) -> None:
        player = self.current_player
        self._ensure_action_allowed(require_roll=False)
        if len(resources) != 2:
            raise ValueError("Choose two resources.")
        self._play_dev_card(player, DevelopmentCard.YEAR_OF_PLENTY)
        for resource in resources:
            if self.bank[resource] <= 0:
                raise ValueError("Bank lacks requested resource.")
            player.receive_resource(resource, 1)
            self.bank[resource] -= 1

    def play_monopoly(self, resource: Resource) -> None:
        player = self.current_player
        self._ensure_action_allowed(require_roll=False)
        self._play_dev_card(player, DevelopmentCard.MONOPOLY)
        total = 0
        for target in self.players:
            if target is player:
                continue
            amount = target.resources[resource]
            if amount:
                target.resources[resource] -= amount
                total += amount
        if total:
            player.receive_resource(resource, total)

    def end_turn(self) -> None:
        if self.phase != Phase.MAIN:
            raise ValueError("Cannot end turn during setup.")
        if not self.has_rolled:
            raise ValueError("You must roll before ending your turn.")
        if self.robber_move_required or self.pending_discards:
            raise ValueError("Resolve robber before ending turn.")
        self.current_player.new_dev_cards.clear()
        self.current_player_index = (self.current_player_index + 1) % len(self.players)
        self.has_rolled = False
        self.dev_card_played = False

    def _grant_initial_resources(self, player: Player, corner_id: CornerCoord) -> None:
        corner = self.board.corners[corner_id]
        for hex_id in corner.adjacent_hexes:
            tile = self.board.hexes[hex_id]
            resource = tile.resource
            if resource == Resource.DESERT:
                continue
            if self.bank[resource] <= 0:
                continue
            player.receive_resource(resource, 1)
            self.bank[resource] -= 1

    def trade_with_bank(self, give: Resource, receive: Resource, amount: int = 1) -> None:
        player = self.current_player
        self._ensure_action_allowed()
        ratio = self._bank_trade_ratio(player, give)
        required = ratio * amount
        if player.resources[give] < required:
            raise ValueError("Insufficient resources to trade.")
        if self.bank[receive] < amount:
            raise ValueError("Bank lacks requested resource.")
        player.resources[give] -= required
        self.bank[give] += required
        player.receive_resource(receive, amount)
        self.bank[receive] -= amount

    def _bank_trade_ratio(self, player: Player, resource: Resource) -> int:
        ratio = 4
        for corner_id in player.settlements.union(player.cities):
            corner = self.board.corners[corner_id]
            if not corner.port:
                continue
            port = corner.port
            if port.kind == PortKind.THREE_FOR_ONE:
                ratio = min(ratio, 3)
            elif port.resource == resource:
                ratio = min(ratio, 2)
        return ratio

    def trade_with_player(
        self,
        target_player: str,
        offer: Dict[Resource, int],
        request: Dict[Resource, int],
    ) -> None:
        player = self.current_player
        self._ensure_action_allowed()
        other = self.player_by_name(target_player)
        if other is player:
            raise ValueError("Cannot trade with yourself.")
        if not offer or not request:
            raise ValueError("Trades must exchange resources both ways.")
        if any(amount <= 0 for amount in offer.values()) or any(
            amount <= 0 for amount in request.values()
        ):
            raise ValueError("Trade amounts must be positive.")
        offer_types = {resource for resource, amount in offer.items() if amount > 0}
        request_types = {resource for resource, amount in request.items() if amount > 0}
        if offer_types == request_types and len(offer_types) == 1:
            raise ValueError("Cannot trade matching resources.")
        for resource, amount in offer.items():
            if player.resources[resource] < amount:
                raise ValueError("Insufficient resources to offer.")
        for resource, amount in request.items():
            if other.resources[resource] < amount:
                raise ValueError("Player lacks requested resources.")
        for resource, amount in offer.items():
            player.resources[resource] -= amount
            other.resources[resource] += amount
        for resource, amount in request.items():
            other.resources[resource] -= amount
            player.resources[resource] += amount

    def _update_longest_road(self) -> None:
        longest_length = 0
        longest_owner: Optional[Player] = None
        for player in self.players:
            length = self._longest_road_length(player)
            if length > longest_length:
                longest_length = length
                longest_owner = player
            elif length == longest_length:
                longest_owner = None
        if longest_length >= 5 and longest_owner:
            for player in self.players:
                player.longest_road = player is longest_owner
        else:
            for player in self.players:
                player.longest_road = False
        if longest_owner:
            self._check_winner()

    def _longest_road_length(self, player: Player) -> int:
        adjacency = {}
        for edge_id in player.roads:
            edge = self.board.edges[edge_id]
            a, b = edge.corners
            adjacency.setdefault(a, []).append((b, edge_id))
            adjacency.setdefault(b, []).append((a, edge_id))
        longest = 0
        for start in adjacency:
            longest = max(longest, self._dfs_longest(start, adjacency, set(), player))
        return longest

    def _dfs_longest(
        self,
        node: CornerCoord,
        adjacency: Dict[CornerCoord, List[Tuple[CornerCoord, EdgeID]]],
        used_edges: Set[EdgeID],
        player: Player,
    ) -> int:
        best = 0
        for neighbor, edge_id in adjacency.get(node, []):
            if edge_id in used_edges:
                continue
            neighbor_corner = self.board.corners[neighbor]
            used_edges.add(edge_id)
            if neighbor_corner.owner and neighbor_corner.owner != player.name:
                best = max(best, 1)
            else:
                best = max(
                    best, 1 + self._dfs_longest(neighbor, adjacency, used_edges, player)
                )
            used_edges.remove(edge_id)
        return best

    def _update_largest_army(self) -> None:
        best = 0
        owner: Optional[Player] = None
        for player in self.players:
            if player.played_knights > best:
                best = player.played_knights
                owner = player
            elif player.played_knights == best:
                owner = None
        if best >= 3 and owner:
            for player in self.players:
                player.largest_army = player is owner
        else:
            for player in self.players:
                player.largest_army = False
        if owner:
            self._check_winner()

    def _check_winner(self) -> None:
        for player in self.players:
            if player.victory_points() >= VICTORY_POINTS_TO_WIN:
                self.winner = player.name
                break

    def observation(self) -> Dict[str, object]:
        return {
            "phase": self.phase.value,
            "current_player": self.current_player.name,
            "players": {
                player.name: {
                    "resources": dict(player.resources),
                    "victory_points": player.victory_points(),
                    "settlements": [self.board.corner_label(c) for c in player.settlements],
                    "cities": [self.board.corner_label(c) for c in player.cities],
                    "roads": [self.board.edge_label(e) for e in player.roads],
                    "dev_cards": len(player.dev_cards),
                    "largest_army": player.largest_army,
                    "longest_road": player.longest_road,
                    "color": player.color,
                }
                for player in self.players
            },
            "bank": {res.value: amount for res, amount in self.bank.items()},
            "pending_discards": {
                name: amount for name, amount in self.pending_discards.items()
            },
            "robber_required": self.robber_move_required,
            "last_roll": self.last_roll,
            "winner": self.winner,
        }
