#include "media/index_cache.hpp"

#include <cstdint>
#include <fstream>
#include <iomanip>
#include <iterator>
#include <sstream>
#include <vector>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#endif

#include "core/source_identity.hpp"
#include "media/access_unit_index.hpp"

namespace smartcut::media {
namespace {

std::string hash_text(const std::string& text) {
  std::uint64_t hash = 14695981039346656037ULL;
  for (const auto byte : text) {
    hash ^= static_cast<unsigned char>(byte);
    hash *= 1099511628211ULL;
  }
  std::ostringstream output;
  output << std::hex << std::setfill('0') << std::setw(16) << hash;
  return output.str();
}

std::string read_text(const std::filesystem::path& path) {
  std::ifstream input(path, std::ios::binary);
  return input ? std::string((std::istreambuf_iterator<char>(input)), {}) : std::string{};
}

std::string trim_line_endings(std::string text) {
  while (!text.empty() && (text.back() == '\r' || text.back() == '\n')) text.pop_back();
  return text;
}

std::string object_after(const std::string& document, const std::string& key) {
  const auto marker = "\"" + key + "\":";
  auto begin = document.find(marker);
  if (begin == std::string::npos) return {};
  begin = document.find('{', begin + marker.size());
  if (begin == std::string::npos) return {};
  std::size_t depth = 0;
  bool quoted = false;
  bool escaped = false;
  for (auto cursor = begin; cursor < document.size(); ++cursor) {
    const char value = document[cursor];
    if (quoted) {
      if (escaped) escaped = false;
      else if (value == '\\') escaped = true;
      else if (value == '"') quoted = false;
      continue;
    }
    if (value == '"') quoted = true;
    else if (value == '{') ++depth;
    else if (value == '}' && --depth == 0) return document.substr(begin, cursor - begin + 1);
  }
  return {};
}

std::string string_value(const std::string& document, const std::string& key) {
  const auto marker = "\"" + key + "\":\"";
  const auto begin = document.find(marker);
  if (begin == std::string::npos) return {};
  const auto value_begin = begin + marker.size();
  const auto end = document.find('"', value_begin);
  return end == std::string::npos ? std::string{} : document.substr(value_begin, end - value_begin);
}

const char* status_name(IndexCacheStatus status) {
  switch (status) {
    case IndexCacheStatus::valid: return "VALID";
    case IndexCacheStatus::stale_source: return "STALE_SOURCE";
    case IndexCacheStatus::corrupt: return "CORRUPT";
    case IndexCacheStatus::unsupported_schema: return "UNSUPPORTED_SCHEMA";
    case IndexCacheStatus::io_error: return "IO_ERROR";
  }
  return "CORRUPT";
}

bool replace_file(const std::filesystem::path& temporary, const std::filesystem::path& destination) {
#ifdef _WIN32
  return MoveFileExW(temporary.c_str(), destination.c_str(),
                     MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) != 0;
#else
  std::error_code error;
  std::filesystem::rename(temporary, destination, error);
  return !error;
#endif
}

}  // namespace

std::string IndexCacheValidation::to_json() const {
  std::ostringstream output;
  output << "{\"schema\":\"index-cache-validation-v1\",\"status\":\""
         << status_name(status) << "\",\"detail\":\"" << detail << "\"}";
  return output.str();
}

std::string make_hevc_index_cache(const std::filesystem::path& source) {
  std::ifstream input(source, std::ios::binary);
  if (!input) return {};
  const std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(input)), {});
  const auto index = index_hevc_annex_b(bytes).to_json();
  return make_index_cache_envelope(source, "index-cache-v3", index);
}

std::string make_index_cache_envelope(const std::filesystem::path& source,
                                      const std::string& envelope_schema,
                                      const std::string& index_document) {
  const auto identity = core::SourceIdentity::from_file(source).to_json();
  const auto payload_hash = hash_text(identity + "|" + index_document);
  return "{\"schema\":\"" + envelope_schema + "\",\"source_identity\":" + identity
      + ",\"index\":" + index_document + ",\"payload_hash\":\"" + payload_hash + "\"}";
}

IndexCacheValidation validate_hevc_index_cache(const std::filesystem::path& source,
                                                const std::string& document) {
  return validate_index_cache_envelope(source, document, "index-cache-v3");
}

IndexCacheValidation validate_index_cache_envelope(const std::filesystem::path& source,
                                                    const std::string& document,
                                                    const std::string& envelope_schema) {
  if (document.find("\"schema\":\"" + envelope_schema + "\"") == std::string::npos)
    return {IndexCacheStatus::unsupported_schema, "unexpected index cache schema"};
  const auto identity = object_after(document, "source_identity");
  const auto index = object_after(document, "index");
  const auto stored_hash = string_value(document, "payload_hash");
  if (identity.empty() || index.empty() || stored_hash.size() != 16)
    return {IndexCacheStatus::corrupt, "missing or truncated cache payload"};
  if (hash_text(identity + "|" + index) != stored_hash)
    return {IndexCacheStatus::corrupt, "cache payload checksum mismatch"};
  const auto canonical = "{\"schema\":\"" + envelope_schema + "\",\"source_identity\":" + identity
      + ",\"index\":" + index + ",\"payload_hash\":\"" + stored_hash + "\"}";
  if (trim_line_endings(document) != canonical)
    return {IndexCacheStatus::corrupt, "cache envelope is malformed or has trailing data"};
  try {
    const auto current = core::SourceIdentity::from_file(source);
    if (string_value(identity, "source_uuid") != current.source_uuid)
      return {IndexCacheStatus::stale_source, "source fingerprint changed"};
  } catch (...) {
    return {IndexCacheStatus::io_error, "source file could not be fingerprinted"};
  }
  return {IndexCacheStatus::valid, "cache checksum and source fingerprint match"};
}

std::string index_payload_from_cache(const std::string& document) { return object_after(document, "index"); }

bool write_index_cache_envelope_atomic(const std::filesystem::path& source,
                                       const std::filesystem::path& cache,
                                       const std::string& envelope_schema,
                                       const std::string& index_document) {
  const auto document = make_index_cache_envelope(source, envelope_schema, index_document);
  if (document.empty()) return false;
  const auto temporary = std::filesystem::path(cache.string() + ".tmp");
  {
    std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
    if (!output) return false;
    output << document << '\n';
    if (!output) return false;
  }
  if (replace_file(temporary, cache)) return true;
  std::error_code ignored;
  std::filesystem::remove(temporary, ignored);
  return false;
}

bool write_hevc_index_cache_atomic(const std::filesystem::path& source,
                                   const std::filesystem::path& cache) {
  std::ifstream input(source, std::ios::binary);
  if (!input) return false;
  const std::vector<std::uint8_t> bytes((std::istreambuf_iterator<char>(input)), {});
  return write_index_cache_envelope_atomic(source, cache, "index-cache-v3", index_hevc_annex_b(bytes).to_json());
}

IndexCacheValidation recover_hevc_index_cache(const std::filesystem::path& source,
                                               const std::filesystem::path& cache) {
  const auto existing = read_text(cache);
  const auto validation = validate_hevc_index_cache(source, existing);
  if (validation.valid()) return validation;
  if (!write_hevc_index_cache_atomic(source, cache))
    return {IndexCacheStatus::io_error, "cache rebuild or atomic replacement failed"};
  const auto rebuilt = read_text(cache);
  const auto rebuilt_validation = validate_hevc_index_cache(source, rebuilt);
  if (!rebuilt_validation.valid()) return rebuilt_validation;
  return {IndexCacheStatus::valid, "cache rebuilt atomically from source"};
}

}  // namespace smartcut::media
