#include "BattleProcessTask.h"

#include <chrono>
#include <future>
#include <ranges>
#include <thread>
#include <utility>

#include "MaaUtils/NoWarningCV.hpp"

#include "Config/GeneralConfig.h"
#include "Config/Miscellaneous/BattleDataConfig.h"
#include "Config/Miscellaneous/CopilotConfig.h"
#include "Config/Miscellaneous/TilePack.h"
#include "Config/TaskData.h"
#include "Controller/Controller.h"
#include "MaaUtils/ImageIo.h"
#include "Task/ProcessTask.h"
#include "Utils/Algorithm.hpp"
#include "Utils/Logger.hpp"
#include "Vision/Battle/BattlefieldMatcher.h"
#include "Vision/Matcher.h"
#include "Vision/RegionOCRer.h"

using namespace asst::battle;
using namespace asst::battle::copilot;

asst::BattleProcessTask::BattleProcessTask(const AsstCallback& callback, Assistant* inst, std::string_view task_chain) :
    AbstractTask(callback, inst, task_chain),
    BattleHelper(inst)
{
}

bool asst::BattleProcessTask::_run()
{
    LogTraceFunction;
    clear();

    if (!calc_tiles_info(m_stage_name)) {
        Log.error("get stage info failed");
        return false;
    }

    if (!update_deployment(true)) {
        Log.error("update deployment failed");
        return false;
    }

    to_group();

    size_t action_size = get_combat_data().actions.size();
    for (size_t i = 0; i < action_size && !need_exit() && m_in_battle; ++i) {
        const auto& action = get_combat_data().actions.at(i);
        do_action(action, i);
    }

    if (need_to_wait_until_end()) {
        wait_until_end();
    }

    return true;
}

void asst::BattleProcessTask::clear()
{
    BattleHelper::clear();

    m_oper_in_group.clear();
    m_in_bullet_time = false;
}

bool asst::BattleProcessTask::set_stage_name(const std::string& stage_name)
{
    LogTraceFunction;

    if (!BattleHelper::set_stage_name(stage_name)) {
        json::value info = basic_info_with_what("UnsupportedLevel");
        auto& details = info["details"];
        details["level"] = stage_name;
        callback(AsstMsg::SubTaskExtraInfo, info);

        return false;
    }

    m_combat_data = Copilot.get_data();

    return true;
}

void asst::BattleProcessTask::set_wait_until_end(bool wait_until_end)
{
    m_need_to_wait_until_end = wait_until_end;
}

void
    asst::BattleProcessTask::set_formation_task_ptr(std::shared_ptr<std::unordered_map<std::string, std::string>> value)
{
    m_formation_ptr = value;
}

bool asst::BattleProcessTask::to_group()
{
    std::unordered_map<std::string, std::vector<std::string>> groups;
    // 从编队任务中获取<干员-组名>映射
    if (m_formation_ptr != nullptr) {
        for (const auto& [group, oper] : *m_formation_ptr) {
            groups.emplace(oper, std::vector<std::string> { group });
        }
    }
    // 补充剩余的干员
    for (const auto& [group_name, oper_list] : get_combat_data().groups) {
        if (groups.contains(group_name)) {
            continue;
        }
        std::vector<std::string> oper_name_list;
        std::ranges::transform(oper_list, std::back_inserter(oper_name_list), [](const auto& oper) {
            return oper.name;
        });
        groups.emplace(group_name, std::move(oper_name_list));
    }

    std::unordered_set<std::string> char_set; // 干员集合
    for (const auto& oper : m_cur_deployment_opers) {
        char_set.emplace(oper.name);
    }

    auto allocation_result = algorithm::get_char_allocation_for_each_group(groups, char_set);
    if (allocation_result.has_value()) {
        m_oper_in_group = std::move(allocation_result.allocation);
    }
    else {
        switch (allocation_result.status) {
        case algorithm::CharAllocationStatus::NoSolution:
            Log.info("get_char_allocation_for_each_group has no feasible solution");
            break;
        case algorithm::CharAllocationStatus::Overflow:
            Log.error("get_char_allocation_for_each_group overflowed while building the DLX model");
            break;
        case algorithm::CharAllocationStatus::InternalError:
            Log.error("get_char_allocation_for_each_group failed due to an internal error");
            break;
        case algorithm::CharAllocationStatus::Success:
            break;
        }

        for (const auto& [gp, names] : groups) {
            if (names.empty()) {
                continue;
            }
            m_oper_in_group.emplace(gp, names.front());
        }
    }

    std::unordered_map<std::string, std::string> ungrouped;
    const auto& grouped_view = m_oper_in_group | std::views::values;
    for (const auto& name : char_set) {
        if (std::ranges::find(grouped_view, name) != grouped_view.end()) {
            continue;
        }
        ungrouped.emplace(name, name);
    }
    m_oper_in_group.merge(ungrouped);

    for (const auto& action : m_combat_data.actions) {
        const std::string& action_name = action.name;
        if (action_name.empty() || m_oper_in_group.contains(action_name)) {
            continue;
        }
        m_oper_in_group.emplace(action_name, action_name);
    }

    for (const auto& [group_name, oper_name] : m_oper_in_group) {
        const auto& group_it = std::ranges::find_if(get_combat_data().groups, [&](const OperUsageGroup& pair) {
            return pair.first == group_name;
        });
        if (group_it == get_combat_data().groups.end()) {
            Log.warn(__FUNCTION__, "Group not found in combat data: ", group_name);
            continue;
        }
        const auto& this_group = group_it->second;
        // there is a build error on macOS
        // https://github.com/MaaAssistantArknights/MaaAssistantArknights/actions/runs/3779762713/jobs/6425284487
        const std::string& oper_name_for_lambda = oper_name;
        auto iter =
            std::ranges::find_if(this_group, [&](const auto& oper) { return oper.name == oper_name_for_lambda; });
        if (iter == this_group.end()) {
            continue;
        }
        m_skill_usage.emplace(oper_name, iter->skill_usage);
        m_skill_times.emplace(oper_name, iter->skill_times);
    }

    return true;
}

bool asst::BattleProcessTask::do_action(const battle::copilot::Action& action, size_t index)
{
    LogTraceFunction;

    notify_action(action);

    thread_local auto prev_frame_time = std::chrono::steady_clock::time_point {};
    static const auto min_frame_interval =
        std::chrono::milliseconds(Config.get_options().copilot_fight_screencap_interval);

    // prevent our program from consuming too much CPU
    if (const auto now = std::chrono::steady_clock::now(); prev_frame_time > now - min_frame_interval) [[unlikely]] {
        Log.debug("Sleeping for framerate limit");
        std::this_thread::sleep_for(min_frame_interval - (now - prev_frame_time));
    }

    if (!wait_condition(action)) {
        return false;
    }

    prev_frame_time = std::chrono::steady_clock::now();

    if (action.pre_delay > 0) {
        sleep_and_do_strategy(action.pre_delay);
        if (action.type == ActionType::Deploy) {
            update_deployment(); // 等待之后画面可能会变化, 更新下干员信息, 但若为非部署动作, 则无需更新
        }
    }

    bool ret = false;
    const std::string& name = get_name_from_group(action.name);
    const auto& location = action.location;

    switch (action.type) {
    case ActionType::Deploy:
        ret = deploy_oper(name, location, action.direction);
        if (ret) {
            m_in_bullet_time = false;
        }
        break;

    case ActionType::Retreat:
        ret = m_in_bullet_time ? click_retreat() : (location.empty() ? retreat_oper(name) : retreat_oper(location));
        if (ret) {
            m_in_bullet_time = false;
        }
        break;

    case ActionType::UseSkill:
        ret = m_in_bullet_time ? click_skill(!action.skip_if_not_ready)
                               : (location.empty() ? use_skill(name, !action.skip_if_not_ready)
                                                   : use_skill(location, !action.skip_if_not_ready));
        if (ret) {
            m_in_bullet_time = false;
        }
        break;

    case ActionType::SwitchSpeed:
        ret = speed_up();
        if (ret) {
            m_in_speedup = !m_in_speedup;
        }
        break;

    case ActionType::BulletTime:
        ret = enter_bullet_time(name, location);
        if (ret) {
            m_in_bullet_time = true;
        }
        break;

    case ActionType::SkillUsage: {
        const auto set_usage = [this](const std::string& name, SkillUsage usage, int times) {
            m_skill_usage[name] = usage;
            if (usage == SkillUsage::Times) {
                m_skill_times[name] = times;
            }
        };
        if (!location.empty()) {
            std::string drone_name;
            if (!name.empty()) {
                LogWarn << "Both name and location are set for SkillUsage action. Skip this step.";
                break;
            }
            if (auto it = m_used_tiles.find(location); it == m_used_tiles.end()) {
                LogInfo << "Tile hasn't used, register for drone" << location;
                drone_name = std::format("drone_{}_{}", location.x, location.y);
                register_deployed_oper(drone_name, location);
            }
            else {
                drone_name = it->second;
            }
            set_usage(drone_name, action.modify_usage, action.modify_times);
        }
        else {
            set_usage(name, action.modify_usage, action.modify_times);
        }
        ret = true;
        break;
    }

    case ActionType::Output:
        // DoNothing
        ret = true;
        break;

    case ActionType::MoveCamera:
        ret = move_camera(action.distance);
        break;

    case ActionType::ResetStopwatch:
        m_stopwatch_start_time = std::chrono::steady_clock::now();
        m_stopwatch_enabled = true;
        break;

    case ActionType::SkillDaemon:
        ret = wait_until_end();
        break;
    default:
        ret = do_derived_action(action, index);
        break;
    }

    sleep_and_do_strategy(action.post_delay);

    return ret;
}

const std::string& asst::BattleProcessTask::get_name_from_group(const std::string& action_name)
{
    auto iter = m_oper_in_group.find(action_name);
    if (iter != m_oper_in_group.cend()) {
        return iter->second;
    }
    m_oper_in_group.emplace(action_name, action_name);
    return m_oper_in_group.at(action_name);
}

void asst::BattleProcessTask::notify_action(const battle::copilot::Action& action)
{
    const static std::unordered_map<ActionType, std::string> ActionNames = {
        { ActionType::Deploy, "Deploy" },
        { ActionType::UseSkill, "UseSkill" },
        { ActionType::Retreat, "Retreat" },
        { ActionType::SkillDaemon, "SkillDaemon" },
        { ActionType::SwitchSpeed, "SwitchSpeed" },
        { ActionType::SkillUsage, "SkillUsage" },
        { ActionType::BulletTime, "BulletTime" },
        { ActionType::Output, "Output" },
        { ActionType::MoveCamera, "MoveCamera" },
        { ActionType::DrawCard, "DrawCard" },
        { ActionType::CheckIfStartOver, "CheckIfStartOver" },
        { ActionType::ResetStopwatch, "ResetStopwatch" },
    };

    json::value info = basic_info_with_what("CopilotAction");
    info["details"] |= json::object { { "action", ActionNames.at(action.type) },
                                      { "target", action.name },
                                      { "doc", action.doc },
                                      { "doc_color", action.doc_color },
                                      { "elapsed_time", elapsed_time() } };
    callback(AsstMsg::SubTaskExtraInfo, info);
}

bool asst::BattleProcessTask::wait_condition(const Action& action)
{
    cv::Mat image, image_prev;
    auto update_image_if_empty = [&]() {
        if (image.empty()) {
            image_prev = cv::Mat();
            image = ctrler()->get_image();
            check_in_battle(image);
        }
    };
    auto do_strategy_and_update_image = [&]() {
        do_strategic_action(image);
        image_prev = std::move(image);
        image = ctrler()->get_image();
    };

    if (action.cost_changes != 0) {
        update_image_if_empty();
        update_cost(image);
        int pre_cost = m_cost;

        while (!need_exit()) {
            update_cost(image, image_prev);
            if (action.cost_changes != 0) {
                if ((pre_cost + action.cost_changes < 0) ? (m_cost <= pre_cost + action.cost_changes)
                                                         : (m_cost >= pre_cost + action.cost_changes)) {
                    break;
                }
            }
            if (!check_in_battle(image)) {
                return false;
            }
            do_strategy_and_update_image();
        }
    }

    if (m_kills < action.kills) {
        update_image_if_empty();
        while (!need_exit() && m_kills < action.kills) {
            update_kills(image, image_prev);
            if (m_kills >= action.kills) {
                break;
            }
            if (!check_in_battle(image)) {
                return false;
            }
            do_strategy_and_update_image();
        }
    }

    if (action.costs) {
        update_image_if_empty();
        update_cost(image); // 保证 m_cost 是最新的
        while (!need_exit()) {
            update_cost(image, image_prev);
            if (m_cost >= action.costs) {
                break;
            }
            if (!check_in_battle(image)) {
                return false;
            }
            do_strategy_and_update_image();
        }
    }

    // 计算有几个干员在cd
    if (action.cooling >= 0) {
        update_image_if_empty();
        while (!need_exit()) {
            if (!update_deployment(false, image)) {
                return false;
            }
            size_t cooling_count =
                std::ranges::count_if(m_cur_deployment_opers, [](const auto& oper) -> bool { return oper.cooling; });
            if (cooling_count == static_cast<size_t>(action.cooling)) {
                break;
            }
            do_strategy_and_update_image();
        }
    }

    // 等待全局计时器
    if (action.elapsed_time > 0) {
        if (m_stopwatch_enabled) {
            update_image_if_empty();
            while (!need_exit()) {
                if (elapsed_time() >= action.elapsed_time) {
                    break;
                }
                if (!check_in_battle(image)) {
                    return false;
                }
                do_strategy_and_update_image();
            }
        }
        else {
            Log.warn(__FUNCTION__, "| Timer not enabled. Reset required before use.");
        }
    }

    // 部署干员还要额外等待费用够或 CD 转好
    if (action.type == ActionType::Deploy) {
        const std::string& name = get_name_from_group(action.name);
        update_image_if_empty();
        while (!need_exit()) {
            if (!update_deployment(false, image)) {
                return false;
            }
            if (auto iter =
                    std::ranges::find_if(m_cur_deployment_opers, [&](const auto& oper) { return oper.name == name; });
                iter != m_cur_deployment_opers.end() && iter->available) {
                break;
            }
            do_strategy_and_update_image();
        }
    }

    return true;
}

bool asst::BattleProcessTask::enter_bullet_time(const std::string& name, const Point& location)
{
    LogTraceFunction;

    bool ret = location.empty() ? click_oper_on_battlefield(name) : click_oper_on_battlefield(location);
    if (!ret) {
        ret = click_oper_on_deployment(name);
    };

    return ret;
}

void asst::BattleProcessTask::sleep_and_do_strategy(unsigned millisecond)
{
    LogTraceScope(__FUNCTION__ + std::string(" ms: ") + std::to_string(millisecond));

    using namespace std::chrono_literals;
    const auto start = std::chrono::steady_clock::now();
    const auto delay = millisecond * 1ms;

    cv::Mat image = ctrler()->get_image();
    while (!need_exit() && check_in_battle(image) && std::chrono::steady_clock::now() - start < delay) {
        do_strategic_action(image);
        std::this_thread::yield();

        image = ctrler()->get_image();
    }
}
