#include "core/source_identity.hpp"
#include "core/json.hpp"

#include <algorithm>
#include <fstream>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <vector>

namespace smartcut::core {
namespace {
constexpr std::size_t kBlockSize = 64U * 1024U;

std::string hash_bytes(const std::vector<std::uint8_t>& bytes) {
  std::uint64_t hash = 1469598103934665603ULL;
  for (const auto byte : bytes) { hash ^= byte; hash *= 1099511628211ULL; }
  std::ostringstream output;
  output << std::hex << std::setfill('0') << std::setw(16) << hash;
  return output.str();
}

std::vector<std::uint8_t> read_block(const std::filesystem::path& path, std::uintmax_t offset, std::size_t length) {
  std::ifstream input(path, std::ios::binary);
  if (!input) throw std::runtime_error("cannot open source file");
  input.seekg(static_cast<std::streamoff>(offset));
  std::vector<std::uint8_t> bytes(length);
  input.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
  bytes.resize(static_cast<std::size_t>(input.gcount()));
  return bytes;
}

}  // namespace

SourceIdentity SourceIdentity::from_file(const std::filesystem::path& path) {
  const auto canonical = std::filesystem::weakly_canonical(path);
  if (!std::filesystem::is_regular_file(canonical)) throw std::runtime_error("source is not a regular file");
  const auto size = std::filesystem::file_size(canonical);
  const auto timestamp = std::filesystem::last_write_time(canonical).time_since_epoch().count();
  const auto block_length = static_cast<std::size_t>(std::min<std::uintmax_t>(size, kBlockSize));
  const auto first = read_block(canonical, 0, block_length);
  const auto last = read_block(canonical, size > kBlockSize ? size - kBlockSize : 0, block_length);

  SourceIdentity identity;
  const auto canonical_utf8 = canonical.u8string();
  identity.canonical_path.assign(reinterpret_cast<const char*>(canonical_utf8.data()), canonical_utf8.size());
  identity.byte_length = size;
  identity.modification_time = static_cast<std::int64_t>(timestamp);
  identity.first_block_hash = hash_bytes(first);
  identity.last_block_hash = hash_bytes(last);
  std::ostringstream seed;
  seed << identity.canonical_path << '|' << identity.byte_length << '|' << identity.modification_time
       << '|' << identity.first_block_hash << '|' << identity.last_block_hash;
  const auto seed_text = seed.str();
  identity.source_uuid = "source-" + hash_bytes(std::vector<std::uint8_t>(seed_text.begin(), seed_text.end()));
  return identity;
}

std::string SourceIdentity::to_json() const {
  std::ostringstream output;
  output << "{\"schema\":\"source-identity-v1\",\"canonical_path\":\"" << json_escape(canonical_path)
         << "\",\"byte_length\":" << byte_length << ",\"modification_time\":" << modification_time
         << ",\"first_block_hash\":\"" << first_block_hash << "\",\"last_block_hash\":\""
         << last_block_hash << "\",\"source_uuid\":\"" << source_uuid << "\"}";
  return output.str();
}
}  // namespace smartcut::core
