package Server.Automaton;

import Server.*;
import Server.Cards.*;
import Server.Utils.LoadingCardsException;

import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Central game controller for MESOS.
 *
 * Owns the full game lifecycle:
 *
 *   SETUP              → newGame()
 *   TOTEM_PLACEMENT    → placeTotem()          — one call per player, in TurnTile order
 *   ACTION_RESOLUTION  → resolveCardAction()   — one call per pending action
 *   EXTRA_DRAW         → extraDrawAction() / skipExtraDraw()  [only if a player owns the building]
 *   EVENT_RESOLUTION   → handled automatically at end of round
 *   GAME_OVER          → endGame()
 *
 * All public methods that advance state return a boolean:
 *   true  = action accepted, state may have changed
 *   false = action rejected, state unchanged (caller should log / notify client)
 *
 * NOTE: GameState must have EXTRA_DRAW added to the enum for this class to compile:
 *   UNKNOWN, SETUP, TOTEM_PLACEMENT, ACTION_RESOLUTION, EXTRA_DRAW, EVENT_RESOLUTION, GAME_OVER
 *
 * NOTE: Tribe.endPoints() already computes end-game building bonuses internally via
 * buildingAbilitiesEndPoints(). Do NOT also call BuildingManager.endgameForSix() etc.
 * in endGame() — that would count those bonuses twice.
 */
public class Game {
    private static final Logger logger = LogManager.getLogger(Game.class);
    // -------------------------------------------------------------------------
    // Constants
    // -------------------------------------------------------------------------

    private static final int TOTAL_ROUNDS = 10;

    // -------------------------------------------------------------------------
    // Core state
    // -------------------------------------------------------------------------

    private final List<Player> players;
    private GameBoard          gameBoard;
    private int                round;
    private GameState          state;

    // -------------------------------------------------------------------------
    // Phase tracking
    // -------------------------------------------------------------------------

    /** How many players have placed their totem this round. */
    private int totemPlacedCount;

    /** Index of the offering tile (central row) currently being resolved (left to right). */
    private int currentOfferingTileIndex;

    /** Actions the current player still has to resolve. */
    private List<Symbol> pendingActions;

    /**
     * Queue of players who own the EXTRA_DRAW building and still need to
     * make their extra-draw decision this round. Ordered by turn order.
     */
    //TODO  to check it should be a single player
    private final List<Player> extraDrawQueue;

    // -------------------------------------------------------------------------
    // Constructor
    // -------------------------------------------------------------------------

    public Game(List<Player> players) {
        this.players        = new ArrayList<>(players);
        this.round          = 0;
        this.state          = GameState.SETUP;
        this.pendingActions = new ArrayList<>();
        this.extraDrawQueue = new ArrayList<>();
    }

    // =========================================================================
    // SETUP
    // =========================================================================

    /**
     * Initialises a new game: loads the deck, builds the board, deals initial
     * food, and randomises the first turn order.
     *
     * Initial food (rulebook setup step 10):
     *   Slot 1 -> 2 food,  slots 2-3 -> 3 food,  slots 4-5 -> 4 food.
     */
    public void newGame(InputStream csvStream) {
        try {
            logger.info("STARTING NEW GAME");

            CardDeck deck = new CardDeck();
            deck.setForNPlayer(csvStream, players.size());

            gameBoard = new GameBoard(Era.I, deck, players.size());
            gameBoard.initOfferingTiles(players.size());
            gameBoard.setupInitialRows(players.size());

            gameBoard.getTurnTile().setInitialOrder(players);
            dealInitialFood();

            round            = 1;
            totemPlacedCount = 0;
            state            = GameState.TOTEM_PLACEMENT;

            logger.info("Game started! Round 1.");
            logTurnOrder();

        } catch (LoadingCardsException e) {
            System.err.println("Fatal: could not load cards — " + e.getMessage());
            state = GameState.GAME_OVER;
        }
    }

    private void dealInitialFood() {
        Player[] order = gameBoard.getTurnTile().getPositions();
        for (int i = 0; i < order.length; i++) {
            int food = (i == 0) ? 2 : (i <= 2) ? 3 : 4;
            order[i].addFood(food);
            logger.info("  " + order[i].getNickname() + " starts with " + food + " food.");
        }
    }

    // =========================================================================
    // PHASE 1 — TOTEM PLACEMENT
    // =========================================================================

    /**
     * Places the player's totem on an offering tile.
     *
     * Players must place in the order defined by the TurnTile (top to bottom).
     * If the wrong player calls this, it is rejected.
     *
     * @param player    the player attempting to place
     * @param tileIndex 0-based index into the offering tile list
     * @return true if placement was accepted
     */
    public boolean placeTotem(Player player, int tileIndex) {
        if (state != GameState.TOTEM_PLACEMENT) {
            logger.info("Error: not the totem-placement phase.");
            return false;
        }

        // Enforce turn order
        Player expected = gameBoard.getTurnTile().getPositions()[totemPlacedCount];
        if (expected != player) {
            logger.info("Error: it is " + expected.getNickname()
                    + "'s turn to place, not " + player.getNickname() + ".");
            return false;
        }

        List<OfferingTile> tiles = gameBoard.getOfferingTiles();
        if (tileIndex < 0 || tileIndex >= tiles.size()) {
            logger.info("Error: tile index " + tileIndex + " out of range.");
            return false;
        }

        OfferingTile chosen = tiles.get(tileIndex);
        if (!gameBoard.placeTotem(player, chosen, gameBoard.getTurnTile())) {
            logger.info("Tile " + chosen.getLetter() + " is already occupied.");
            return false;
        }

        totemPlacedCount++;
        logger.info("player {} TOTEM PLACED ON TILE {}", player.getNickname() , chosen.getLetter());

        //        + " (" + totemPlacedCount + "/" + players.size() + ").");

        if (totemPlacedCount == players.size()) {
            logger.info("START ACTION RESOLUTION");
            startActionResolution();
        }
        return true;
    }

    // =========================================================================
    // PHASE 2 — ACTION RESOLUTION
    // =========================================================================

    private void startActionResolution() {
        state            = GameState.ACTION_RESOLUTION;
        currentOfferingTileIndex = 0;
        logger.info("All totems placed — resolving actions left to right.");
        loadNextPlayerActions();
    }

    /**
     * Scans offering tiles left to right for the next occupied one, loads that
     * player's actions, and either resolves them automatically (food tile) or
     * waits for player input (card tiles).
     *
     * Calls onAllActionsResolved() when every tile has been processed.
     */
    private void loadNextPlayerActions() {
        List<OfferingTile> tiles = gameBoard.getOfferingTiles();

        while (currentOfferingTileIndex < tiles.size()) {
            OfferingTile tile = tiles.get(currentOfferingTileIndex);

            if (tile.isEmpty()) {
                currentOfferingTileIndex++;
                continue;
            }

            Player player = tile.getOccupant();
            pendingActions = new ArrayList<>(tile.getActions());
            logger.info(" PLAYER {} PENDING_ACTIONS {} ", player.getNickname() , pendingActions );
            // --- Food tile: resolve entirely without player input ---
            if (pendingActions.get(0) == Symbol.FOOD) {

                int food = pendingActions.size();
                player.addFood(food);
                BuildingManager.bonusEndTurn(player, food); // BONUS_FOOD_ENDTURN building effect
                logger.info(player.getNickname() + " receives " + food
                        + " food from tile " + tile.getLetter() + ".");
                pendingActions.clear();
                finishPlayerTurn(player);
                currentOfferingTileIndex++;
                continue;
            }

            // --- Card tile: strip impossible actions first ---
            filterImpossibleActions(player);

            if (pendingActions.isEmpty()) {
                // All actions were impossible (both rows empty)
                finishPlayerTurn(player);
                currentOfferingTileIndex++;
                continue;
            }

            // Wait for the player to call resolveCardAction()
            logger.info("player {} pending ACTIONS {}" , player.getNickname() ,  pendingActions);
           // logger.info("It is " + player.getNickname() + "'s turn — actions: " + pendingActions);
            return;
        }

        // Every tile has been processed
        onAllActionsResolved();
    }

    /**
     * Strips actions that cannot be executed because the required row is empty.
     * Pure filter — no side effects beyond mutating pendingActions.
     */
    private void filterImpossibleActions(Player player) {

        Card ctop = gameBoard.getTopRow().stream().filter(s -> s instanceof CharacterCard  || s instanceof  BuildingCard).findFirst().orElse(null);
        Card cdown = gameBoard.getBottomRow().stream().filter(s -> s instanceof CharacterCard  || s instanceof  BuildingCard).findFirst().orElse(null);

        boolean topEmpty    = ctop == null;
        boolean bottomEmpty = cdown == null;

        if (topEmpty) {
            long removed = pendingActions.stream().filter(s -> s == Symbol.UP).count();
            pendingActions.removeIf(s -> s == Symbol.UP);
            if (removed > 0)
                logger.info("{} LOSES ACTION UP" ,player.getNickname(), removed);

        }
        if (bottomEmpty) {
            long removed = pendingActions.stream().filter(s -> s == Symbol.DOWN).count();
            pendingActions.removeIf(s -> s == Symbol.DOWN);
            if (removed > 0)
                logger.info("{} LOSES ACTION DOWN" ,player.getNickname(), removed);

        }
    }

    /**
     * Resolves one card-pick action for the current player.
     * Works for both CharacterCards and BuildingCards.
     *
     * For BuildingCards: builder discount is applied and the player must have
     * enough food. If not, the card is put back and the call is rejected — the
     * player must choose a different card.
     *
     * @param player  the acting player (must match the current tile occupant)
     * @param fromTop true = pick from the top row; false = from the bottom row
     * @param cardId  the ID of the card to take
     * @return true if the action was accepted and consumed
     */
    public ActionResult resolveCardAction(Player player, boolean fromTop, int cardId) {
        if (state != GameState.ACTION_RESOLUTION) {
            logger.info("Error: not in ACTION_RESOLUTION phase.");
            return ActionResult.failure("Error: not in ACTION_RESOLUTION phase.");

        }

        OfferingTile currentTile = gameBoard.getOfferingTiles().get(currentOfferingTileIndex);
        if (currentTile.getOccupant() != player) {
            logger.info("Error: it is not " + player.getNickname() + "'s turn to act.");
            return ActionResult.failure("Error: it is not " + player.getNickname() + "'s turn to act.");
        }

        Symbol required = fromTop ? Symbol.UP : Symbol.DOWN;
        if (!pendingActions.contains(required)) {
            logger.info("Error: no " + required + " action available for "
                    + player.getNickname() + ".");
            return ActionResult.failure("Error: no " + required + " action available for "
                    + player.getNickname() + ".");
        }

        // Find the card in the correct row
        Card card = fromTop
                ? gameBoard.takeFromTopRow(cardId)
                : gameBoard.takeFromBottomRow(cardId);

        if (card == null) {
            logger.info("Error: card " + cardId + " not found in the "
                    + (fromTop ? "top" : "bottom") + " row.");
            return ActionResult.failure("Error: card " + cardId + " not found in the "
                    + (fromTop ? "top" : "bottom") + " row.");
        }

        // Event cards can never be taken by players
        if (card instanceof EventCard) {
            putCardBack(card, fromTop);
            logger.info("Error: Event cards cannot be taken.");
            return ActionResult.failure("Error: Event cards cannot be taken.");
        }

        // Resolve the card
        if (card instanceof BuildingCard) {
            ActionResult result =resolveBuildingCard(player, (BuildingCard) card);
            if (!result.isSuccess()) {
                putCardBack(card, fromTop);
                return result;
            }
        } else {
            resolveCharacterCard(player, (CharacterCard) card);
        }

        // Consume the action
        pendingActions.remove(required);
        logger.info("player {} TOOK CARD {} --> PEDNING ACTIONS {}",player.getNickname(),  cardId, pendingActions);


        // A row may have become empty after this draw — re-check impossible actions
        if (!pendingActions.isEmpty()) {
            filterImpossibleActions(player);
        }

        // If no more actions remain, finish this player's turn
        if (pendingActions.isEmpty()) {
            finishPlayerTurn(player);
            currentOfferingTileIndex++;
            loadNextPlayerActions();
        }

        return ActionResult.ok();
    }

    /**
     * Handles building card acquisition.
     * Applies builder discount, checks affordability, deducts food, and
     * registers the building with BuildingManager.
     *
     * @return true if the acquisition succeeded
     */
    private ActionResult resolveBuildingCard(Player player, BuildingCard building) {
        int discount   = player.getPlayerTribe().buildersDiscount();
        int actualCost = Math.max(0, building.getCost() - discount);

        if (player.getFoodTokens() < actualCost) {
            logger.info(player.getNickname() + " cannot afford building "
                    + building.getCardId() + " (cost after discount: " + actualCost
                    + ", has: " + player.getFoodTokens() + " food).");
            return ActionResult.failure(player.getNickname() + " cannot afford building "
                    + building.getCardId() + " (cost after discount: " + actualCost
                    + ", has: " + player.getFoodTokens() + " food).");
        }

        player.removeFood(actualCost);
        player.getPlayerTribe().addBuilding(building);
        gameBoard.getBuildingManager().addActiveBuilding(building, player);

        logger.info(player.getNickname() + " bought building "
                + building.getCardId() + " for " + actualCost + " food.");
        return ActionResult.ok();
    }

    /**
     * Handles character card acquisition and triggers all immediate effects:
     *
     *   Hunter with leg icon (iconValue > 0):
     *     Take 1 food per Hunter currently in tribe (including this one).
     *
     *   FOOD_FOR_SIX building:
     *     Take 6 food if this draw completes a full set of all 6 character types.
     *
     *   FOOD_PER_INVENTORS building:
     *     Take 3 food if this Inventor forms a matching pair.
     *
     * The card is added to the tribe BEFORE effects fire so all counts include it.
     */
    private void resolveCharacterCard(Player player, CharacterCard character) {
        player.getPlayerTribe().addCharacter(character);

        // Hunter with leg icon: immediate food reward
        if (character.getCharacterType() == CharacterType.HUNTER
                && character.getIconValue() > 0) {
            int food = player.getPlayerTribe().huntersNumber() * character.getIconValue();
            player.addFood(food);
            logger.info(player.getNickname() + "'s hunter (leg icon) grants +"
                    + food + " food.");
        }

        // Building-triggered effects on character draw
        gameBoard.getBuildingManager().foodForSix(player, character);
        if (character.getCharacterType() == CharacterType.INVENTOR) {
            gameBoard.getBuildingManager().foodPerInventors(player, character);
        }

        logger.info(player.getNickname() + " drew "
                + character.getCharacterType() + " (id " + character.getCardId() + ").");
    }

    /**
     * Marks the current player as done with their tile actions.
     * Calls TurnTile.returnTotem(), which places them in the next available
     * slot and immediately applies the position food reward or penalty.
     */
    private void finishPlayerTurn(Player player) {
        int slot = gameBoard.getTurnTile().returnTotem(player);
        OfferingTile off = gameBoard.getOfferingTile(player);
        logger.info("player {} leaving offering {} " ,player.getNickname(), off);
        if(off!=null) off.setOccupant(null);
        logger.info("player {} left offering {}  slot {} " ,player.getNickname(), off, slot);

    }

    // =========================================================================
    // PHASE 2b — EXTRA DRAW  (EXTRA_DRAW building effect)
    // =========================================================================

    /**
     * Called when every offering tile has been resolved.
     * Checks if any player owns the EXTRA_DRAW building and, if so, enters
     * the EXTRA_DRAW phase. Otherwise proceeds directly to end of round.
     *
     * Rulebook appendix: "After resolving all actions (once all totems are back
     * on the TurnTile) and before the end-of-round phase, you may take 1
     * Character or Building card (paying its cost) from the top row."
     */
    private void onAllActionsResolved() {
        logger.info("All actions resolved.");

        extraDrawQueue.clear();
        // Iterate in turn order so extra draws happen in a consistent sequence
        for (Player p : gameBoard.getTurnTile().getPositions()) {
            if (p != null && gameBoard.getBuildingManager().extraDraw(p)) {
                extraDrawQueue.add(p);
            }
        }

        if (!extraDrawQueue.isEmpty()) {
            state = GameState.EXTRA_DRAW;
            System.out.println("Extra draw phase: "
                    + extraDrawQueue.get(0).getNickname() + " may draw first.");
        } else {
            endRound();
        }
    }

    /**
     * The player uses their EXTRA_DRAW building to take one card from the top row.
     * Building cards may also be taken, paying the cost after builder discounts.
     *
     * @param player the player taking the extra draw
     * @param cardId the ID of the card they want from the top row
     * @return true if the action was accepted
     */
    public ActionResult extraDrawAction(Player player, int cardId) {
        if (state != GameState.EXTRA_DRAW) {
            System.out.println("Error: not in EXTRA_DRAW phase.");
            return ActionResult.failure("Error: not in EXTRA_DRAW phase.");
        }
        if (extraDrawQueue.isEmpty() || extraDrawQueue.get(0) != player) {
            System.out.println("Error: it is not " + player.getNickname()
                    + "'s extra draw turn.");
            return ActionResult.failure("Error: it is not " + player.getNickname()
                    + "'s extra draw turn.");
        }

        Card card = gameBoard.takeFromTopRow(cardId);
        if (card == null) {
            System.out.println("Error: card " + cardId + " not found in top row.");
            return ActionResult.failure("Error: card " + cardId + " not found in top row.");
        }
        if (card instanceof EventCard) {
            gameBoard.getTopRow().add(card);
            System.out.println("Error: cannot take Event cards.");
            return ActionResult.failure("Error: cannot take Event cards.");
        }

        if (card instanceof BuildingCard) {
            ActionResult result = resolveBuildingCard(player, (BuildingCard) card);
            if (!result.isSuccess()) {
                gameBoard.getTopRow().add(card);
                return result;
            }
        } else {
            resolveCharacterCard(player, (CharacterCard) card);
        }

        advanceExtraDrawQueue();
        return ActionResult.ok();
    }

    /**
     * The player declines to use their EXTRA_DRAW building this round.
     *
     * @return true if accepted
     */
    public boolean skipExtraDraw(Player player) {
        if (state != GameState.EXTRA_DRAW) return false;
        if (extraDrawQueue.isEmpty() || extraDrawQueue.get(0) != player) return false;

        System.out.println(player.getNickname() + " skips extra draw.");
        advanceExtraDrawQueue();
        return true;
    }

    private void advanceExtraDrawQueue() {
        extraDrawQueue.remove(0);
        if (extraDrawQueue.isEmpty()) {
            endRound();
        } else {
            System.out.println("Next extra draw: " + extraDrawQueue.get(0).getNickname());
        }
    }

    // =========================================================================
    // END OF ROUND
    // =========================================================================

    /**
     * Runs all end-of-round steps in rulebook order:
     *
     *  Step 1 — Resolve Event cards.
     *           Normal rounds:  only bottom row events.
     *           Round 10:       events from BOTH rows; then GAME_OVER.
     *  Step 2 — Advance rows: discard bottom non-buildings, shift top
     *           non-buildings down, draw (numPlayers + 4) new cards.
     *  Step 3 — If a new Era card was revealed in step 2, trigger
     *           the Era transition (building row swap).
     *  Step 4 — Start the next round.
     */
    private void endRound() {
        state = GameState.EVENT_RESOLUTION;
        //notify( GameState.EVENT_RESOLUTION);

        logger.info("--- End of round " + round + " ---");

        boolean isFinalRound = (round == TOTAL_ROUNDS);

        // Step 1: resolve events
        resolveVisibleEvents(isFinalRound);

        if (isFinalRound) {
            state = GameState.GAME_OVER;
            notify( GameState.GAME_OVER, endGame());
            logger.info("Round 10 complete — game over!");
            return;
        }

        // Step 2: advance rows; returns true if a new Era card was revealed
        boolean eraChangeTriggered = gameBoard.advanceRows(players.size());

        // Step 3: handle Era transition if needed
        if (eraChangeTriggered) {
            gameBoard.triggerEraChange(players.size());
        }

        // Step 4: start next round
        startNewRound();
    }

    /**
     * Resolves all visible Event cards in the correct order.
     *
     * Rules:
     *  - Multiple events of different types: any order, Sustainment always last.
     *  - Two events of the same type in the same round: resolve in Era order.
     *  - Round 10: events from both rows must be resolved.
     *
     * @param bothRows true means events from both rows are included (round 10)
     */
    private void resolveVisibleEvents(boolean bothRows) {
        List<EventCard> events = bothRows
                ? gameBoard.getAllVisibleEvents()   // already sorted by Era ordinal
                : gameBoard.getVisibleEvents();     // already sorted by Era ordinal

        if (events.isEmpty()) {
            logger.info("No events to resolve this round.");
            return;
        }

        // Sustainment must always be resolved last
        List<EventCard> others       = new ArrayList<>();
        List<EventCard> sustainments = new ArrayList<>();
        for (EventCard e : events) {
            if (e.getEvent() == Event.SUSTAINMENT) sustainments.add(e);
            else                                   others.add(e);
        }

        for (EventCard e : others)       resolveOneEvent(e);
        for (EventCard e : sustainments) resolveOneEvent(e);
    }

    private void resolveOneEvent(EventCard event) {
        notify( GameState.EVENT_RESOLUTION, event.toString());
        logger.info("EVENT Resolving: " + event.getEvent() + " (Era " + event.getEra() + ")");
        EventsSolver.solveEvents(Collections.singletonList(event), players);
    }

    private void startNewRound() {

        round++;

        notify( GameState.NEXT_ROUND, "Round="+ round);
        logger.info("startNewRound:  {}", round );
        totemPlacedCount = 0;
        currentOfferingTileIndex = 0;
        pendingActions.clear();

        gameBoard.clearOfferingTiles();
        gameBoard.getTurnTile().resetTrack();

       // notify( GameState.TOTEM_PLACEMENT);
        state = GameState.TOTEM_PLACEMENT;
        logger.info("=== NEW Round " + round + " ===");
        logTurnOrder();
    }

    // =========================================================================
    // END GAME
    // =========================================================================

    /**
     * Computes the final leaderboard and returns it as a formatted string.
     *
     * Final score = prestige points accumulated during play
     *             + Tribe.endPoints() which covers: builder PP, inventor PP,
     *               artist PP, building base PP, and building end-game abilities.
     *
     * IMPORTANT: do NOT call BuildingManager.endgameForSix() /
     * endgameBonusCharacter() / endgameBonusPoints() here.
     * Tribe.endPoints() already computes those bonuses. Calling both would
     * count them twice.
     *
     * Tie-breaking rule: most food wins. Further ties share the victory.
     */
    public String endGame() {
        state = GameState.GAME_OVER;

        List<Player> ranked = new ArrayList<>(players);
        ranked.sort((a, b) -> {
            int diff = totalScore(b) - totalScore(a);
            if (diff != 0) return diff;
            return b.getFoodTokens() - a.getFoodTokens();
        });

        StringBuilder sb = new StringBuilder("=== FINAL LEADERBOARD ===\n");
        for (int i = 0; i < ranked.size(); i++) {
            Player p = ranked.get(i);
            sb.append(i + 1).append(". ")
                    .append(p.getNickname())
                    .append("  —  ").append(totalScore(p)).append(" PP")
                    .append("  (food: ").append(p.getFoodTokens()).append(")\n");
        }

        Player winner = ranked.get(0);
        sb.append("\nWINNER: ")
                .append(winner.getNickname().toUpperCase())
                .append("  with  ").append(totalScore(winner)).append(" PP!");

        return sb.toString();
    }

    /** Prestige points accumulated during play + final scoring from the Tribe engine. */
    private int totalScore(Player p) {
        return p.getPrestigePoints() + p.getPlayerTribe().endPoints();
    }

    // =========================================================================
    // HELPERS
    // =========================================================================

    private void putCardBack(Card card, boolean wasTopRow) {
        if (wasTopRow) gameBoard.getTopRow().add(card);
        else           gameBoard.getBottomRow().add(card);
    }

    private void logTurnOrder() {
        Player[] order = gameBoard.getTurnTile().getPositions();
        StringBuilder sb = new StringBuilder("Turn order: ");
        for (int i = 0; i < order.length; i++) {
            if (order[i] != null)
                sb.append(i + 1).append(". ").append(order[i].getNickname()).append("  ");
        }
        logger.info("logTurnOrder: " + sb);
    }

    // =========================================================================
    // GETTERS
    // =========================================================================

    public List<Player>  getPlayers()         { return Collections.unmodifiableList(players); }
    public GameBoard     getGameBoard()        { return gameBoard; }
    public int           getRound()            { return round; }
    public GameState     getState()            { return state; }
    public void          setState(GameState s) { this.state = s; }
    public List<Symbol>  getPendingActions()   { return Collections.unmodifiableList(pendingActions); }

    /**
     * Returns the player currently expected to act:
     *   TOTEM_PLACEMENT   — the next player who should place their totem
     *   ACTION_RESOLUTION — the player currently resolving tile actions
     *   EXTRA_DRAW        — the next player to make their extra-draw decision
     */
    public Player getCurrentPlayer() {
        switch (state) {
            case TOTEM_PLACEMENT:
                if (totemPlacedCount < players.size())
                    return gameBoard.getTurnTile().getPositions()[totemPlacedCount];
                return null;
            case ACTION_RESOLUTION:
                List<OfferingTile> tiles = gameBoard.getOfferingTiles();
                if (currentOfferingTileIndex < tiles.size())
                    return tiles.get(currentOfferingTileIndex).getOccupant();
                return null;
            case EXTRA_DRAW:
                return extraDrawQueue.isEmpty() ? null : extraDrawQueue.get(0);
            default:
                return null;
        }
    }

    @Override
    public String toString() {
        return "Game{" +
                " state=" + state +
                ",\n round=" + round +
                ",\n totemPlacedCount=" + totemPlacedCount +
                ",\n currentOfferingTileIndex=" + currentOfferingTileIndex +
                ",\n pendingActions=" + pendingActions +
                ",\n extraDrawQueue=" + extraDrawQueue +
                " \n GameBoard=" + gameBoard +
                '}';
    }


    public record GameEventNotification(GameState event, String message) {}

    public interface GameEventListener {
        void onEvent(GameEventNotification notification);
    }

    // Campo privato
    private GameEventListener eventListener;

    // Setter per il controller FX
    public void setEventListener(GameEventListener listener) {
        this.eventListener = listener;
    }

    // Metodi privati di notifica
    private void notify(GameState event, String message) {
        if (eventListener != null)
            eventListener.onEvent(new GameEventNotification(event, message));
    }

    private void notify(GameState event) {
        notify(event, null);
    }


}
