#include "features/quests/QuestFeature.h"

#include <algorithm>
#include <chrono>
#include <optional>
#include <ranges>
#include <set>
#include <string>
#include <utility>

#include "foundation/diagnostics/ExecutionPath.h"

namespace ta::features::quests
{
    QuestFeature::QuestFeature(dependencies::IQuestGateway& gateway) noexcept
        : gateway_(gateway)
    {
    }

    void QuestFeature::submit(contracts::QuestCommandBatch batch)
    {
        diagnostics_.mark_command_requested();
        if (batch.trace_id == 0)
        {
            batch.trace_id = ta::foundation::execution_path::current_or_begin(
                "Quests",
                std::string(contracts::to_string(batch.action)),
                std::to_string(batch.selection_count()) + " selected");
        }

        if (batch.empty())
        {
            ta::foundation::execution_path::fail(batch.trace_id, "QuestFeature", "no quests or conditions selected");
            return;
        }

        if (batch.action == contracts::QuestAction::SkipTask)
        {
            batch.condition_ids.clear();
            if (batch.quest_ids.empty())
            {
                ta::foundation::execution_path::fail(batch.trace_id, "QuestFeature", "skip requires whole quest selection");
                return;
            }
        }

        ta::foundation::execution_path::step(batch.trace_id, "QuestFeature", "queue command batch");
        commands_.push(std::move(batch));
    }

    std::shared_ptr<const contracts::QuestListSnapshot> QuestFeature::latest_snapshot() const noexcept
    {
        return snapshot_.latest();
    }

    void QuestFeature::reset_session() noexcept
    {
        std::set<std::uint64_t> interrupted;
        for (const auto& [_, trace_id] : complete_trace_ids_)
            interrupted.insert(trace_id);
        for (const auto& [_, trace_id] : skip_trace_ids_)
            interrupted.insert(trace_id);

        for (const std::uint64_t trace_id : interrupted)
            ta::foundation::execution_path::fail(trace_id, "QuestFeature", "raid reset before completion");

        while (std::optional<contracts::QuestCommandBatch> command = commands_.try_pop())
        {
            ta::foundation::execution_path::fail(
                command->trace_id,
                "QuestFeature",
                "raid reset before command consumption");
        }

        pending_ = contracts::PendingQuestCommands{};
        complete_trace_ids_.clear();
        skip_trace_ids_.clear();
        processing_logged_.clear();
        snapshot_.clear();
        diagnostics_.reset();
        next_process_at_ = {};
    }

    void QuestFeature::on_game_tick()
    {
        constexpr auto normal_refresh_interval = std::chrono::milliseconds(350);
        constexpr auto pending_refresh_interval = std::chrono::milliseconds(50);
        constexpr auto unavailable_retry_interval = std::chrono::milliseconds(100);

        const bool commands_consumed = drain_commands_into_pending();
        const auto now = std::chrono::steady_clock::now();
        if (!commands_consumed && now < next_process_at_)
            return;

        std::set<std::uint64_t> active_traces;
        for (const auto& [_, trace_id] : complete_trace_ids_)
            active_traces.insert(trace_id);
        for (const auto& [_, trace_id] : skip_trace_ids_)
            active_traces.insert(trace_id);

        for (const std::uint64_t trace_id : active_traces)
        {
            if (processing_logged_.insert(trace_id).second)
                ta::foundation::execution_path::step(trace_id, "IQuestGateway", "process pending quests");
        }

        dependencies::QuestProcessResult result = gateway_.process(pending_);
        if (!result.visited)
        {
            next_process_at_ = now + unavailable_retry_interval;
            return;
        }

        clear_completed(result.completed_ids);
        next_process_at_ = now + (has_pending_commands()
            ? pending_refresh_interval
            : normal_refresh_interval);

        contracts::QuestListSnapshot next_snapshot;
        next_snapshot.quests = std::move(result.snapshot);
        const auto current = snapshot_.latest();
        if (current && *current == next_snapshot)
            return;

        snapshot_.publish(std::move(next_snapshot));
    }

    bool QuestFeature::drain_commands_into_pending()
    {
        bool consumed = false;
        while (std::optional<contracts::QuestCommandBatch> command = commands_.try_pop())
        {
            consumed = true;
            diagnostics_.mark_command_consumed();
            ta::foundation::execution_path::step(
                command->trace_id,
                "QuestFeature",
                "consume command batch",
                std::to_string(command->selection_count()) + " targets");

            const bool complete = command->action == contracts::QuestAction::CompleteQuest;
            auto& trace_ids = complete ? complete_trace_ids_ : skip_trace_ids_;

            if (complete)
            {
                pending_.complete_quest.insert(command->quest_ids.begin(), command->quest_ids.end());
                for (auto& [quest_id, condition_ids] : command->condition_ids)
                {
                    if (condition_ids.empty())
                        continue;

                    auto& pending_ids = pending_.complete_conditions[quest_id];
                    pending_ids.insert(condition_ids.begin(), condition_ids.end());
                }
            }
            else
            {
                pending_.skip_task.insert(command->quest_ids.begin(), command->quest_ids.end());
            }

            for (const std::string& id : command->quest_ids)
                trace_ids[id] = command->trace_id;

            if (complete)
            {
                for (const auto& [quest_id, condition_ids] : command->condition_ids)
                {
                    if (!condition_ids.empty())
                        trace_ids[quest_id] = command->trace_id;
                }
            }

            ta::foundation::execution_path::step(command->trace_id, "QuestFeature", "pending set updated");
        }
        return consumed;
    }

    bool QuestFeature::has_pending_commands() const noexcept
    {
        return !pending_.complete_quest.empty() ||
               !pending_.skip_task.empty() ||
               !pending_.complete_conditions.empty();
    }

    void QuestFeature::clear_completed(const std::set<std::string>& completed_ids)
    {
        std::set<std::uint64_t> affected_traces;

        for (const auto& id : completed_ids)
        {
            if (const auto found = complete_trace_ids_.find(id); found != complete_trace_ids_.end())
            {
                affected_traces.insert(found->second);
                complete_trace_ids_.erase(found);
            }

            if (const auto found = skip_trace_ids_.find(id); found != skip_trace_ids_.end())
            {
                affected_traces.insert(found->second);
                skip_trace_ids_.erase(found);
            }

            pending_.complete_quest.erase(id);
            pending_.skip_task.erase(id);
            pending_.complete_conditions.erase(id);
        }

        for (const std::uint64_t trace_id : affected_traces)
        {
            const auto still_pending = [trace_id](const auto& entry)
            {
                return entry.second == trace_id;
            };

            const bool remains = std::ranges::any_of(complete_trace_ids_, still_pending) ||
                                 std::ranges::any_of(skip_trace_ids_, still_pending);
            if (!remains)
            {
                processing_logged_.erase(trace_id);
                ta::foundation::execution_path::complete(
                    trace_id,
                    "QuestFeature",
                    std::to_string(completed_ids.size()) + " quest results applied");
            }
        }
    }
}
