#include "web_browser.h"
#include "app_menu.h"
#include "browsers.h"
#include <cmath>
#include "../common.h"
#include "../settings.h"
#include "logging.h"
#include "../mpv/event.h"
#include "../playback/coordinator.h"
#include "../playback/event.h"
#include "../theme_color.h"
#include "../cef/color.h"
#include "../input/dispatch.h"
#include "../cjson/cJSON.h"
#include "../paths/paths.h"
#include "../jellyfin/device_profile.h"

// =====================================================================
// Helpers
// =====================================================================

static MediaMetadata parseMetadataJson(const std::string& json) {
    MediaMetadata meta;
    cJSON* root = cJSON_Parse(json.c_str());
    if (!root) return meta;

    cJSON* item;
    if ((item = cJSON_GetObjectItem(root, "Id")) && cJSON_IsString(item))
        meta.id = item->valuestring;
    if ((item = cJSON_GetObjectItem(root, "Name")) && cJSON_IsString(item))
        meta.title = item->valuestring;
    if ((item = cJSON_GetObjectItem(root, "SeriesName")) && cJSON_IsString(item))
        meta.artist = item->valuestring;
    if (meta.artist.empty()) {
        if ((item = cJSON_GetObjectItem(root, "Artists")) && cJSON_IsArray(item)) {
            cJSON* first = cJSON_GetArrayItem(item, 0);
            if (first && cJSON_IsString(first))
                meta.artist = first->valuestring;
        }
    }
    if ((item = cJSON_GetObjectItem(root, "SeasonName")) && cJSON_IsString(item))
        meta.album = item->valuestring;
    if (meta.album.empty()) {
        if ((item = cJSON_GetObjectItem(root, "Album")) && cJSON_IsString(item))
            meta.album = item->valuestring;
    }
    if ((item = cJSON_GetObjectItem(root, "IndexNumber")) && cJSON_IsNumber(item))
        meta.track_number = item->valueint;
    if ((item = cJSON_GetObjectItem(root, "RunTimeTicks")) && cJSON_IsNumber(item))
        meta.duration_us = static_cast<int64_t>(item->valuedouble) / 10;
    if ((item = cJSON_GetObjectItem(root, "Type")) && cJSON_IsString(item)) {
        std::string type = item->valuestring;
        if (type == "Audio") meta.media_type = MediaType::Audio;
        else if (type == "Movie" || type == "Episode" || type == "Video" || type == "MusicVideo")
            meta.media_type = MediaType::Video;
    }
    cJSON_Delete(root);
    return meta;
}

static void applySettingValue(const std::string& section, const std::string& key, const std::string& value) {
    auto& s = Settings::instance();
    if (key == "hwdec") s.setHwdec(value);
    else if (key == "audioPassthrough") s.setAudioPassthrough(value);
    else if (key == "audioExclusive") s.setAudioExclusive(value == "true");
    else if (key == "audioChannels") s.setAudioChannels(value);
    else if (key == "titlebarThemeColor") s.setTitlebarThemeColor(value == "true");
    else if (key == "logLevel") s.setLogLevel(value);
    else if (key == "forceTranscoding") s.setForceTranscoding(value == "true");
    else if (key == "deviceName") s.setDeviceName(value);
    else LOG_WARN(LOG_CEF, "Unknown setting key: {}.{}", section.c_str(), key.c_str());
    s.saveAsync();
}

// Helper to read an int from CefListValue that may have been sent as double.
static int getIntArg(CefRefPtr<CefListValue> args, size_t idx) {
    if (args->GetType(idx) == VTYPE_DOUBLE)
        return static_cast<int>(std::lround(args->GetDouble(idx)));
    return args->GetInt(idx);
}

// =====================================================================
// WebBrowser
// =====================================================================

CefRefPtr<CefDictionaryValue> WebBrowser::injectionProfile() {
    static const char* const kFunctions[] = {
        "playerLoad", "playerStop", "playerPause", "playerPlay", "playerSeek",
        "playerSetVolume", "playerSetMuted", "playerSetSpeed",
        "playerSetSubtitle", "playerAddSubtitle", "playerSetAudio", "playerAddAudio",
        "playerSetAudioDelay", "playerSetSubtitleDelay", "playerSetAspectMode", "playerOsdActive",
        "openConfigDir", "saveServerUrl",
        "notifyMetadata", "notifyPosition", "notifySeek",
        "notifyPlaybackState", "notifyArtwork", "notifyQueueChange",
        "notifyRateChange",
        "appExit", "setSettingValue", "themeColor",
        "setOsdVisible", "setCursorVisible", "toggleFullscreen",
    };
    static const char* const kScripts[] = {
        "native-shim.js",
        "mpv-player-base.js",
        "mpv-video-player.js",
        "mpv-audio-player.js",
        "input-plugin.js",
        "client-settings.js",
    };

    CefRefPtr<CefListValue> fns = CefListValue::Create();
    for (size_t i = 0; i < sizeof(kFunctions) / sizeof(*kFunctions); i++)
        fns->SetString(i, kFunctions[i]);
    CefRefPtr<CefListValue> scripts = CefListValue::Create();
    for (size_t i = 0; i < sizeof(kScripts) / sizeof(*kScripts); i++)
        scripts->SetString(i, kScripts[i]);

    CefRefPtr<CefDictionaryValue> d = CefDictionaryValue::Create();
    d->SetList("functions", fns);
    d->SetList("scripts", scripts);
    const std::string& profile_json = jellyfin_device_profile::CachedJson();
    if (!profile_json.empty())
        d->SetString("device_profile_json", profile_json);
    return d;
}

WebBrowser::WebBrowser(CefRefPtr<CefLayer> layer)
    : layer_(std::move(layer))
{
    layer_->setName("web");
    layer_->setMessageHandler([this](const std::string& name,
                                     CefRefPtr<CefListValue> args,
                                     CefRefPtr<CefBrowser> browser) {
        return handleMessage(name, args, browser);
    });
    layer_->setCreatedCallback([](CefRefPtr<CefBrowser> browser) {
        // Main browser takes input only if no other layer has already
        // claimed it (e.g. the server-selection overlay).
        if (g_browsers && !g_browsers->active())
            g_browsers->setActive(browser);
    });
    layer_->setContextMenuBuilder(&app_menu::build);
    layer_->setContextMenuDispatcher(&app_menu::dispatch);
}

WebBrowser::~WebBrowser() {
    release_layer(layer_.get());
}

bool WebBrowser::handleMessage(const std::string& name,
                               CefRefPtr<CefListValue> args,
                               CefRefPtr<CefBrowser> browser) {
    if (!g_mpv.IsValid()) return false;

    if (name == "playerLoad") {
        std::string url = args->GetString(0).ToString();
        int startMs = args->GetSize() > 1 ? getIntArg(args, 1) : 0;
        int videoIdx = getIntArg(args, 2);
        int audioIdx = getIntArg(args, 3);
        int subIdx = getIntArg(args, 4);
        // arg 5 is metadataJson (consumed elsewhere); args 6 and 7 are
        // optional external audio / subtitle URLs bundled into load so
        // their audio-add / sub-add can be queued before the FILE_LOADED-
        // driven unpause, gating playback on each external file being
        // opened and its track selected.
        std::string metadataJson = args->GetSize() > 5 ? args->GetString(5).ToString() : "";
        std::string externalAudioUrl = args->GetSize() > 6 ? args->GetString(6).ToString() : "";
        std::string externalSubUrl = args->GetSize() > 7 ? args->GetString(7).ToString() : "";
        bool isInfiniteStream = args->GetSize() > 8 ? args->GetBool(8) : false;
        LOG_INFO(LOG_CEF, "playerLoad: video={} audio={} sub={} start={}ms infinite={} extAudio={} extSub={} url={}",
                 videoIdx, audioIdx, subIdx, startMs, isInfiniteStream, externalAudioUrl.c_str(), externalSubUrl.c_str(), url.c_str());
        // Push next-track metadata + load-starting hint atomically before
        // mpv loadfile. Parse metadata first so the Jellyfin item Id can
        // ride along with postLoadStarting — SM compares it to the prior
        // Id to set snapshot.variant_switch_pending on same-item reload
        // (bitrate / transcode-variant change). Coord seeds
        // snapshot.position_us with the resume offset so MPRIS/JS see
        // the start position before mpv has opened the file. Coord also
        // swallows the resulting END_FILE for the outgoing track
        // (no Stopped flicker); MPRIS sees phase=Starting with the new
        // content immediately.
        MediaMetadata meta = metadataJson.empty()
            ? MediaMetadata{}
            : parseMetadataJson(metadataJson);
        if (g_playback_coord) {
            g_playback_coord->postLoadStarting(meta.id);
            g_playback_coord->postPosition(static_cast<int64_t>(startMs) * 1000);
        }
        if (!metadataJson.empty()) {
            if (g_theme_color) g_theme_color->setVideoMode(meta.media_type == MediaType::Video);
            if (g_playback_coord) g_playback_coord->postMetadata(meta);
        }
        MpvHandle::LoadOptions opts;
        opts.startSecs = startMs / 1000.0;
        opts.videoTrack = videoIdx;
        opts.audioTrack = audioIdx;
        opts.subTrack = subIdx;
        opts.externalAudioUrl = externalAudioUrl;
        opts.externalSubUrl = externalSubUrl;
        opts.isInfiniteStream = isInfiniteStream;
        g_mpv.LoadFile(url, opts);
    } else if (name == "playerStop") {
        g_mpv.Stop();
    } else if (name == "playerPause") {
        g_mpv.Pause();
    } else if (name == "playerPlay") {
        g_mpv.Play();
    } else if (name == "playerSeek") {
        double pos = getIntArg(args, 0) / 1000.0;
        g_mpv.SeekAbsolute(pos);
    } else if (name == "playerSetVolume") {
        g_mpv.SetVolume(getIntArg(args, 0));
    } else if (name == "playerSetMuted") {
        g_mpv.SetMuted(args->GetBool(0));
    } else if (name == "playerSetSpeed") {
        g_mpv.SetSpeed(getIntArg(args, 0) / 1000.0);
    } else if (name == "playerSetSubtitle") {
        LOG_INFO(LOG_CEF, "playerSetSubtitle: {}", getIntArg(args, 0));
        g_mpv.SetSubtitleTrack(getIntArg(args, 0));
    } else if (name == "playerAddSubtitle") {
        std::string url = args->GetString(0).ToString();
        LOG_INFO(LOG_CEF, "playerAddSubtitle: {}", url.c_str());
        g_mpv.SubAdd(url);
    } else if (name == "playerSetAudio") {
        g_mpv.SetAudioTrack(getIntArg(args, 0));
    } else if (name == "playerAddAudio") {
        std::string url = args->GetString(0).ToString();
        LOG_INFO(LOG_CEF, "playerAddAudio: {}", url.c_str());
        g_mpv.AudioAdd(url);
    } else if (name == "playerSetAudioDelay") {
        g_mpv.SetAudioDelay(args->GetDouble(0));
    } else if (name == "playerSetSubtitleDelay") {
        g_mpv.SetSubtitleDelay(args->GetDouble(0));
    } else if (name == "playerSetAspectMode") {
        g_mpv.SetAspectMode(args->GetString(0).ToString());
    } else if (name == "playerOsdActive") {
        bool active = args->GetBool(0);
        if (active) {
            was_fullscreen_before_osd_ = mpv::fullscreen();
        } else {
            if (!was_fullscreen_before_osd_)
                g_platform.set_fullscreen(false);
        }
    } else if (name == "toggleFullscreen") {
        g_platform.toggle_fullscreen();
    } else if (name == "saveServerUrl") {
        std::string url = args->GetString(0).ToString();
        Settings::instance().setServerUrl(url);
        Settings::instance().saveAsync();
    } else if (name == "setSettingValue") {
        std::string section = args->GetString(0).ToString();
        std::string key = args->GetString(1).ToString();
        std::string value = args->GetString(2).ToString();
        applySettingValue(section, key, value);
    } else if (name == "themeColor") {
        std::string color = args->GetString(0).ToString();
        LOG_DEBUG(LOG_CEF, "themeColor IPC: {}", color.c_str());
        if (g_theme_color) g_theme_color->onThemeColor(cef::parseColor(color));
    } else if (name == "notifyMetadata") {
        std::string json = args->GetString(0).ToString();
        MediaMetadata meta = parseMetadataJson(json);
        if (g_theme_color) g_theme_color->setVideoMode(meta.media_type == MediaType::Video);
        if (g_playback_coord) g_playback_coord->postMetadata(std::move(meta));
    } else if (name == "notifyArtwork") {
        std::string artworkUri = args->GetString(0).ToString();
        if (g_playback_coord) g_playback_coord->postArtwork(std::move(artworkUri));
    } else if (name == "notifyQueueChange") {
        bool canNext = args->GetBool(0);
        bool canPrev = args->GetBool(1);
        if (g_playback_coord) g_playback_coord->postQueueCaps(canNext, canPrev);
    } else if (name == "notifyPlaybackState") {
        // mpv is the authoritative playback-state source via the coordinator.
        // JS still emits this hint as it navigates; ignore it for state but
        // keep the IPC callable so the JS side does not see a missing handler.
    } else if (name == "notifySeek") {
        int posMs = getIntArg(args, 0);
        if (g_playback_coord)
            g_playback_coord->postSeeked(static_cast<int64_t>(posMs) * 1000);
    } else if (name == "setCursorVisible") {
        g_platform.set_cursor(args->GetBool(0) ? CT_POINTER : CT_NONE);
    } else if (name == "appExit") {
        initiate_shutdown();
    } else if (name == "openConfigDir") {
        LOG_INFO(LOG_CEF, "Opening mpv home directory");
        paths::openMpvHome();
    } else {
        return false;
    }
    return true;
}
