#include "application.hpp"

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <fstream>

using json = nlohmann::json;
namespace fs = std::filesystem;

Application::Application(int width, int height, int fps,
                         const std::array<std::string, CameraManager::CameraCount> &initial_sources,
                         std::string data_directory)
    : cameras_(width, height, fps), detector_(width, height), data_directory_(std::move(data_directory))
{
    fs::create_directories(data_directory_);

    auto sources = initial_sources;
    loadConfiguration(sources);
    std::string error;
    cameras_.configureSources(sources, error);
    if (!error.empty())
        addEvent("camera", error);
}

Application::~Application()
{
    shutdown();
}

std::int64_t Application::nowMillis()
{
    return std::chrono::duration_cast<std::chrono::milliseconds>(
               std::chrono::system_clock::now().time_since_epoch())
        .count();
}

void Application::loadConfiguration(std::array<std::string, CameraManager::CameraCount> &sources)
{
    const fs::path path = fs::path(data_directory_) / "config.json";
    std::ifstream input(path);
    if (!input)
        return;

    try
    {
        const json config = json::parse(input);
        const auto saved_cameras = config.value("cameras", json::array());
        for (std::size_t index = 0; index < CameraManager::CameraCount && index < saved_cameras.size(); ++index)
        {
            if (sources[index].empty())
                sources[index] = saved_cameras[index].value("source", "");
            const auto view_json = saved_cameras[index].value("view", json::object());
            CameraManager::View view{
                view_json.value("zoom", 1.0),
                view_json.value("pan_x", 0.0),
                view_json.value("pan_y", 0.0),
            };
            std::string ignored;
            cameras_.updateView(index, view, ignored);
        }
    }
    catch (...)
    {
        addEvent("config", "Kunde inte läsa sparad kamerakonfiguration");
    }
}

void Application::saveConfiguration() const
{
    try
    {
        fs::create_directories(data_directory_);
        const json camera_status = cameras_.status();
        json cameras = json::array();
        for (const auto &camera : camera_status.at("cameras"))
            cameras.push_back({{"source", camera.value("source", "")}, {"view", camera.at("view")}});

        std::ofstream output(fs::path(data_directory_) / "config.json");
        output << json({{"version", 1}, {"cameras", cameras}}).dump(2) << '\n';
    }
    catch (...)
    {
        // Configuration persistence is helpful but must never stop scoring.
    }
}

void Application::addEvent(const std::string &type, const std::string &message, const json &details)
{
    std::lock_guard<std::mutex> lock(event_mutex_);
    json event = {
        {"id", next_event_id_++},
        {"type", type},
        {"message", message},
        {"timestamp", nowMillis()},
    };
    if (!details.is_null() && !details.empty())
        event["details"] = details;
    events_.push_front(event);
    while (events_.size() > 50)
        events_.pop_back();
}

void Application::invalidateCalibration(const std::string &message)
{
    stopDetection();
    detector_.clearCalibration();
    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        calibration_status_ = CalibrationStatus{"idle", message, 0, 0};
    }
}

json Application::configureCameras(const std::array<std::string, CameraManager::CameraCount> &sources)
{
    if (calibration_running_)
        return {{"ok", false}, {"message", "Vänta tills kalibreringen är klar"}};

    invalidateCalibration("Kamerorna ändrades – kalibrera igen");
    std::string error;
    const bool ready = cameras_.configureSources(sources, error);
    saveConfiguration();
    addEvent("camera", ready ? "Tre kameror är anslutna" : error);
    return {{"ok", ready}, {"message", ready ? "Tre kameror är anslutna" : error}, {"cameras", cameras_.status()}};
}

json Application::updateCameraView(std::size_t index, const CameraManager::View &view)
{
    if (calibration_running_)
        return {{"ok", false}, {"message", "Zoom kan inte ändras under kalibrering"}};

    std::string error;
    if (!cameras_.updateView(index, view, error))
        return {{"ok", false}, {"message", error}};

    if (detector_.calibrated())
        invalidateCalibration("Zoom eller utsnitt ändrades – kalibrera igen");
    saveConfiguration();
    return {{"ok", true}, {"message", "Kamerautsikt uppdaterad"}, {"cameras", cameras_.status()}};
}

json Application::availableCameraSources() const
{
    return {{"sources", cameras_.availableSources()}, {"configured", cameras_.status()}};
}

cv::Mat Application::cameraSnapshot(std::size_t index, bool overlay) const
{
    cv::Mat image = cameras_.frame(index, true);
    if (overlay && detector_.calibrated())
        image = detector_.drawOverlay(index, image);
    return image;
}

json Application::startCalibration()
{
    if (calibration_running_)
        return {{"ok", false}, {"message", "Kalibrering pågår redan"}};
    if (!cameras_.ready())
        return {{"ok", false}, {"message", "Tre fungerande kameror krävs före kalibrering"}};

    stopDetection();
    if (calibration_thread_.joinable())
        calibration_thread_.join();

    calibration_running_ = true;
    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        calibration_status_ = CalibrationStatus{"capturing", "Samlar stabila bilder från kamerorna", nowMillis(), 0};
    }
    addEvent("calibration", "Kalibrering startad");
    calibration_thread_ = std::thread(&Application::calibrationWorker, this);
    return {{"ok", true}, {"message", "Kalibrering startad"}};
}

void Application::calibrationWorker()
{
    std::string error;
    auto frames = cameras_.averageFrames(24, 45, error);
    if (!error.empty() || shutting_down_)
    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        calibration_status_ = CalibrationStatus{"error", error.empty() ? "Servern stoppas" : error, calibration_status_.started_at, nowMillis()};
        calibration_running_ = false;
        return;
    }

    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        calibration_status_.state = "analyzing";
        calibration_status_.message = "Identifierar ringar, trådar och 20-segment";
    }

    const bool success = detector_.calibrate(frames, error);
    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        calibration_status_.state = success ? "ready" : "error";
        calibration_status_.message = success ? "Alla kameror är kalibrerade" : error;
        calibration_status_.finished_at = nowMillis();
    }
    calibration_running_ = false;
    addEvent("calibration", success ? "Kalibreringen lyckades" : error, detector_.calibrationInfo());

    if (success && !shutting_down_)
        startDetection();
}

json Application::startDetection()
{
    if (processing_)
        return {{"ok", true}, {"message", "Dartmotorn kör redan"}};
    if (!detector_.calibrated())
        return {{"ok", false}, {"message", "Kalibrera kamerorna först"}};
    if (!cameras_.ready())
        return {{"ok", false}, {"message", "Alla tre kameror måste vara online"}};

    if (processing_thread_.joinable())
        processing_thread_.join();
    processing_ = true;
    processing_thread_ = std::thread(&Application::processingLoop, this);
    addEvent("engine", "Dartmotorn startades");
    return {{"ok", true}, {"message", "Dartmotorn kör"}};
}

json Application::stopDetection()
{
    const bool was_running = processing_.exchange(false);
    if (processing_thread_.joinable() && processing_thread_.get_id() != std::this_thread::get_id())
        processing_thread_.join();
    if (was_running)
        addEvent("engine", "Dartmotorn stoppades");
    return {{"ok", true}, {"message", was_running ? "Dartmotorn stoppades" : "Dartmotorn var redan stoppad"}};
}

void Application::processingLoop()
{
    const auto interval = std::chrono::milliseconds(std::max(1, 1000 / cameras_.fps()));
    while (processing_ && !shutting_down_)
    {
        const auto started = std::chrono::steady_clock::now();
        const auto frames = cameras_.frames(true);
        const auto event = detector_.process(frames);
        if (event)
        {
            json detection = {
                {"score", event->score},
                {"confidence", event->confidence},
                {"camera", event->camera_index},
                {"position", {{"x", event->position.x}, {"y", event->position.y}}},
                {"processing_time", event->processing_time_ms},
                {"timestamp", event->timestamp},
            };

            json game_result;
            if (event->score == "END")
                game_result = game_.markBoardClear();
            else
                game_result = game_.addDart(event->score, "detector");

            {
                std::lock_guard<std::mutex> lock(event_mutex_);
                last_detection_ = detection;
            }
            addEvent("dart", event->score == "END" ? "Tavlan är tom" : "Detekterat kast: " + event->score,
                     {{"detection", detection}, {"accepted", game_result.value("accepted", false)}});
        }

        const auto elapsed = std::chrono::steady_clock::now() - started;
        if (elapsed < interval)
            std::this_thread::sleep_for(interval - elapsed);
    }
}

json Application::startGame(const std::vector<std::string> &players)
{
    const json result = game_.start(players);
    addEvent("game", result.value("message", "501 startat"));
    return result;
}

json Application::submitDart(const std::string &score, const std::string &source)
{
    const json result = game_.addDart(score, source);
    addEvent("dart", result.value("message", score), {{"score", score}, {"source", source}, {"accepted", result.value("accepted", false)}});
    return result;
}

json Application::undoDart()
{
    const json result = game_.undo();
    addEvent("game", result.value("message", "Kast ångrat"));
    return result;
}

json Application::markBoardClear()
{
    const json result = game_.markBoardClear();
    addEvent("game", result.value("message", "Tavlan är tom"));
    return result;
}

json Application::resetGame()
{
    const json result = game_.reset();
    addEvent("game", result.value("message", "Spelet nollställt"));
    return result;
}

json Application::calibrationStatusJson() const
{
    CalibrationStatus status;
    {
        std::lock_guard<std::mutex> lock(calibration_mutex_);
        status = calibration_status_;
    }
    json result = detector_.calibrationInfo();
    result["state"] = status.state;
    result["message"] = status.message;
    result["started_at"] = status.started_at;
    result["finished_at"] = status.finished_at;
    return result;
}

json Application::state() const
{
    json events;
    json last_detection;
    {
        std::lock_guard<std::mutex> lock(event_mutex_);
        events = events_;
        last_detection = last_detection_;
    }

    return {
        {"service", "OpenDartboard x86"},
        {"version", APP_VERSION},
        {"platform", "linux-x86_64"},
        {"timestamp", nowMillis()},
        {"engine", {
             {"running", processing_.load()},
             {"last_detection", last_detection},
         }},
        {"cameras", cameras_.status()},
        {"calibration", calibrationStatusJson()},
        {"game", game_.state()},
        {"events", events},
    };
}

void Application::shutdown()
{
    if (shutting_down_.exchange(true))
        return;

    stopDetection();
    cameras_.stop();
    if (calibration_thread_.joinable() && calibration_thread_.get_id() != std::this_thread::get_id())
        calibration_thread_.join();
    processing_ = false;
    if (processing_thread_.joinable() && processing_thread_.get_id() != std::this_thread::get_id())
        processing_thread_.join();
}

