//
//  ShardokGameController.hpp
//  eagle0_engine
//
//  Created by Dan Crosby on 7/9/17.
//  Copyright © 2017 none. All rights reserved.
//

#ifndef EAGLE0_SHARDOK_CONTROLLER_SHARDOK_GAME_CONTROLLER_HPP
#define EAGLE0_SHARDOK_CONTROLLER_SHARDOK_GAME_CONTROLLER_HPP

#include <atomic>
#include <condition_variable>
#include <ctime>
#include <functional>
#include <map>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
#include <vector>

#include "GameUpdateReceiver.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/UnitPlacementInfo.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
#include "src/main/protobuf/net/eagle0/shardok/api/placement_command.pb.h"

namespace shardok {
using net::eagle0::shardok::api::ActionResultView;
using net::eagle0::shardok::api::AvailableCommands;

// Forward declaration
class ShardokGameController;

/// Info about a game that has ended, for notifying subscribers
struct GameOverInfo {
    std::vector<net::eagle0::shardok::common::PlayerInfo> playerInfos;
    std::vector<net::eagle0::shardok::storage::ResolvedUnit> endGameUnits;
    net::eagle0::shardok::common::GameStatus gameStatus;
};

/// Updates for a single player (includes faction ID for client routing)
struct OnePlayerUpdates {
    int32_t eagleFactionId;
    std::vector<ActionResultView> resultViews;
    std::shared_ptr<AvailableCommands> availableCommands;

    OnePlayerUpdates(
            const int32_t fid,
            const std::vector<ActionResultView>& arvs,
            const std::shared_ptr<AvailableCommands>& acs)
        : eagleFactionId(fid),
          resultViews(arvs),
          availableCommands(acs) {}
};

struct PlayerBattleProgress {
    int32_t playerId;
    int32_t troopCount;
    bool isRetreating;
};

[[nodiscard]] auto PlayerBattleProgressFrom(
        const net::eagle0::shardok::storage::fb::GameState& gameState)
        -> std::vector<PlayerBattleProgress>;

/// Result of WaitForUpdatesAndPush indicating how the wait ended
enum class WaitResult {
    GAME_OVER,     // Game ended normally
    DISCONNECTED,  // Subscriber disconnected
    BATTLE_RESET   // Tutorial battle reset (defender lost)
};

/// Interface for subscribers that receive streaming updates from a game
class StreamSubscriber {
public:
    virtual ~StreamSubscriber() = default;

    /// Called when new game updates are available
    virtual void OnUpdate(
            const std::vector<ActionResult>& mainResults,
            const std::vector<OnePlayerUpdates>& filteredResults,
            int32_t newUnfilteredCount,
            const byte_vector& currentGameState,
            int32_t currentRound,
            const std::vector<PlayerBattleProgress>& playerBattleProgress) = 0;

    /// Called when the game ends
    virtual void OnGameOver(const GameOverInfo& info) = 0;

    /// Called when a tutorial battle resets (defender lost, battle will restart)
    virtual void OnBattleReset() = 0;

    /// Returns true if this subscriber is still active and should receive updates
    [[nodiscard]] virtual auto IsActive() const -> bool = 0;
};

class ShardokGameController {
private:
    // This lock should be held any time we call into engine or modify clients.
    std::mutex masterLock{};
    std::condition_variable aiCondition{};
    bool aiThreadKeepGoing = true;

    // Fires whenever there is a new game state update.
    mutable std::condition_variable updateCondition{};

    // Stream subscribers - protected by separate lock to avoid deadlock with masterLock
    mutable std::mutex subscriberLock{};
    std::vector<std::weak_ptr<StreamSubscriber>> subscribers{};

    std::string serializedRequest;

    std::unique_ptr<ShardokEngine> engine;

    // Cached immutable data - safe to access without lock since it never changes after construction
    const GameId cachedGameId;

    std::atomic_int incomingRegistrations = 0;

    const std::string mapName;
    const std::string logFilePath;

    // AI thread error state - written once by AI thread before setting atomic flag
    std::atomic<bool> aiThreadFailed{false};
    std::string aiThreadErrorMessage;

    std::vector<std::shared_ptr<ShardokAIClient>> aiClients;
    std::thread aiThread;

    std::mutex recommendationLock{};
    std::map<std::pair<int64_t, PlayerId>, std::vector<net::eagle0::shardok::api::PlacementCommand>>
            recommendedStartingPositionsCache;

    // Each entry maps an Eagle faction id of a non-participant watcher to the
    // Shardok pids of participants whose allies list named this watcher. Used
    // when generating per-watcher filtered views in GetUpdates.
    const std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies;

    static auto MakeLogFilePath() -> std::string {
        const std::time_t timer = std::time(nullptr);
        char buf[255];
        std::strftime(buf, 255, "/tmp/playthrough_%Y_%m_%d_%H%M%S.log", std::localtime(&timer));
        return std::string(buf);
    }

    void LockedNotifyClients() const;

    auto LockedAIClientForPid(PlayerId pid) const -> std::shared_ptr<ShardokAIClient>;

    static std::vector<std::shared_ptr<ShardokAIClient>> MakeAIClients(
            const std::unique_ptr<ShardokEngine>& e);

    void DoAIThread();

public:
    ShardokGameController(
            std::unique_ptr<ShardokEngine> e,
            std::string mapName,
            std::string serializedRequest = "",
            std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies = {})
        : serializedRequest(std::move(serializedRequest)),
          engine(std::move(e)),
          cachedGameId(engine->GetGameId()),
          mapName(std::move(mapName)),
          logFilePath(MakeLogFilePath()),
          aiClients(MakeAIClients(engine)),
          aiThread(&ShardokGameController::DoAIThread, this),
          watcherAllies(std::move(watcherAllies)) {
        std::unique_lock lk(masterLock);
        aiCondition.notify_one();
    }

    ~ShardokGameController();

    auto SerializedRequest() const -> std::string { return serializedRequest; }

    void PostCommand(
            PlayerId shardokPlayerId,
            int eagleFactionId,
            int64_t token,
            int64_t commandIndex,
            std::optional<int32_t> roll);

    void PostPlacementCommands(
            PlayerId shardokPlayerId,
            int eagleFactionId,
            int64_t token,
            const std::vector<UnitPlacementInfo>& infos);

    struct AllUpdates {
        std::vector<ActionResult> mainResults;
        std::vector<OnePlayerUpdates> filteredResults;
        int32_t newUnfilteredCount;
        byte_vector currentGameState;
        int32_t currentRound = 0;
        std::vector<PlayerBattleProgress> playerBattleProgress;
    };
    auto GetUpdates(int64_t startingActionId) -> AllUpdates;
    auto GetCurrentGameStateBytes() -> byte_vector;

    auto GameIsOver() -> bool;
    auto GameOverStatus() -> net::eagle0::shardok::common::GameStatus;
    auto ResolvedPlayerInfos() -> std::vector<net::eagle0::shardok::common::PlayerInfo>;
    auto EndGameUnits() -> std::vector<net::eagle0::shardok::storage::ResolvedUnit>;

    [[nodiscard]] auto GetGameId() const -> GameId { return cachedGameId; }

    [[nodiscard]] auto GetLogFilePath() const -> std::string { return logFilePath; }

    [[nodiscard]] auto HasAIThreadError() const -> bool { return aiThreadFailed.load(); }
    [[nodiscard]] auto GetAIThreadError() const -> std::string { return aiThreadErrorMessage; }

    /// Register a subscriber to receive streaming updates for this game.
    /// The subscriber will receive updates until it becomes inactive or is unregistered.
    void RegisterSubscriber(std::shared_ptr<StreamSubscriber> subscriber);

    /// Unregister a subscriber. Safe to call even if the subscriber was never registered.
    void UnregisterSubscriber(const StreamSubscriber* subscriber);

    /// Wait for game updates, pushing them to the given subscriber.
    /// Blocks until the game ends, subscriber disconnects, or tutorial battle resets.
    auto WaitForUpdatesAndPush(
            std::shared_ptr<StreamSubscriber> subscriber,
            int64_t startingActionId) -> WaitResult;
};
}  // namespace shardok

#endif  // EAGLE0_SHARDOK_CONTROLLER_SHARDOK_GAME_CONTROLLER_HPP
