//
//  TutorialBattleController.cpp
//  eagle0
//
//  Created for tutorial battle system.
//  Copyright 2025 none. All rights reserved.
//

#include "TutorialBattleController.hpp"

#include <ranges>
#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"

namespace shardok {

using UnitStatusFB = net::eagle0::shardok::storage::fb::UnitStatus;
using ActionType = net::eagle0::shardok::common::ActionType;
using FleeActionProto = net::eagle0::common::FleeAction;
using ReinforcementsActionProto = net::eagle0::common::ReinforcementsAction;
using UnitKilledTriggerProto = net::eagle0::common::UnitKilledTrigger;

TutorialBattleController::TutorialBattleController()
    : enabled_(false),
      config_(),
      firedEventIds_() {}

TutorialBattleController::TutorialBattleController(const TutorialBattleConfigProto& protoConfig)
    : enabled_(protoConfig.enabled()),
      config_(protoConfig),
      firedEventIds_() {}

auto TutorialBattleController::CheckAndExecuteEvents(
        const GameStateW& state,
        const SettingsGetter& settings,
        const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults {
    if (!enabled_) { return {}; }

    TutorialEventResults combined;
    combined.actionResults.reserve(config_.events_size());

    // Check events in config order
    for (const auto& event : config_.events()) {
        // Skip already-fired events
        if (firedEventIds_.contains(event.event_id())) { continue; }

        // Check if trigger condition is met
        if (EvaluateTrigger(event.trigger(), state)) {
            // Mark as fired before executing (to prevent re-triggering)
            firedEventIds_.insert(event.event_id());

            // Execute the action
            auto eventResults = ExecuteAction(event.action(), state, settings, randomGenerator);
            combined.actionResults.insert(
                    combined.actionResults.end(),
                    eventResults.actionResults.begin(),
                    eventResults.actionResults.end());

            // Only one reinforcement placement can be pending at a time
            if (eventResults.reinforcementPlacement.has_value()) {
                combined.reinforcementPlacement = eventResults.reinforcementPlacement;
            }
        }
    }

    return combined;
}

auto TutorialBattleController::EvaluateTrigger(
        const TutorialTriggerProto& trigger,
        const GameStateW& state) const -> bool {
    switch (trigger.trigger_type_case()) {
        case TutorialTriggerProto::kAfterRound: {
            const int currentRound = state->current_round();
            return currentRound >= trigger.after_round().round_number();
        }

        case TutorialTriggerProto::kUnitsLost: {
            const auto& unitsLost = trigger.units_lost();
            const int lostCount = CountLostUnits(state, unitsLost.player_id());
            return lostCount >= unitsLost.threshold();
        }

        case TutorialTriggerProto::kDamageTaken: {
            const auto& damageTaken = trigger.damage_taken();
            const int totalDamage = GetTotalDamageTaken(state, damageTaken.player_id());
            return totalDamage >= damageTaken.threshold();
        }

        case TutorialTriggerProto::kUnitKilled: {
            return IsUnitKilled(state, trigger.unit_killed());
        }

        case TutorialTriggerProto::TRIGGER_TYPE_NOT_SET:
        default: return false;
    }
}

auto TutorialBattleController::ExecuteAction(
        const TutorialActionProto& action,
        const GameStateW& state,
        [[maybe_unused]] const SettingsGetter& settings,
        [[maybe_unused]] const std::shared_ptr<RandomGenerator>& randomGenerator) const
        -> TutorialEventResults {
    switch (action.action_type_case()) {
        case TutorialActionProto::kFlee:
            return {.actionResults = ExecuteFleeAction(action.flee(), state),
                    .reinforcementPlacement = std::nullopt};

        case TutorialActionProto::kReinforcements:
            return ExecuteReinforcementsAction(action.reinforcements(), state);

        case TutorialActionProto::ACTION_TYPE_NOT_SET:
        default: return {};
    }
}

auto TutorialBattleController::CountLostUnits(const GameStateW& state, const PlayerId playerId)
        const -> int {
    int count = 0;
    for (const auto* unit : *state->units()) {
        if (unit->player_id() != playerId) { continue; }

        // Count units that are no longer in battle (destroyed, captured, fled, retreated)
        const auto status = unit->status();
        if (status != UnitStatusFB::UnitStatus_NORMAL_UNIT &&
            status != UnitStatusFB::UnitStatus_RESERVE_UNIT &&
            status != UnitStatusFB::UnitStatus_NEVER_ENTERED_UNIT &&
            status != UnitStatusFB::UnitStatus_RESERVED_SLOT &&
            status != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) {
            count++;
        }
    }
    return count;
}

auto TutorialBattleController::GetTotalDamageTaken(const GameStateW& state, const PlayerId playerId)
        const -> int {
    int totalDamage = 0;
    for (const auto* unit : *state->units()) {
        if (unit->player_id() != playerId) { continue; }

        // Calculate damage as difference between initial and current size
        // Initial sizes are stored in config since flatbuffers don't track starting sizes
        int initialSize = 0;
        for (const auto& initial : config_.initial_sizes()) {
            if (initial.unit_id() == unit->unit_id()) {
                initialSize = initial.size();
                break;
            }
        }
        if (initialSize == 0) { continue; }

        const int currentSize = unit->battalion().size();
        if (initialSize > currentSize) { totalDamage += (initialSize - currentSize); }
    }
    return totalDamage;
}

auto TutorialBattleController::IsUnitKilled(
        const GameStateW& state,
        const UnitKilledTriggerProto& trigger) const -> bool {
    for (const auto* unit : *state->units()) {
        // Check if this unit matches the identifier
        bool matches = false;

        switch (trigger.unit_identifier_case()) {
            case UnitKilledTriggerProto::kUnitId:
                matches = (unit->unit_id() == trigger.unit_id());
                break;

            case UnitKilledTriggerProto::kEagleHeroId:
                if (unit->has_attached_hero()) {
                    matches = (unit->attached_hero().eagle_hero_id() == trigger.eagle_hero_id());
                }
                break;

            case UnitKilledTriggerProto::UNIT_IDENTIFIER_NOT_SET:
            default: continue;
        }

        if (!matches) { continue; }

        // Check if the unit is "killed" (destroyed/captured status)
        const auto status = unit->status();
        if (status == UnitStatusFB::UnitStatus_DESTROYED_SUMMONED_UNIT ||
            status == UnitStatusFB::UnitStatus_CAPTURED_UNIT) {
            return true;
        }
    }
    return false;
}

auto TutorialBattleController::ExecuteFleeAction(
        const FleeActionProto& fleeAction,
        const GameStateW& state) const -> std::vector<ActionResult> {
    std::vector<ActionResult> results;
    results.reserve(state->units()->size() + 1);
    const PlayerId fleeingPlayerId = fleeAction.player_id();

    // First, create a "tutorial enemy fled" notification
    ActionResult fleeNotification{};
    fleeNotification.set_type(ActionType::TUTORIAL_ENEMY_FLED);
    results.push_back(std::move(fleeNotification));

    // Determine which units should flee
    std::set<int32_t> specificUnitIds(fleeAction.unit_ids().begin(), fleeAction.unit_ids().end());
    std::set<int32_t> specificHeroIds(
            fleeAction.eagle_hero_ids().begin(),
            fleeAction.eagle_hero_ids().end());
    const bool fleeAllUnits = specificUnitIds.empty() && specificHeroIds.empty();

    // Find all matching units that are still active and make them flee
    for (const auto* unit : *state->units()) {
        if (unit->player_id() != fleeingPlayerId) { continue; }
        if (unit->status() != UnitStatusFB::UnitStatus_NORMAL_UNIT) { continue; }

        // Check if this unit should flee
        if (!fleeAllUnits) {
            const bool matchesUnitId = specificUnitIds.contains(unit->unit_id());
            const bool matchesHeroId =
                    unit->has_attached_hero() &&
                    specificHeroIds.contains(unit->attached_hero().eagle_hero_id());
            if (!matchesUnitId && !matchesHeroId) { continue; }
        }

        // Create flee result for this unit (100% success - guaranteed flee)
        ActionResult fleeResult{};
        fleeResult.set_type(ActionType::FLED);
        fleeResult.mutable_player()->set_value(fleeingPlayerId);
        fleeResult.mutable_actor()->set_value(unit->unit_id());

        auto* resolvedUnit = fleeResult.add_resolved_units();
        AddUnitToResolved(*resolvedUnit, *unit);
        resolvedUnit->set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT);

        results.push_back(std::move(fleeResult));
    }

    return results;
}

auto TutorialBattleController::ExecuteReinforcementsAction(
        const ReinforcementsActionProto& reinforcementsAction,
        const GameStateW& state) const -> TutorialEventResults {
    TutorialEventResults eventResults;

    if (reinforcementsAction.units().empty()) { return eventResults; }

    const PlayerId playerId = reinforcementsAction.player_id();

    // Look up starting positions from the hex map's attacker starting positions
    using Coords = net::eagle0::shardok::storage::fb::Coords;
    std::vector<Coords> availablePositions;
    const int startingPosGroupIndex = reinforcementsAction.attacker_starting_position_index();
    const auto* attackerPositions = state->hex_map()->attacker_starting_positions();
    if (startingPosGroupIndex >= 0 &&
        startingPosGroupIndex < static_cast<int>(attackerPositions->size())) {
        const auto* startingPositions = attackerPositions->Get(startingPosGroupIndex)->positions();
        availablePositions.reserve(startingPositions->size());
        for (const auto* pos : *startingPositions) {
            if (!state.GetOccupant(*pos)) { availablePositions.push_back(*pos); }
        }
    }

    // Build set of eagle hero IDs for this specific reinforcement event
    std::set<int32_t> expectedHeroIds;
    for (const auto& commonUnit : reinforcementsAction.units()) {
        if (commonUnit.has_hero()) { expectedHeroIds.insert(commonUnit.hero().eagle_hero_id()); }
    }

    // Place only the PENDING_REINFORCEMENT units that belong to this event
    ActionResult reinforcementsResult{};
    reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
    reinforcementsResult.mutable_player()->set_value(playerId);

    size_t posIndex = 0;
    for (const auto* unit : *state->units()) {
        if (unit->status() != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) { continue; }
        if (!unit->has_attached_hero() ||
            !expectedHeroIds.contains(unit->attached_hero().eagle_hero_id())) {
            continue;
        }
        if (posIndex >= availablePositions.size()) { break; }

        net::eagle0::shardok::storage::fb::Unit changedUnit = *unit;
        changedUnit.mutate_status(UnitStatusFB::UnitStatus_NORMAL_UNIT);
        changedUnit.mutable_location() = availablePositions[posIndex++];
        AddChangedUnit(reinforcementsResult, changedUnit);
    }

    eventResults.actionResults.push_back(std::move(reinforcementsResult));
    return eventResults;
}

}  // namespace shardok
