//
// Created by sebastian on 02.08.26.
//

#include "LudoGameMode.h"

#include <algorithm>
#include <cassert>

#include "spdlog/spdlog.h"

void ludo::LudoGameMode::sendCommand(const std::variant<MovePieceCommand, RollDiceCommand> &command) {
    std::visit([this](const auto& cmd) { handle(cmd);}, command);
}

std::vector<ludo::LudoEvent> ludo::LudoGameMode::pollEvents() {
    auto events = std::move(m_pendingEvents);
    m_pendingEvents.clear();
    return events;
}

[[nodiscard]] std::optional<ludo::LudoGameMode::Destination> ludo::LudoGameMode::computeDestination(const Piece &piece, int diceValue) const {
    switch (piece.state) {
        case PieceState::AtHome:
            return Destination{PieceState::OnTrack, getEntryOffset(piece.color)};

        case PieceState::OnTrack: {
            int distanceTraveled = (piece.position - getEntryOffset(piece.color) + kTrackLength) % kTrackLength;
            int newDistance = distanceTraveled + diceValue;
            if (newDistance <= kTrackLength - 1) {
                return Destination{PieceState::OnTrack, (piece.position + diceValue) % kTrackLength};
            }
            int homeIndex = newDistance - kTrackLength;
            if (homeIndex > kHomeStretchLength - 1) return std::nullopt; // überzählig
            return Destination{homeIndex == kHomeStretchLength - 1 ? PieceState::Finished : PieceState::InHomeStretch, homeIndex};
        }

        case PieceState::InHomeStretch: {
            int newIndex = piece.position + diceValue;
            if (newIndex > kHomeStretchLength - 1) return std::nullopt; // überzählig
            return Destination{newIndex == kHomeStretchLength - 1 ? PieceState::Finished : PieceState::InHomeStretch, newIndex};
        }

        case PieceState::Finished:
            return std::nullopt; // nie bewegbar
    }
    return std::nullopt;
}

std::optional<int> ludo::LudoGameMode::findLowestHomeSlotPiece(PlayerColor color) const {
    for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) {
        const auto& piece = m_gameState.m_pieces[i];
        if (piece.color == color && piece.state == PieceState::AtHome) {
            return i; // erste Übereinstimmung = kleinster Home-Slot, dank Erzeugungsreihenfolge
        }
    }
    return std::nullopt;
}

ludo::LudoGameMode::MoveResult ludo::LudoGameMode::movePieceInternal(int pieceIndex, int diceValue) {
    Piece& piece = m_gameState.m_pieces[pieceIndex];
    int fromPosition = piece.position;

    Destination destination = computeDestination(piece, diceValue).value();

    PieceState startState = piece.state;
    PieceState targetState = destination.state;
    piece.state = destination.state;
    piece.position = destination.position;
    MoveResult result{fromPosition, piece.position, false, -1, targetState, startState};


    if (piece.state == PieceState::OnTrack) {
        for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) {
            if (i == pieceIndex) continue;
            Piece& other = m_gameState.m_pieces[i];
            if (other.color != piece.color && other.state == PieceState::OnTrack && other.position == piece.position) {
                other.state = PieceState::AtHome;
                other.position = -1;
                result.captured = true;
                result.capturedPieceIndex = i;
                break; // dank Blockade-Regel kann max. eine gegnerische Figur auf einem Feld stehen
            }
        }
    }

    return result;
}

void ludo::LudoGameMode::autoReleasePiece(int pieceIndex) {
    auto result = movePieceInternal(pieceIndex, m_gameState.getDiceValue());
    publishEvent(PieceMovedEvent::create(pieceIndex, result.fromPosition, result.toPosition, result.captured, result.startState, result.targetState));
    if (result.captured) {
        //Todo: Determine real toPosition Index dependent on home belegung
        std::vector<int> freeHomePositions = m_gameState.getFreeHomePositions(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex));
        const int smallestFreeHomePosition = *std::ranges::min_element(freeHomePositions);
        publishEvent(PieceMovedEvent::create(result.capturedPieceIndex, result.fromPosition, smallestFreeHomePosition, result.captured, result.startState, result.targetState));
    }
}

void ludo::LudoGameMode::handle(const RollDiceCommand &command) {
    if (!isCurrentPlayer(command.player) || m_gameState.m_phase != TurnPhase::AwaitingRoll) return;

    m_gameState.m_diceValue = rollDice();
    publishEvent(DiceRolledEvent(command.player, m_gameState.m_diceValue));

    if (m_gameState.m_diceValue == 6) {
        if (auto homeIndex = findLowestHomeSlotPiece(getColorOf(command.player))) {
            if (!isBlockedByOwnPiece(getColorOf(command.player), PieceState::OnTrack, getEntryOffset(getColorOf(command.player)))) {
                autoReleasePiece(*homeIndex);
                finishTurn();
                return;
            }
        }
    }

    if (getMoveablePieceIndices(getColorOf(command.player), m_gameState.m_diceValue).empty()) {
        finishTurn(); // kein gültiger zug, sofort weitergeben
    } else {
         m_gameState.m_phase = TurnPhase::AwaitingPieceSelection;
    }
}

ludo::PlayerColor ludo::LudoGameMode::getColorOf(engine::PlayerID player) const {
    const auto& order = getPlayerOrder();
    auto it = std::find(order.begin(), order.end(), player);
    assert(it != order.end() && "getColorOf mit unregistrierter PlayerID aufgerufen");
    return static_cast<PlayerColor>(std::distance(order.begin(), it));
}

void ludo::LudoGameMode::handle(const MovePieceCommand &command) {
    if (!isCurrentPlayer(command.player)) {
        spdlog::warn("MovePieceCommand abgewiesen: {} ist nicht am Zug (aktuell: {})", command.player, getCurrentPlayer());
        return;
    }
    if (m_gameState.m_phase != TurnPhase::AwaitingPieceSelection) {
        spdlog::warn("MovePieceCommand abgewiesen: falsche Phase ({})", static_cast<int>(m_gameState.m_phase));
        return;
    }

    auto movable = getMoveablePieceIndices(getColorOf(command.player), m_gameState.m_diceValue);
    if (std::find(movable.begin(), movable.end(), command.pieceIndex) == movable.end()) {
        spdlog::warn("MovePieceCommand abgewiesen: pieceIndex={} nicht in movable-Liste", command.pieceIndex);
        return;
    }
        // ungültiger Zug, abweisen
    spdlog::info("MovePieceCommand akzeptiert: pieceIndex={}", command.pieceIndex);

    auto result = movePieceInternal(command.pieceIndex, m_gameState.m_diceValue);
    publishEvent(PieceMovedEvent::create(command.pieceIndex, result.fromPosition, result.toPosition, result.captured, result.startState, result.targetState));
    if (result.captured) {
        spdlog::info("MovePieceCommand: gegnerische Figur auf {} geworfen", result.toPosition);
        auto freeHomePositions = m_gameState.getFreeHomePositions(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex));
        if (!freeHomePositions.empty()) {
            Piece capturedPiece = m_gameState.m_pieces[result.capturedPieceIndex];
            PieceMovedEvent event = PieceMovedEvent::create(result.capturedPieceIndex, capturedPiece.position, freeHomePositions.front(), true, capturedPiece.state, PieceState::AtHome);
            publishEvent(event);
        } else {
            throw std::runtime_error(
            std::format("No free home positions for captured piece {}",std::string(ludo::toString(m_gameState.getColorOfPieceIndex(result.capturedPieceIndex))))
            );
        }
    }
    finishTurn();
}

void ludo::LudoGameMode::finishTurn() {
    bool extraTurn = (m_gameState.m_diceValue == 6 && (++m_gameState.m_consecutiveSixes < 2));

    if (m_gameState.m_diceValue != 6) {
        m_gameState.m_consecutiveSixes = 0;
    }

    endTurn(extraTurn);
    m_gameState.m_phase = TurnPhase::AwaitingRoll;
    publishEvent(TurnChangedEvent{getCurrentPlayer()});
}

std::vector<int> ludo::LudoGameMode::getMoveablePieceIndices(PlayerColor color, int diceValue) const {
    std::vector<int> moveablePieceIndices;
    for (int i = 0; i < m_gameState.getPieces().size(); ++i) {
        const Piece& piece = m_gameState.getPieces()[i];
        if (piece.color != color) continue;

        Destination dest = computeDestination(piece, diceValue).value();

        // Überschuss in der Zielgeraden abfangen: computeDestination liefert bei Überschuss
        // aktuell denselben Zustand zurück, wie unten in computeDestination definiert
        if (piece.state == PieceState::InHomeStretch && dest.position > kHomeStretchLength - 1) continue;

        if (piece.state == PieceState::AtHome && diceValue != 6) {
            continue;
        }

        if (!isBlockedByOwnPiece(color, dest.state, dest.position)) {
            moveablePieceIndices.push_back(i);
        }
    }
    return moveablePieceIndices;
}

std::optional<int> ludo::LudoGameMode::wouldCapture(int pieceIndex, int diceValue) const {
    const auto& piece = m_gameState.m_pieces[pieceIndex];
    auto dest = computeDestination(piece, diceValue);
    if (dest.value().state != PieceState::OnTrack) return std::nullopt;

    for (int i = 0; i < static_cast<int>(m_gameState.m_pieces.size()); ++i) {
        if (i == pieceIndex) continue;
        const auto& other = m_gameState.m_pieces[i];
        if (other.color != piece.color && other.state == PieceState::OnTrack && other.position == dest.value().position) {
            return i;
        }
    }
    return std::nullopt;
}


int ludo::LudoGameMode::rollDice() {
    return m_diceSource->roll();
}

int ludo::LudoGameMode::getEntryOffset(PlayerColor color) {
    return static_cast<int>(color) * 10;
}

bool ludo::LudoGameMode::isBlockedByOwnPiece(PlayerColor owner, PieceState state, int position) const {
    for (const auto& piece : m_gameState.getPieces()) {
        if (piece.color == owner && piece.state == state && piece.position == position) {
            return true;
        }
    }
    return false;
}

void ludo::LudoGameMode::publishEvent(LudoEvent event) {
    m_pendingEvents.push_back(event);
    m_eventBus.publish(event);
}

namespace {
    int stateOrder(ludo::PieceState state) {
        switch (state) {
            case ludo::PieceState::AtHome:        return 0;
            case ludo::PieceState::OnTrack:       return 1;
            case ludo::PieceState::InHomeStretch: return 2;
            case ludo::PieceState::Finished:      return 3;
        }
        throw std::invalid_argument("computePath: unbekannter PieceState");
    }


}


std::vector<ludo::PathNode> ludo::LudoGameMode::computePath(PlayerColor color, PieceState startingState, int startPosition, PieceState targetState, int targetPosition) {
    auto errorContext = [&]() {
        return std::format(
            "color={}, startState={}, startPosition={}, targetState={}, targetPosition={}, "
            "kTrackLength={}, kHomeStretchLength={}",
            toString(color),
            toString(startingState),
            startPosition,
            toString(targetState),
            targetPosition,
            kTrackLength,
            ludo::kHomeStretchLength
        );
    };

    if (startingState == targetState && startPosition == targetPosition) {
        return {};
    }

    // --- Sonderfall: Figur wird geschlagen und springt direkt zurück ---
    if (targetState == PieceState::AtHome) {
        if (startingState != PieceState::OnTrack) {
            throw std::invalid_argument(
                std::format(
                    "computePath: targetState==AtHome ist nur von OnTrack aus erlaubt (Capture). {}",
                    errorContext()
                )
            );
        }

        return {
            PathNode{startingState, startPosition},
            PathNode{targetState, targetPosition}
        };
    }


    // --- Nur Vorwärtsbewegungen sind erlaubt ---
    if (stateOrder(targetState) < stateOrder(startingState)) {
        throw std::invalid_argument(
            std::format(
                "computePath: targetState liegt vor startingState (Rueckwaerts nicht unterstuetzt). "
                "stateOrder(start)={}, stateOrder(target)={}. {}",
                stateOrder(startingState),
                stateOrder(targetState),
                errorContext()
            )
        );
    }


    if (startingState == PieceState::Finished) {
        throw std::invalid_argument(
            std::format(
                "computePath: startingState ist bereits Finished, aber Ziel weicht ab. {}",
                errorContext()
            )
        );
    }


    if (startingState == PieceState::InHomeStretch &&
        targetState == PieceState::InHomeStretch &&
        targetPosition < startPosition) {

        throw std::invalid_argument(
            std::format(
                "computePath: targetPosition liegt im HomeStretch vor startPosition "
                "(Rueckwaertsbewegung). {}",
                errorContext()
            )
        );
    }


    // --- Positionsbereiche ---
    if (targetState == PieceState::OnTrack &&
        (targetPosition < 0 || targetPosition >= kTrackLength)) {

        throw std::invalid_argument(
            std::format(
                "computePath: targetPosition ausserhalb OnTrack-Bereich. "
                "Erwartet [0,{}), erhalten {}. {}",
                kTrackLength,
                targetPosition,
                errorContext()
            )
        );
    }


    if (targetState == PieceState::InHomeStretch &&
        (targetPosition < 0 || targetPosition >= kHomeStretchLength)) {

        throw std::invalid_argument(
            std::format(
                "computePath: targetPosition ausserhalb InHomeStretch-Bereich. "
                "Erwartet [0,{}), erhalten {}. {}",
                kHomeStretchLength,
                targetPosition,
                errorContext()
            )
        );
    }


    if (targetState == PieceState::Finished &&
        targetPosition != kHomeStretchLength - 1) {

        throw std::invalid_argument(
            std::format(
                "computePath: targetPosition fuer Finished ungueltig. "
                "Erwartet {}, erhalten {}. {}",
                kHomeStretchLength - 1,
                targetPosition,
                errorContext()
            )
        );
    }


    if (startingState == PieceState::OnTrack &&
        (startPosition < 0 || startPosition >= kTrackLength)) {

        throw std::invalid_argument(
            std::format(
                "computePath: startPosition ausserhalb OnTrack-Bereich. "
                "Erwartet [0,{}), erhalten {}. {}",
                kTrackLength,
                startPosition,
                errorContext()
            )
        );
    }


    if (startingState == PieceState::InHomeStretch &&
        (startPosition < 0 || startPosition >= kHomeStretchLength)) {

        throw std::invalid_argument(
            std::format(
                "computePath: startPosition ausserhalb InHomeStretch-Bereich. "
                "Erwartet [0,{}), erhalten {}. {}",
                kHomeStretchLength,
                startPosition,
                errorContext()
            )
        );
    }

    std::vector<PathNode> path;

    if (startPosition == targetPosition && startingState == targetState) return path;

    PieceState state = startingState;
    int position = startPosition;

    path.push_back({state, position});

    // Defense in depth: falls die Validierung oben eine Kombination übersieht
    // (z.B. nach zukünftigen Erweiterungen), lieber laut abstürzen als RAM fressen.
    const int maxSteps = kTrackLength + kHomeStretchLength + 4;
    int steps = 0;

    while (state != targetState || position != targetPosition) {
        if (++steps > maxSteps) {
            throw std::logic_error(
                "computePath: Iterationslimit ueberschritten - unreachable Ziel trotz Validierung");
        }

        switch (state) {
            case PieceState::AtHome:
                state = PieceState::OnTrack;
                position = getEntryOffset(color);
                break;
            case PieceState::OnTrack:
                if (position == getEntryOffset(color) && (targetState == PieceState::InHomeStretch || targetState == PieceState::Finished)) {
                    state = PieceState::InHomeStretch;
                    position = 0;
                }  else {
                    position = (position + 1) % kTrackLength;
                }
                break;
            case PieceState::InHomeStretch:
                if (targetState == PieceState::Finished && position == kHomeStretchLength - 1) {
                    state = PieceState::Finished;
                } else {
                    ++position;
                }
                break;
            case PieceState::Finished:
                break;
        }
        path.push_back({state, position});
    }
    return path;
}
