#include "core/entitlement.hpp"

#include <algorithm>
#include <array>
#include <charconv>
#include <cctype>
#include <cwchar>
#include <cstring>
#include <fstream>
#include <limits>
#include <map>
#include <string_view>
#include <utility>

#include "core/license_public_key.hpp"

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <bcrypt.h>
#endif

namespace smartcut::core {
namespace {

constexpr std::string_view kTokenPrefix = "SC64-1";
constexpr std::size_t kMaximumTokenBytes = 16 * 1024;

int hex_value(char value) {
  if (value >= '0' && value <= '9') return value - '0';
  value = static_cast<char>(std::tolower(static_cast<unsigned char>(value)));
  if (value >= 'a' && value <= 'f') return value - 'a' + 10;
  return -1;
}

std::vector<std::uint8_t> decode_hex(std::string_view text) {
  std::vector<std::uint8_t> result;
  if (text.empty() || text.size() % 2 != 0) return result;
  result.reserve(text.size() / 2);
  for (std::size_t index = 0; index < text.size(); index += 2) {
    const int high = hex_value(text[index]);
    const int low = hex_value(text[index + 1]);
    if (high < 0 || low < 0) return {};
    result.push_back(static_cast<std::uint8_t>((high << 4) | low));
  }
  return result;
}

std::vector<std::uint8_t> decode_base64url(std::string_view text) {
  static constexpr std::string_view alphabet =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
  std::array<int, 256> reverse{};
  reverse.fill(-1);
  for (std::size_t index = 0; index < alphabet.size(); ++index)
    reverse[static_cast<unsigned char>(alphabet[index])] = static_cast<int>(index);
  if (text.empty() || text.size() % 4 == 1) return {};
  std::vector<std::uint8_t> output;
  output.reserve(text.size() * 3 / 4 + 2);
  std::uint32_t accumulator = 0;
  int bits = 0;
  for (const char value : text) {
    const int decoded = reverse[static_cast<unsigned char>(value)];
    if (decoded < 0) return {};
    accumulator = (accumulator << 6U) | static_cast<std::uint32_t>(decoded);
    bits += 6;
    if (bits >= 8) {
      bits -= 8;
      output.push_back(static_cast<std::uint8_t>((accumulator >> bits) & 0xffU));
    }
  }
  if (bits > 0 && (accumulator & ((1U << bits) - 1U)) != 0) return {};
  return output;
}

bool parse_integer(std::string_view text, std::int64_t& value) {
  if (text.empty()) return false;
  const auto* begin = text.data();
  const auto* end = begin + text.size();
  const auto parsed = std::from_chars(begin, end, value);
  return parsed.ec == std::errc{} && parsed.ptr == end;
}

bool parse_claims(std::string_view payload, EntitlementResult& result,
                  std::map<std::string, std::string>& claims) {
  if (payload.empty() || payload.size() > 8192 || payload.back() == '\n' ||
      std::any_of(payload.begin(), payload.end(), [](unsigned char character) {
        return (character < 0x20 && character != '\n') || character == 0x7f;
      }))
    return false;
  std::size_t cursor = 0;
  while (cursor < payload.size()) {
    const auto end = payload.find('\n', cursor);
    const auto line = payload.substr(cursor, end == std::string_view::npos ? payload.size() - cursor
                                                                          : end - cursor);
    if (line.empty()) return false;
    const auto separator = line.find('=');
    if (separator == std::string_view::npos || separator == 0) return false;
    const std::string key(line.substr(0, separator));
    const std::string value(line.substr(separator + 1));
    if (!std::all_of(key.begin(), key.end(), [](unsigned char character) {
          return std::islower(character) || std::isdigit(character) || character == '_';
        }) || !claims.emplace(key, value).second)
      return false;
    if (end == std::string_view::npos) break;
    cursor = end + 1;
  }
  const auto required = [&](const char* key) { return claims.find(key) != claims.end(); };
  if (claims.size() != 9 || !required("product") || !required("edition") || !required("license_id") ||
      !required("customer") || !required("installation_id") || !required("issued_unix") ||
      !required("not_before_unix") || !required("expires_unix") || !required("clean_export"))
    return false;
  result.license_id = claims["license_id"];
  result.customer = claims["customer"];
  result.installation_id = claims["installation_id"];
  if (result.license_id.empty() || result.license_id.size() > 128 || result.customer.empty() ||
      result.customer.size() > 512 || result.installation_id.size() != 32 ||
      !std::all_of(result.installation_id.begin(), result.installation_id.end(), [](unsigned char character) { return std::isxdigit(character); }) ||
      !parse_integer(claims["issued_unix"], result.issued_unix) ||
      !parse_integer(claims["not_before_unix"], result.not_before_unix) ||
      !parse_integer(claims["expires_unix"], result.expires_unix))
    return false;
  std::transform(result.installation_id.begin(), result.installation_id.end(), result.installation_id.begin(), [](unsigned char character) { return static_cast<char>(std::tolower(character)); });
  return result.issued_unix >= 0 && result.not_before_unix >= 0 && result.expires_unix >= 0 &&
         (result.expires_unix == 0 ||
          result.expires_unix >= result.not_before_unix);
}

#ifdef _WIN32
bool sha256(std::string_view input, std::vector<std::uint8_t>& digest) {
  BCRYPT_ALG_HANDLE algorithm = nullptr;
  BCRYPT_HASH_HANDLE hash = nullptr;
  DWORD object_size = 0;
  DWORD digest_size = 0;
  DWORD received = 0;
  if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0 ||
      BCryptGetProperty(algorithm, BCRYPT_OBJECT_LENGTH,
                        reinterpret_cast<PUCHAR>(&object_size), sizeof(object_size), &received, 0) < 0 ||
      BCryptGetProperty(algorithm, BCRYPT_HASH_LENGTH,
                        reinterpret_cast<PUCHAR>(&digest_size), sizeof(digest_size), &received, 0) < 0) {
    if (algorithm) BCryptCloseAlgorithmProvider(algorithm, 0);
    return false;
  }
  std::vector<std::uint8_t> object(object_size);
  digest.resize(digest_size);
  const auto cleanup = [&] {
    if (hash) BCryptDestroyHash(hash);
    BCryptCloseAlgorithmProvider(algorithm, 0);
  };
  if (BCryptCreateHash(algorithm, &hash, object.data(), static_cast<ULONG>(object.size()),
                       nullptr, 0, 0) < 0 ||
      BCryptHashData(hash, reinterpret_cast<PUCHAR>(const_cast<char*>(input.data())),
                     static_cast<ULONG>(input.size()), 0) < 0 ||
      BCryptFinishHash(hash, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0) {
    cleanup();
    return false;
  }
  cleanup();
  return true;
}

bool verify_signature(std::string_view signed_text, const std::vector<std::uint8_t>& signature,
                      const EntitlementPublicKey& public_key) {
  if (public_key.modulus.size() < 256 || public_key.exponent.empty() || signature.empty() ||
      signature.size() != public_key.modulus.size() ||
      public_key.modulus.size() > std::numeric_limits<ULONG>::max())
    return false;
  BCRYPT_ALG_HANDLE algorithm = nullptr;
  BCRYPT_KEY_HANDLE key = nullptr;
  if (BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_RSA_ALGORITHM, nullptr, 0) < 0) return false;
  BCRYPT_RSAKEY_BLOB header{};
  header.Magic = BCRYPT_RSAPUBLIC_MAGIC;
  header.BitLength = static_cast<ULONG>(public_key.modulus.size() * 8);
  header.cbPublicExp = static_cast<ULONG>(public_key.exponent.size());
  header.cbModulus = static_cast<ULONG>(public_key.modulus.size());
  std::vector<std::uint8_t> blob(sizeof(header) + public_key.exponent.size() +
                                 public_key.modulus.size());
  std::memcpy(blob.data(), &header, sizeof(header));
  std::memcpy(blob.data() + sizeof(header), public_key.exponent.data(), public_key.exponent.size());
  std::memcpy(blob.data() + sizeof(header) + public_key.exponent.size(), public_key.modulus.data(),
              public_key.modulus.size());
  const auto imported = BCryptImportKeyPair(algorithm, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key,
                                             blob.data(), static_cast<ULONG>(blob.size()), 0);
  std::vector<std::uint8_t> digest;
  const bool hashed = imported >= 0 && sha256(signed_text, digest);
  BCRYPT_PKCS1_PADDING_INFO padding{BCRYPT_SHA256_ALGORITHM};
  const bool valid = hashed &&
      BCryptVerifySignature(key, &padding, digest.data(), static_cast<ULONG>(digest.size()),
                            const_cast<PUCHAR>(signature.data()),
                            static_cast<ULONG>(signature.size()), BCRYPT_PAD_PKCS1) >= 0;
  if (key) BCryptDestroyKey(key);
  BCryptCloseAlgorithmProvider(algorithm, 0);
  return valid;
}
#endif

EntitlementResult failure(EntitlementState state, std::string detail) {
  EntitlementResult result;
  result.state = state;
  result.detail = std::move(detail);
  return result;
}

}  // namespace

EntitlementPublicKey compiled_entitlement_public_key() {
  return {decode_hex(SMARTCUT_LICENSE_PUBLIC_MODULUS_HEX),
          decode_hex(SMARTCUT_LICENSE_PUBLIC_EXPONENT_HEX)};
}
std::string current_installation_id() {
#ifndef _WIN32
  return {};
#else
  wchar_t machine_guid[256]{};
  DWORD bytes = sizeof(machine_guid);
  const auto status = RegGetValueW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Cryptography",
                                   L"MachineGuid", RRF_RT_REG_SZ | RRF_SUBKEY_WOW6464KEY,
                                   nullptr, machine_guid, &bytes);
  if (status != ERROR_SUCCESS || bytes <= sizeof(wchar_t)) return {};
  const auto characters = std::wcslen(machine_guid);
  const int utf8_bytes = WideCharToMultiByte(CP_UTF8, 0, machine_guid,
      static_cast<int>(characters), nullptr, 0, nullptr, nullptr);
  if (utf8_bytes <= 0) return {};
  std::string guid(static_cast<std::size_t>(utf8_bytes), '\0');
  if (WideCharToMultiByte(CP_UTF8, 0, machine_guid, static_cast<int>(characters),
                          guid.data(), utf8_bytes, nullptr, nullptr) <= 0) return {};
  std::transform(guid.begin(), guid.end(), guid.begin(), [](unsigned char character) {
    return static_cast<char>(std::tolower(character));
  });
  std::string material("SmartCut64 installation v1\n");
  material += guid;
  std::vector<std::uint8_t> digest;
  if (!sha256(material, digest) || digest.size() < 16) return {};
  constexpr char hex[] = "0123456789abcdef";
  std::string result; result.reserve(32);
  for (std::size_t index = 0; index < 16; ++index) {
    result.push_back(hex[digest[index] >> 4]);
    result.push_back(hex[digest[index] & 0x0f]);
  }
  return result;
#endif
}

std::filesystem::path default_entitlement_path() {
#ifndef _WIN32
  return {};
#else
  const auto environment_path = [](const wchar_t* name) {
    const DWORD required = GetEnvironmentVariableW(name, nullptr, 0);
    if (required == 0) return std::filesystem::path{};
    std::wstring value(static_cast<std::size_t>(required), L'\0');
    const DWORD written = GetEnvironmentVariableW(name, value.data(), required);
    if (written == 0 || written >= required) return std::filesystem::path{};
    value.resize(written);
    return std::filesystem::path(value);
  };
  const auto local_app_data = environment_path(L"LOCALAPPDATA");
  return local_app_data.empty()
             ? std::filesystem::path{}
             : local_app_data / L"SmartCut64" / L"license.sc64";
#endif
}

std::filesystem::path installed_entitlement_path() {
#ifndef _WIN32
  return default_entitlement_path();
#else
  const DWORD required = GetEnvironmentVariableW(L"SMARTCUT64_LICENSE_FILE", nullptr, 0);
  if (required != 0) {
    std::wstring value(static_cast<std::size_t>(required), L'\0');
    const DWORD written = GetEnvironmentVariableW(L"SMARTCUT64_LICENSE_FILE", value.data(), required);
    if (written > 0 && written < required) {
      value.resize(written);
      return std::filesystem::path(value);
    }
  }
  return default_entitlement_path();
#endif
}

std::string load_installed_entitlement_token() {
  const auto path = installed_entitlement_path();
  if (path.empty()) return {};
  std::ifstream input(path, std::ios::binary);
  if (!input) return {};
  std::string token;
  token.reserve(kMaximumTokenBytes);
  std::array<char, 4096> buffer{};
  while (input) {
    input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
    const auto received = input.gcount();
    if (received <= 0) continue;
    const auto count = static_cast<std::size_t>(received);
    if (token.size() > kMaximumTokenBytes - count) return {};
    token.append(buffer.data(), count);
  }
  if (!input.eof()) return {};
  const auto whitespace = [](unsigned char character) { return std::isspace(character); };
  token.erase(token.begin(), std::find_if_not(token.begin(), token.end(), whitespace));
  token.erase(std::find_if_not(token.rbegin(), token.rend(), whitespace).base(), token.end());
  return token;
}

EntitlementResult verify_installed_entitlement(std::int64_t now_unix) {
  return verify_entitlement(load_installed_entitlement_token(), now_unix);
}

EntitlementResult verify_entitlement(const std::string& token,
                                     const EntitlementPublicKey& public_key,
                                     std::int64_t now_unix,
                                     const std::string& installation_id) {
  if (token.empty()) return failure(EntitlementState::missing, "No licence is installed");
  if (token.size() > kMaximumTokenBytes)
    return failure(EntitlementState::malformed, "The licence token is too large");
  const auto first_dot = token.find('.');
  const auto second_dot = first_dot == std::string::npos ? std::string::npos : token.find('.', first_dot + 1);
  if (first_dot == std::string::npos || second_dot == std::string::npos ||
      token.find('.', second_dot + 1) != std::string::npos ||
      std::string_view(token).substr(0, first_dot) != kTokenPrefix)
    return failure(EntitlementState::malformed, "The licence token format is invalid");
  const auto payload_segment = std::string_view(token).substr(first_dot + 1, second_dot - first_dot - 1);
  const auto signature_segment = std::string_view(token).substr(second_dot + 1);
  const auto payload_bytes = decode_base64url(payload_segment);
  const auto signature = decode_base64url(signature_segment);
  if (payload_bytes.empty() || signature.empty())
    return failure(EntitlementState::malformed, "The licence token encoding is invalid");
  EntitlementResult result;
  std::map<std::string, std::string> claims;
  if (!parse_claims(std::string_view(reinterpret_cast<const char*>(payload_bytes.data()), payload_bytes.size()),
                    result, claims))
    return failure(EntitlementState::malformed, "The licence claims are invalid");
  if (public_key.modulus.empty() || public_key.exponent.empty())
    return failure(EntitlementState::no_verification_key,
                   "This build has no paid-licence verification key");
#ifndef _WIN32
  return failure(EntitlementState::unsupported_platform,
                 "Licence signature verification is unavailable on this platform");
#else
  const std::string signed_text = token.substr(0, second_dot);
  if (!verify_signature(signed_text, signature, public_key))
    return failure(EntitlementState::invalid_signature, "The licence signature is invalid");
#endif
  if (claims["product"] != "SmartCut64")
    return failure(EntitlementState::wrong_product, "The licence is for another product");
  if (claims["edition"] != "paid" || claims["clean_export"] != "1")
    return failure(EntitlementState::wrong_edition,
                   "The licence does not permit clean video exports");
  constexpr std::int64_t clock_tolerance = 300;
  if (installation_id.empty())
    return failure(EntitlementState::unsupported_platform, "The installation identifier is unavailable");
  if (result.installation_id != installation_id)
    return failure(EntitlementState::wrong_installation, "The licence belongs to another installation");
  if (result.issued_unix > now_unix + clock_tolerance ||
      result.not_before_unix > now_unix + clock_tolerance)
    return failure(EntitlementState::not_yet_valid, "The licence is not yet valid");
  if (result.expires_unix != 0 && now_unix > result.expires_unix)
    return failure(EntitlementState::expired, "The licence has expired");
  result.state = EntitlementState::paid;
  result.detail = "Paid licence verified";
  return result;
}

EntitlementResult verify_entitlement(const std::string& token,
                                     const EntitlementPublicKey& public_key,
                                     std::int64_t now_unix) {
  return verify_entitlement(token, public_key, now_unix, current_installation_id());
}

EntitlementResult verify_entitlement(const std::string& token, std::int64_t now_unix) {
  return verify_entitlement(token, compiled_entitlement_public_key(), now_unix,
                            current_installation_id());
}

const char* entitlement_state_name(EntitlementState state) {
  switch (state) {
    case EntitlementState::paid: return "paid";
    case EntitlementState::missing: return "missing";
    case EntitlementState::malformed: return "malformed";
    case EntitlementState::unsupported_platform: return "unsupported_platform";
    case EntitlementState::no_verification_key: return "no_verification_key";
    case EntitlementState::invalid_signature: return "invalid_signature";
    case EntitlementState::wrong_product: return "wrong_product";
    case EntitlementState::wrong_edition: return "wrong_edition";
    case EntitlementState::not_yet_valid: return "not_yet_valid";
    case EntitlementState::expired: return "expired";
    case EntitlementState::wrong_installation: return "wrong_installation";
  }
  return "unknown";
}

}  // namespace smartcut::core
