/*
 *  Copyright (C) 2005-2026 Team Kodi
 *  This file is part of Kodi - https://kodi.tv
 *
 *  SPDX-License-Identifier: GPL-2.0-or-later
 *  See LICENSES/README.md for more information.
 */

#include "JSONRPC.h"

#include "FileItem.h"
#include "GUIUserMessages.h"
#include "ServiceBroker.h"
#include "ServiceDescription.h"
#include "TextureDatabase.h"
#include "addons/Addon.h"
#include "addons/IAddon.h"
#include "addons/addoninfo/AddonInfo.h"
#include "addons/addoninfo/AddonType.h"
#include "dbwrappers/DatabaseQuery.h"
#include "guilib/GUIComponent.h"
#include "guilib/GUIMessage.h"
#include "guilib/GUIWindowManager.h"
#include "input/WindowTranslator.h"
#include "input/actions/ActionTranslator.h"
#include "interfaces/AnnouncementManager.h"
#include "playlists/SmartPlayList.h"
#include "settings/AdvancedSettings.h"
#include "settings/SettingsComponent.h"
#include "utils/StringUtils.h"
#include "utils/Variant.h"
#include "utils/log.h"

#include <string.h>

using namespace KODI;
using namespace JSONRPC;

bool CJSONRPC::m_initialized = false;

void CJSONRPC::Initialize()
{
  if (m_initialized)
    return;

  // Add some types/enums at runtime
  std::vector<std::string> enumList;
  for (int addonType = static_cast<int>(ADDON::AddonType::UNKNOWN);
       addonType < static_cast<int>(ADDON::AddonType::MAX_TYPES); addonType++)
    enumList.push_back(
        ADDON::CAddonInfo::TranslateType(static_cast<ADDON::AddonType>(addonType), false));
  CJSONServiceDescription::AddEnum("Addon.Types", enumList);

  enumList.clear();
  ACTION::CActionTranslator::GetActions(enumList);
  CJSONServiceDescription::AddEnum("Input.Action", enumList);

  enumList.clear();
  CWindowTranslator::GetWindows(enumList);
  CJSONServiceDescription::AddEnum("GUI.Window", enumList);

  // filter-related enums
  std::vector<std::string> smartplaylistList;
  CDatabaseQueryRule::GetAvailableOperators(smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Operators", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("movies", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Movies", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("tvshows", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.TVShows", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("episodes", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Episodes", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("musicvideos", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.MusicVideos", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("artists", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Artists", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("albums", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Albums", smartplaylistList);

  smartplaylistList.clear();
  PLAYLIST::CSmartPlaylist::GetAvailableFields("songs", smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Songs", smartplaylistList);

  smartplaylistList.clear();
  CTextureRule::GetAvailableFields(smartplaylistList);
  CJSONServiceDescription::AddEnum("List.Filter.Fields.Textures", smartplaylistList);

  unsigned int size = sizeof(JSONRPC_SERVICE_TYPES) / sizeof(char*);

  for (unsigned int index = 0; index < size; index++)
    CJSONServiceDescription::AddType(JSONRPC_SERVICE_TYPES[index]);

  size = sizeof(JSONRPC_SERVICE_METHODS) / sizeof(char*);

  for (unsigned int index = 0; index < size; index++)
    CJSONServiceDescription::AddBuiltinMethod(JSONRPC_SERVICE_METHODS[index]);

  size = sizeof(JSONRPC_SERVICE_NOTIFICATIONS) / sizeof(char*);

  for (unsigned int index = 0; index < size; index++)
    CJSONServiceDescription::AddNotification(JSONRPC_SERVICE_NOTIFICATIONS[index]);

  CJSONServiceDescription::ResolveReferences();

  m_initialized = true;
  CLog::Log(LOGINFO, "JSONRPC v{}: Successfully initialized",
            CJSONServiceDescription::GetVersion());
}

void CJSONRPC::Cleanup()
{
  CJSONServiceDescription::Cleanup();
  m_initialized = false;
}

JSONRPC_STATUS CJSONRPC::Introspect(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  return CJSONServiceDescription::Print(result, transport, client,
    parameterObject["getdescriptions"].asBoolean(), parameterObject["getmetadata"].asBoolean(), parameterObject["filterbytransport"].asBoolean(),
    parameterObject["filter"]["id"].asString(), parameterObject["filter"]["type"].asString(), parameterObject["filter"]["getreferences"].asBoolean());
}

JSONRPC_STATUS CJSONRPC::Version(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  result["version"]["major"] = 0;
  result["version"]["minor"] = 0;
  result["version"]["patch"] = 0;

  const char* version = CJSONServiceDescription::GetVersion();
  if (version != NULL)
  {
    std::vector<std::string> parts = StringUtils::Split(version, ".");
    if (!parts.empty())
      result["version"]["major"] = (int)strtol(parts[0].c_str(), NULL, 10);
    if (parts.size() > 1)
      result["version"]["minor"] = (int)strtol(parts[1].c_str(), NULL, 10);
    if (parts.size() > 2)
      result["version"]["patch"] = (int)strtol(parts[2].c_str(), NULL, 10);
  }

  return OK;
}

JSONRPC_STATUS CJSONRPC::Permission(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  int flags = client->GetPermissionFlags();

  for (int i = 1; i <= OPERATION_PERMISSION_ALL; i *= 2)
    result[PermissionToString((OperationPermission)i)] = (flags & i) == i;

  return OK;
}

JSONRPC_STATUS CJSONRPC::Ping(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  CVariant temp = "pong";
  result.swap(temp);
  return OK;
}

JSONRPC_STATUS CJSONRPC::GetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  int flags = client->GetAnnouncementFlags();

  for (int i = 1; i <= ANNOUNCEMENT::ANNOUNCE_ALL; i *= 2)
    result["notifications"][AnnouncementFlagToString((ANNOUNCEMENT::AnnouncementFlag)i)] = (flags & i) == i;

  return OK;
}

JSONRPC_STATUS CJSONRPC::SetConfiguration(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  int flags = 0;
  int oldFlags = client->GetAnnouncementFlags();

  if (parameterObject.isMember("notifications"))
  {
    CVariant notifications = parameterObject["notifications"];
    if ((notifications["Player"].isNull() && (oldFlags & ANNOUNCEMENT::Player)) ||
        (notifications["Player"].isBoolean() && notifications["Player"].asBoolean()))
      flags |= ANNOUNCEMENT::Player;
    if ((notifications["Playlist"].isNull() && (oldFlags & ANNOUNCEMENT::Playlist)) ||
        (notifications["Playlist"].isBoolean() && notifications["Playlist"].asBoolean()))
      flags |= ANNOUNCEMENT::Playlist;
    if ((notifications["GUI"].isNull() && (oldFlags & ANNOUNCEMENT::GUI)) ||
        (notifications["GUI"].isBoolean() && notifications["GUI"].asBoolean()))
      flags |= ANNOUNCEMENT::GUI;
    if ((notifications["System"].isNull() && (oldFlags & ANNOUNCEMENT::System)) ||
        (notifications["System"].isBoolean() && notifications["System"].asBoolean()))
      flags |= ANNOUNCEMENT::System;
    if ((notifications["VideoLibrary"].isNull() && (oldFlags & ANNOUNCEMENT::VideoLibrary)) ||
        (notifications["VideoLibrary"].isBoolean() && notifications["VideoLibrary"].asBoolean()))
      flags |= ANNOUNCEMENT::VideoLibrary;
    if ((notifications["AudioLibrary"].isNull() && (oldFlags & ANNOUNCEMENT::AudioLibrary)) ||
        (notifications["AudioLibrary"].isBoolean() && notifications["AudioLibrary"].asBoolean()))
      flags |= ANNOUNCEMENT::AudioLibrary;
    if ((notifications["Application"].isNull() && (oldFlags & ANNOUNCEMENT::Other)) ||
        (notifications["Application"].isBoolean() && notifications["Application"].asBoolean()))
      flags |= ANNOUNCEMENT::Application;
    if ((notifications["Input"].isNull() && (oldFlags & ANNOUNCEMENT::Input)) ||
        (notifications["Input"].isBoolean() && notifications["Input"].asBoolean()))
      flags |= ANNOUNCEMENT::Input;
    if ((notifications["Other"].isNull() && (oldFlags & ANNOUNCEMENT::Other)) ||
        (notifications["Other"].isBoolean() && notifications["Other"].asBoolean()))
      flags |= ANNOUNCEMENT::Other;
  }

  if (!client->SetAnnouncementFlags(flags))
    return BadPermission;

  return GetConfiguration(method, transport, client, parameterObject, result);
}

JSONRPC_STATUS CJSONRPC::NotifyAll(const std::string &method, ITransportLayer *transport, IClient *client, const CVariant& parameterObject, CVariant &result)
{
  if (parameterObject["data"].isNull())
    CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Other,
                                                       parameterObject["sender"].asString(),
                                                       parameterObject["message"].asString());
  else
  {
    CVariant data = parameterObject["data"];
    CServiceBroker::GetAnnouncementManager()->Announce(ANNOUNCEMENT::Other,
                                                       parameterObject["sender"].asString(),
                                                       parameterObject["message"].asString(), data);
  }

  return ACK;
}

std::string CJSONRPC::MethodCall(const std::string &inputString, ITransportLayer *transport, IClient *client)
{
  CVariant inputroot, outputroot, result;
  bool hasResponse = false;

  CLog::Log(LOGDEBUG, LOGJSONRPC, "JSONRPC: Incoming request: {}", inputString);

  if (CJSONVariantParser::Parse(inputString, inputroot) && !inputroot.isNull())
  {
    if (inputroot.isArray())
    {
      if (inputroot.empty())
      {
        CLog::Log(LOGERROR, "JSONRPC: Empty batch call");
        BuildResponse(inputroot, InvalidRequest, CVariant(), outputroot);
        hasResponse = true;
      }
      else
      {
        for (CVariant::const_iterator_array itr = inputroot.begin_array();
             itr != inputroot.end_array(); ++itr)
        {
          CVariant response;
          if (HandleMethodCall(*itr, response, transport, client))
          {
            outputroot.append(response);
            hasResponse = true;
          }
        }
      }
    }
    else
      hasResponse = HandleMethodCall(inputroot, outputroot, transport, client);
  }
  else
  {
    CLog::Log(LOGERROR, "JSONRPC: Failed to parse '{}'", inputString);
    BuildResponse(inputroot, ParseError, CVariant(), outputroot);
    hasResponse = true;
  }

  std::string str;
  if (hasResponse)
    CJSONVariantWriter::Write(outputroot, str, CServiceBroker::GetSettingsComponent()->GetAdvancedSettings()->m_jsonOutputCompact);

  return str;
}

bool CJSONRPC::HandleMethodCall(const CVariant& request, CVariant& response, ITransportLayer *transport, IClient *client)
{
  JSONRPC_STATUS errorCode = OK;
  CVariant result;
  bool isNotification = false;

  if (IsProperJSONRPC(request))
  {
    isNotification = !request.isMember("id");

    std::string methodName = request["method"].asString();
    StringUtils::ToLower(methodName);

    JSONRPC::MethodCall method;
    CVariant params;

    if ((errorCode = CJSONServiceDescription::CheckCall(methodName.c_str(), request["params"], transport, client, isNotification, method, params)) == OK)
      errorCode = method(methodName, transport, client, params, result);
    else
      result = params;
  }
  else
  {
    std::string str;
    CJSONVariantWriter::Write(request, str, true);

    CLog::Log(LOGERROR, "JSONRPC: Failed to parse '{}'", str);
    errorCode = InvalidRequest;
  }

  BuildResponse(request, errorCode, result, response);

  return !isNotification;
}

inline bool CJSONRPC::IsProperJSONRPC(const CVariant& inputroot)
{
  return inputroot.isMember("jsonrpc") && inputroot["jsonrpc"].isString() && inputroot["jsonrpc"] == CVariant("2.0") && inputroot.isMember("method") && inputroot["method"].isString() && (!inputroot.isMember("params") || inputroot["params"].isArray() || inputroot["params"].isObject());
}

inline void CJSONRPC::BuildResponse(const CVariant& request, JSONRPC_STATUS code, const CVariant& result, CVariant& response)
{
  response["jsonrpc"] = "2.0";
  response["id"] = request.isMember("id") ? request["id"] : CVariant();

  switch (code)
  {
    case OK:
      response["result"] = result;
      break;
    case ACK:
      response["result"] = "OK";
      break;
    default:
    {
      const JsonRpcStatusDescription* status = StatusToDescription(code);
      if (status == nullptr)
        status = StatusToDescription(InternalError);

      response["error"]["code"] = status->status;
      response["error"]["message"] = status->message;
      if (status->hasData && !result.isNull())
        response["error"]["data"] = result;
      break;
    }
  }
}

void CJSONRPCUtils::NotifyItemUpdated()
{
  CGUIMessage message(GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE,
                      CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow());
  CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
}

void CJSONRPCUtils::NotifyItemUpdated(const std::shared_ptr<CFileItem>& item)
{
  auto& wm = CServiceBroker::GetGUI()->GetWindowManager();
  CGUIMessage message(GUI_MSG_NOTIFY_ALL, wm.GetActiveWindow(), 0, GUI_MSG_UPDATE_ITEM, 0, item);
  wm.SendThreadMessage(message);
}

void CJSONRPCUtils::NotifyItemUpdated(const CVideoInfoTag& info, const KODI::ART::Artwork& artwork)
{
  CFileItemPtr msgItem(new CFileItem(info));
  if (!artwork.empty())
    msgItem->SetArt(artwork);
  CGUIMessage message(GUI_MSG_NOTIFY_ALL,
                      CServiceBroker::GetGUI()->GetWindowManager().GetActiveWindow(), 0,
                      GUI_MSG_UPDATE_ITEM, 0, msgItem);
  CServiceBroker::GetGUI()->GetWindowManager().SendThreadMessage(message);
}
