#include "media/video_frame_index.hpp"

#include <algorithm>
#include <charconv>
#include <cstdint>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <limits>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>

#include "core/source_identity.hpp"
#include "media/ffmpeg_raii.hpp"
#include "media/index_cache.hpp"
#include "media/nal_inventory.hpp"

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavutil/frame.h>
}
#endif

namespace smartcut::media {

#ifdef SMARTCUT_HAS_FFMPEG
namespace {
struct PacketMetadata {
  std::size_t number{};
  std::int64_t position{-1};
  int size{};
  std::int64_t pts{AV_NOPTS_VALUE};
  std::int64_t dts{AV_NOPTS_VALUE};
  std::int64_t duration{};
  bool container_key{};
  bool corrupt{};
  bool has_vcl{};
  unsigned vcl_nal_type{};
  unsigned random_access_nal_type{};
  bool parameter_sets_in_band{};
  std::string parameter_set_hash;
  std::size_t rpu_count{};
  std::string rpu_hash;
  std::string active_parameter_set_hash;
  std::size_t gop_number{};
  bool closed_gop{};
  bool dependency_safe{};
  bool requires_leading_frames{};
  bool discardable_leading{};
  std::size_t decode_start_packet{};
  bool consumed{};
};

std::string hash_bytes(const std::uint8_t* data, std::size_t size) {
  if (data == nullptr || size == 0) return {};
  std::uint64_t hash = 14695981039346656037ULL;
  for (std::size_t i = 0; i < size; ++i) { hash ^= data[i]; hash *= 1099511628211ULL; }
  std::ostringstream output; output << std::hex << std::setfill('0') << std::setw(16) << hash; return output.str();
}

std::string hex_bytes(const std::uint8_t* data, std::size_t size) {
  if (!data || size == 0) return {};
  std::ostringstream output;
  output << std::hex << std::setfill('0');
  for (std::size_t index = 0; index < size; ++index)
    output << std::setw(2) << static_cast<unsigned>(data[index]);
  return output.str();
}

const char* random_access_name(Codec codec, unsigned type) {
  if (codec == Codec::h264) return type == 5U ? "H264_IDR" : "NONE";
  switch (type) {
    case 16: return "BLA_W_LP"; case 17: return "BLA_W_RADL"; case 18: return "BLA_N_LP";
    case 19: return "IDR_W_RADL"; case 20: return "IDR_N_LP"; case 21: return "CRA";
    case 22: return "RSV_IRAP_VCL22"; case 23: return "RSV_IRAP_VCL23"; default: return "NONE";
  }
}

const char* vcl_name(Codec codec, bool present, unsigned type) {
  if (!present) return "NONE";
  if (codec == Codec::h264) return type == 5U ? "H264_IDR" : "H264_NON_IDR";
  switch (type) {
    case 0: return "TRAIL_N"; case 1: return "TRAIL_R"; case 2: return "TSA_N"; case 3: return "TSA_R";
    case 4: return "STSA_N"; case 5: return "STSA_R"; case 6: return "RADL_N"; case 7: return "RADL_R";
    case 8: return "RASL_N"; case 9: return "RASL_R"; case 16: return "BLA_W_LP"; case 17: return "BLA_W_RADL";
    case 18: return "BLA_N_LP"; case 19: return "IDR_W_RADL"; case 20: return "IDR_N_LP"; case 21: return "CRA";
    case 22: return "RSV_IRAP_VCL22"; case 23: return "RSV_IRAP_VCL23"; default: return "OTHER_VCL";
  }
}

bool closed_random_access(Codec codec, unsigned type) {
  return codec == Codec::h264 ? type == 5U : type >= 16U && type <= 20U;
}

ffmpeg::Bsf make_annex_b_filter(const AVStream* stream) {
  if (stream->codecpar->codec_id != AV_CODEC_ID_HEVC && stream->codecpar->codec_id != AV_CODEC_ID_H264) return {};
  const char* name = stream->codecpar->codec_id == AV_CODEC_ID_HEVC ? "hevc_mp4toannexb" : "h264_mp4toannexb";
  const auto* filter = av_bsf_get_by_name(name);
  AVBSFContext* raw = nullptr;
  if (filter == nullptr || av_bsf_alloc(filter, &raw) < 0) return {};
  ffmpeg::Bsf context(raw);
  if (avcodec_parameters_copy(context->par_in, stream->codecpar) < 0) return {};
  context->time_base_in = stream->time_base;
  if (av_bsf_init(context.get()) < 0) return {};
  return context;
}

void classify_packet(PacketMetadata& metadata, AVPacket* packet, ffmpeg::Bsf& filter, Codec codec) {
  auto inspect = [&](const AVPacket* candidate) {
    const auto inventory = inventory_annex_b(codec, {candidate->data, static_cast<std::size_t>(candidate->size)});
    std::vector<std::uint8_t> parameter_sets;
    std::vector<std::uint8_t> rpu_payloads;
    for (const auto& unit : inventory.units) {
      if (unit.type <= (codec == Codec::h264 ? 5U : 31U) && !metadata.has_vcl) {
        metadata.has_vcl = true;
        metadata.vcl_nal_type = unit.type;
      }
      if (unit.parameter_set) {
        metadata.parameter_sets_in_band = true;
        parameter_sets.insert(parameter_sets.end(), candidate->data + unit.offset, candidate->data + unit.offset + unit.size);
      }
      if (unit.random_access && metadata.random_access_nal_type == 0U) metadata.random_access_nal_type = unit.type;
      if (unit.dolby_vision_rpu) {
        ++metadata.rpu_count;
        rpu_payloads.insert(rpu_payloads.end(), candidate->data + unit.offset, candidate->data + unit.offset + unit.size);
      }
    }
    if (!parameter_sets.empty()) metadata.parameter_set_hash = hash_bytes(parameter_sets.data(), parameter_sets.size());
    if (!rpu_payloads.empty()) metadata.rpu_hash = hash_bytes(rpu_payloads.data(), rpu_payloads.size());
  };
  if (!filter) { inspect(packet); return; }
  auto filtered = ffmpeg::make_packet();
  if (!filtered) return;
  if (av_packet_ref(filtered.get(), packet) < 0 || av_bsf_send_packet(filter.get(), filtered.get()) < 0) return;
  while (av_bsf_receive_packet(filter.get(), filtered.get()) >= 0) { inspect(filtered.get()); av_packet_unref(filtered.get()); }
}

PacketMetadata* packet_for_frame(std::vector<PacketMetadata>& packets, std::size_t& first_unconsumed, std::int64_t pts) {
  PacketMetadata* selected = nullptr;
  for (std::size_t index = first_unconsumed; index < packets.size(); ++index) if (!packets[index].consumed && packets[index].pts == pts) { selected = &packets[index]; break; }
  if (selected == nullptr) for (std::size_t index = first_unconsumed; index < packets.size(); ++index) if (!packets[index].consumed) { selected = &packets[index]; break; }
  if (selected == nullptr) return nullptr;
  selected->consumed = true;
  while (first_unconsumed < packets.size() && packets[first_unconsumed].consumed) ++first_unconsumed;
  return selected;
}

std::string side_hash(const AVFrame* frame, AVFrameSideDataType type) {
  const auto* data = av_frame_get_side_data(frame, type);
  return data == nullptr ? std::string{} : hash_bytes(data->data, data->size);
}
}  // namespace
#endif

std::string build_video_frame_index(const std::filesystem::path& path, std::size_t stream_index) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)stream_index;
  return "{\"schema\":\"video-frame-index-v2\",\"error\":\"FFmpeg support is not enabled\"}";
#else
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0)
    return "{\"schema\":\"video-frame-index-v2\",\"error\":\"input open failed\"}";
  std::vector<int> video_streams;
  for (unsigned i = 0; i < format->nb_streams; ++i)
    if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) video_streams.push_back(static_cast<int>(i));
  if (stream_index >= video_streams.size()) return "{\"schema\":\"video-frame-index-v2\",\"error\":\"video stream not found\"}";
  const int selected = video_streams[stream_index];
  auto* stream = format->streams[selected];
  const auto* decoder = ffmpeg::preferred_video_decoder(stream->codecpar->codec_id);
  auto context = ffmpeg::make_codec_context(decoder);
  if (!context || avcodec_parameters_to_context(context.get(), stream->codecpar) < 0 || avcodec_open2(context.get(), decoder, nullptr) < 0)
    return "{\"schema\":\"video-frame-index-v2\",\"error\":\"decoder open failed\"}";
  auto packet = ffmpeg::make_packet(); auto frame = ffmpeg::make_frame(); auto filter = make_annex_b_filter(stream);
  if (!packet || !frame) return "{\"schema\":\"video-frame-index-v2\",\"error\":\"allocation failed\"}";
  const bool nal_inventory_supported = stream->codecpar->codec_id == AV_CODEC_ID_H264 || stream->codecpar->codec_id == AV_CODEC_ID_HEVC;
  const Codec inventory_codec = stream->codecpar->codec_id == AV_CODEC_ID_H264 ? Codec::h264 : Codec::hevc;
  std::vector<PacketMetadata> packets; std::ostringstream frames; std::ostringstream keyframes; std::size_t display_frame = 0; std::size_t first_unconsumed_packet = 0; bool first_json = true; bool first_keyframe = true;
  auto receive = [&] {
    while (avcodec_receive_frame(context.get(), frame.get()) >= 0) {
      const auto pts = frame->best_effort_timestamp != AV_NOPTS_VALUE ? frame->best_effort_timestamp : frame->pts;
      auto* source_packet = packet_for_frame(packets, first_unconsumed_packet, pts);
      const unsigned ra_type = source_packet == nullptr ? 0U : source_packet->random_access_nal_type;
      const bool random_access = source_packet != nullptr && (nal_inventory_supported ? ra_type != 0U : (source_packet->container_key || frame->pict_type == AV_PICTURE_TYPE_I));
      if (random_access) {
        if (!first_keyframe) keyframes << ',';
        first_keyframe = false;
        keyframes << display_frame;
      }
      if (!first_json) frames << ',';
      first_json = false;
      const auto mastering = side_hash(frame.get(), AV_FRAME_DATA_MASTERING_DISPLAY_METADATA);
      const auto cll = side_hash(frame.get(), AV_FRAME_DATA_CONTENT_LIGHT_LEVEL);
      const auto hdr10plus = side_hash(frame.get(), AV_FRAME_DATA_DYNAMIC_HDR_PLUS);
      const auto dovi_rpu = side_hash(frame.get(), AV_FRAME_DATA_DOVI_RPU_BUFFER);
      const auto captions = side_hash(frame.get(), AV_FRAME_DATA_A53_CC);
      const auto* caption_side = av_frame_get_side_data(frame.get(), AV_FRAME_DATA_A53_CC);
      frames << "{\"stream_index\":" << selected << ",\"packet_number\":" << (source_packet ? source_packet->number : 0)
             << ",\"file_position\":" << (source_packet ? source_packet->position : -1) << ",\"packet_size\":" << (source_packet ? source_packet->size : 0)
             << ",\"pts\":" << pts << ",\"dts\":" << (source_packet ? source_packet->dts : AV_NOPTS_VALUE)
             << ",\"duration\":" << (source_packet ? source_packet->duration : 0) << ",\"time_base_num\":" << stream->time_base.num
             << ",\"time_base_den\":" << stream->time_base.den << ",\"display_frame\":" << display_frame
             << ",\"frame_type\":\"" << av_get_picture_type_char(frame->pict_type) << "\",\"container_key\":" << (source_packet && source_packet->container_key ? "true" : "false")
             << ",\"vcl_nal_type\":" << (source_packet ? source_packet->vcl_nal_type : 0U)
             << ",\"vcl_type\":\"" << (nal_inventory_supported ? vcl_name(inventory_codec, source_packet && source_packet->has_vcl, source_packet ? source_packet->vcl_nal_type : 0U) : "MPEG2_PICTURE")
             << "\",\"random_access_type\":\"" << (nal_inventory_supported ? random_access_name(inventory_codec, ra_type) : (random_access ? "MPEG2_I" : "NONE")) << "\",\"parameter_sets_in_band\":" << (source_packet && source_packet->parameter_sets_in_band ? "true" : "false")
             << ",\"parameter_set_hash\":\"" << (source_packet ? source_packet->active_parameter_set_hash : std::string{})
             << "\",\"rpu_count\":" << (source_packet ? source_packet->rpu_count : 0)
             << ",\"gop_number\":" << (source_packet ? source_packet->gop_number : 0)
             << ",\"closed_gop\":" << (source_packet && source_packet->closed_gop ? "true" : "false")
             << ",\"dependency_safe\":" << (source_packet && source_packet->dependency_safe ? "true" : "false")
             << ",\"requires_leading_frames\":" << (source_packet && source_packet->requires_leading_frames ? "true" : "false")
             << ",\"discardable_leading\":" << (source_packet && source_packet->discardable_leading ? "true" : "false")
             << ",\"decode_start_packet\":" << (source_packet ? source_packet->decode_start_packet : 0)
             << ",\"pixel_format\":" << frame->format << ",\"width\":" << frame->width << ",\"height\":" << frame->height
             << ",\"color_range\":" << frame->color_range << ",\"color_primaries\":" << frame->color_primaries
             << ",\"color_transfer\":" << frame->color_trc << ",\"color_matrix\":" << frame->colorspace
             << ",\"sar_num\":" << frame->sample_aspect_ratio.num << ",\"sar_den\":" << frame->sample_aspect_ratio.den
             << ",\"interlaced\":" << ((frame->flags & AV_FRAME_FLAG_INTERLACED) ? "true" : "false")
             << ",\"mastering_hash\":\"" << mastering << "\",\"cll_hash\":\"" << cll << "\",\"hdr10_plus_hash\":\"" << hdr10plus
             << "\",\"dovi_rpu_hash\":\"" << (dovi_rpu.empty() && source_packet ? source_packet->rpu_hash : dovi_rpu)
             << "\",\"a53_caption_hash\":\"" << captions
             << "\",\"a53_caption_bytes\":" << (caption_side ? caption_side->size : 0)
             << ",\"a53_caption_data\":\"" << (caption_side ? hex_bytes(caption_side->data, caption_side->size) : std::string{})
             << "\",\"decode_corrupt\":"
             << (((frame->flags & AV_FRAME_FLAG_CORRUPT) || (source_packet && source_packet->corrupt)) ? "true" : "false") << '}';
      ++display_frame; av_frame_unref(frame.get());
    }
  };
  std::size_t packet_number = 0; std::size_t current_gop = 0; bool current_gop_closed = false;
  std::size_t decode_start_packet = 0; std::string active_parameter_set_hash;
  while (av_read_frame(format.get(), packet.get()) >= 0) {
    if (packet->stream_index == selected) {
      PacketMetadata metadata;
      metadata.number = packet_number++;
      metadata.position = packet->pos;
      metadata.size = packet->size;
      metadata.pts = packet->pts;
      metadata.dts = packet->dts;
      metadata.duration = packet->duration;
      metadata.container_key = (packet->flags & AV_PKT_FLAG_KEY) != 0;
      metadata.corrupt = (packet->flags & AV_PKT_FLAG_CORRUPT) != 0;
      if (nal_inventory_supported) classify_packet(metadata, packet.get(), filter, inventory_codec);
      if (!metadata.parameter_set_hash.empty()) active_parameter_set_hash = metadata.parameter_set_hash;
      const bool packet_random_access = nal_inventory_supported ? metadata.random_access_nal_type != 0U : metadata.container_key;
      if (packet_random_access) {
        if (metadata.number != 0) ++current_gop;
        current_gop_closed = nal_inventory_supported ? closed_random_access(inventory_codec, metadata.random_access_nal_type) : true;
        if (current_gop_closed || metadata.number == 0) decode_start_packet = metadata.number;
      }
      metadata.active_parameter_set_hash = active_parameter_set_hash;
      metadata.gop_number = current_gop;
      metadata.closed_gop = current_gop_closed;
      metadata.requires_leading_frames = nal_inventory_supported && inventory_codec == Codec::hevc && metadata.vcl_nal_type >= 6U && metadata.vcl_nal_type <= 9U;
      metadata.discardable_leading = nal_inventory_supported && inventory_codec == Codec::hevc && metadata.vcl_nal_type >= 8U && metadata.vcl_nal_type <= 9U;
      metadata.dependency_safe = current_gop_closed && !metadata.requires_leading_frames;
      metadata.decode_start_packet = decode_start_packet;
      packets.push_back(std::move(metadata));
      if (avcodec_send_packet(context.get(), packet.get()) >= 0) receive();
    }
    av_packet_unref(packet.get());
  }
  avcodec_send_packet(context.get(), nullptr); receive();
  const auto identity = core::SourceIdentity::from_file(path);
  std::ostringstream output; output << "{\"schema\":\"video-frame-index-v2\",\"indexer_revision\":2,\"source_uuid\":\"" << identity.source_uuid
    << "\",\"video_stream\":" << selected << ",\"codec\":\"" << avcodec_get_name(stream->codecpar->codec_id)
    << "\",\"time_base_num\":" << stream->time_base.num << ",\"time_base_den\":" << stream->time_base.den
    << ",\"packet_count\":" << packets.size() << ",\"frame_count\":" << display_frame
    << ",\"keyframes\":[" << keyframes.str() << "],\"frames\":[" << frames.str() << "]}";
  return output.str();
#endif
}

std::string build_fast_video_navigation(const std::filesystem::path& path, std::size_t stream_index) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; (void)stream_index;
  return "{\"schema\":\"worker-navigation-v1\",\"error\":\"FFmpeg support is not enabled\"}";
#else
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0)
    return "{\"schema\":\"worker-navigation-v1\",\"error\":\"input open failed\"}";
  std::vector<int> video_streams;
  for (unsigned index = 0; index < format->nb_streams; ++index)
    if (format->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
      video_streams.push_back(static_cast<int>(index));
  if (stream_index >= video_streams.size())
    return "{\"schema\":\"worker-navigation-v1\",\"error\":\"video stream not found\"}";
  auto* stream = format->streams[video_streams[stream_index]];
  const auto nominal_frame_rate = stream->r_frame_rate;
  const auto average_frame_rate = stream->avg_frame_rate;
  const auto metadata_integer = [&](const char* key) -> std::int64_t {
    const auto* value = av_dict_get(stream->metadata, key, nullptr, 0);
    if (!value || !value->value) return 0;
    try { return std::stoll(value->value); } catch (...) { return 0; }
  };
  AVRational frame_rate = average_frame_rate;
  const auto tagged_rate_num = metadata_integer("SMARTCUT_FRAME_RATE_NUM");
  const auto tagged_rate_den = metadata_integer("SMARTCUT_FRAME_RATE_DEN");
  if (tagged_rate_num > 0 && tagged_rate_num <= std::numeric_limits<int>::max() &&
      tagged_rate_den > 0 && tagged_rate_den <= std::numeric_limits<int>::max())
    frame_rate = AVRational{static_cast<int>(tagged_rate_num), static_cast<int>(tagged_rate_den)};
  else if (nominal_frame_rate.num <= 0 || nominal_frame_rate.den <= 0 ||
           average_frame_rate.num <= 0 || average_frame_rate.den <= 0 ||
           av_cmp_q(nominal_frame_rate, average_frame_rate) != 0)
    return "{\"schema\":\"worker-navigation-v1\",\"error\":\"variable frame timing requires a detailed index\"}";
  if (stream->time_base.num <= 0 || stream->time_base.den <= 0)
    return "{\"schema\":\"worker-navigation-v1\",\"error\":\"constant frame timing is unavailable\"}";
  std::int64_t duration = stream->duration;
  if (duration <= 0 || duration == AV_NOPTS_VALUE) {
    if (format->duration <= 0 || format->duration == AV_NOPTS_VALUE)
      return "{\"schema\":\"worker-navigation-v1\",\"error\":\"source duration is unavailable\"}";
    duration = av_rescale_q(format->duration, AV_TIME_BASE_Q, stream->time_base);
  }
  const auto frame_time_base = AVRational{frame_rate.den, frame_rate.num};
  const auto first_pts = stream->start_time == AV_NOPTS_VALUE ? std::int64_t{0} : stream->start_time;
  std::int64_t frame_count = metadata_integer("SMARTCUT_FRAME_COUNT");
  if (frame_count <= 0) {
    std::int64_t last_pts = AV_NOPTS_VALUE;
    const auto ten_seconds = av_rescale_q(10, AVRational{1, 1}, stream->time_base);
    const auto seek_target = first_pts + std::max<std::int64_t>(0, duration - ten_seconds);
    if (av_seek_frame(format.get(), video_streams[stream_index], seek_target, AVSEEK_FLAG_BACKWARD) >= 0) {
      auto packet = ffmpeg::make_packet();
      while (packet && av_read_frame(format.get(), packet.get()) >= 0) {
        if (packet->stream_index == video_streams[stream_index]) {
          const auto pts = packet->pts != AV_NOPTS_VALUE ? packet->pts : packet->dts;
          if (pts != AV_NOPTS_VALUE && (last_pts == AV_NOPTS_VALUE || pts > last_pts)) last_pts = pts;
        }
        av_packet_unref(packet.get());
      }
    }
    if (last_pts != AV_NOPTS_VALUE && last_pts >= first_pts)
      frame_count = av_rescale_q_rnd(last_pts - first_pts, stream->time_base, frame_time_base, AV_ROUND_NEAR_INF) + 1;
    if (frame_count <= 0) frame_count = stream->nb_frames;
    if (frame_count <= 0) frame_count = av_rescale_q_rnd(duration, stream->time_base, frame_time_base, AV_ROUND_UP);
  }
  if (frame_count <= 0 || frame_count > 100000000)
    return "{\"schema\":\"worker-navigation-v1\",\"error\":\"derived frame count is invalid\"}";
  const auto identity = core::SourceIdentity::from_file(path);
  std::ostringstream output;
  output << "{\"schema\":\"worker-navigation-v1\",\"source_uuid\":\"" << identity.source_uuid
         << "\",\"time_base_num\":" << stream->time_base.num << ",\"time_base_den\":" << stream->time_base.den
         << ",\"frame_rate_num\":" << frame_rate.num << ",\"frame_rate_den\":" << frame_rate.den
         << ",\"frame_count\":" << frame_count << ",\"keyframes\":[0],\"frame_pts\":[";
  for (std::int64_t frame = 0; frame < frame_count; ++frame) {
    if (frame != 0) output << ',';
    output << first_pts + av_rescale_q(frame, frame_time_base, stream->time_base);
  }
  output << "],\"timing_source\":\"container-cfr\"}";
  return output.str();
#endif
}

std::string load_or_build_video_frame_index(const std::filesystem::path& path,
                                            std::size_t stream_index,
                                            const std::filesystem::path& cache_path) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)cache_path;
  return build_video_frame_index(path, stream_index);
#else
  std::filesystem::path cache = cache_path;
  if (cache.empty()) {
    const auto identity = core::SourceIdentity::from_file(path);
    const char* local_app_data = std::getenv("LOCALAPPDATA");
    const auto root = local_app_data != nullptr ? std::filesystem::path(local_app_data)
                                                 : std::filesystem::temp_directory_path();
    cache = root / "SmartCut64" / "indexes" /
        (identity.source_uuid + "-video-" + std::to_string(stream_index) + ".json");
  }
  std::error_code directory_error;
  if (!cache.parent_path().empty()) std::filesystem::create_directories(cache.parent_path(), directory_error);
  if (!directory_error) {
    std::ifstream input(cache, std::ios::binary);
    const std::string document(input ? std::string((std::istreambuf_iterator<char>(input)), {}) : std::string{});
    if (validate_index_cache_envelope(path, document, "video-frame-index-cache-v2").valid()) {
      const auto payload = index_payload_from_cache(document);
      if (payload.find("\"schema\":\"video-frame-index-v2\"") != std::string::npos && payload.find("\"indexer_revision\":2") != std::string::npos) return payload;
    }
  }
  const auto index = build_video_frame_index(path, stream_index);
  if (index.find("\"error\"") != std::string::npos) return index;
  if (!directory_error) write_index_cache_envelope_atomic(path, cache, "video-frame-index-cache-v2", index);
  return index;
#endif
}

namespace {

#ifdef SMARTCUT_HAS_FFMPEG
std::string build_video_frame_pts_index(const std::filesystem::path& path,
                                        std::size_t stream_index) {
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0)
    return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"input open failed\"}";
  std::vector<int> video_streams;
  for (unsigned index = 0; index < format->nb_streams; ++index)
    if (format->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
      video_streams.push_back(static_cast<int>(index));
  if (stream_index >= video_streams.size())
    return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video stream not found\"}";
  const int selected = video_streams[stream_index];
  const auto* stream = format->streams[selected];
  auto packet = ffmpeg::make_packet();
  if (!packet)
    return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"allocation failed\"}";

  // Supported video demuxers expose one compressed access unit per packet.
  // Packet PTS is presentation order even when B-frames make packet order
  // differ, so sorting the unique timestamps produces the display-frame map
  // without a cold-cache software decode of the entire source. DTS must remain
  // strictly increasing in decode order; resets/wraps therefore fail closed
  // and the boundary encoder uses sequential decoding instead of guessing.
  struct PacketPresentationTimestamp {
    std::int64_t pts{AV_NOPTS_VALUE};
    std::size_t decode_order{};
  };
  std::vector<PacketPresentationTimestamp> presentation_timestamps;
  std::int64_t previous_dts = AV_NOPTS_VALUE;
  bool have_decode_timestamp = false;
  while (true) {
    const int read = av_read_frame(format.get(), packet.get());
    if (read < 0) {
      if (read != AVERROR_EOF)
        return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"packet read failed\"}";
      break;
    }
    if (packet->stream_index == selected) {
      if (packet->pts == AV_NOPTS_VALUE ||
          (have_decode_timestamp && packet->dts == AV_NOPTS_VALUE)) {
        av_packet_unref(packet.get());
        return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video packet timestamp is unavailable\"}";
      }
      if (packet->dts != AV_NOPTS_VALUE) {
        if (have_decode_timestamp && packet->dts <= previous_dts) {
          av_packet_unref(packet.get());
          return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video decode timestamps are not globally increasing\"}";
        }
        previous_dts = packet->dts;
        have_decode_timestamp = true;
      }
      presentation_timestamps.push_back(
          {packet->pts, presentation_timestamps.size()});
    }
    av_packet_unref(packet.get());
  }
  if (presentation_timestamps.empty() || !have_decode_timestamp)
    return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video decode timestamps are unavailable\"}";
  std::sort(presentation_timestamps.begin(), presentation_timestamps.end(),
            [](const auto& left, const auto& right) { return left.pts < right.pts; });
  const auto maximum_reorder = stream->codecpar->video_delay;
  if (maximum_reorder < 0)
    return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video reorder depth is unavailable\"}";
  for (std::size_t presentation_order = 0;
       presentation_order < presentation_timestamps.size(); ++presentation_order) {
    const auto& timestamp = presentation_timestamps[presentation_order];
    if (presentation_order != 0 &&
        timestamp.pts == presentation_timestamps[presentation_order - 1].pts)
      return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video presentation timestamps are missing or duplicated\"}";
    const auto displacement = presentation_order >= timestamp.decode_order
                                  ? presentation_order - timestamp.decode_order
                                  : timestamp.decode_order - presentation_order;
    if (displacement > static_cast<std::size_t>(maximum_reorder))
      return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"video presentation timestamps exceed the declared reorder depth\"}";
  }

  std::ostringstream pts;
  for (std::size_t index = 0; index < presentation_timestamps.size(); ++index) {
    if (index != 0) pts << ',';
    pts << presentation_timestamps[index].pts;
  }
  const auto identity = core::SourceIdentity::from_file(path);
  std::ostringstream output;
  output << "{\"schema\":\"video-frame-pts-index-v2\",\"indexer_revision\":2,\"source_uuid\":\""
         << identity.source_uuid << "\",\"video_stream\":" << selected
         << ",\"index_method\":\"packet-presentation-order\",\"frame_count\":"
         << presentation_timestamps.size() << ",\"pts\":[" << pts.str() << "]}";
  return output.str();
}
#endif

std::string load_or_build_video_frame_pts_index(const std::filesystem::path& path,
                                                std::size_t stream_index) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path;
  (void)stream_index;
  return "{\"schema\":\"video-frame-pts-index-v2\",\"error\":\"FFmpeg support is not enabled\"}";
#else
  const auto identity = core::SourceIdentity::from_file(path);
  const char* local_app_data = std::getenv("LOCALAPPDATA");
  const auto root = local_app_data != nullptr ? std::filesystem::path(local_app_data)
                                               : std::filesystem::temp_directory_path();
  const auto cache = root / "SmartCut64" / "indexes" /
      (identity.source_uuid + "-video-" + std::to_string(stream_index) + "-pts-v2.json");
  std::error_code directory_error;
  std::filesystem::create_directories(cache.parent_path(), directory_error);
  if (!directory_error) {
    std::ifstream input(cache, std::ios::binary);
    const std::string document(
        input ? std::string((std::istreambuf_iterator<char>(input)), {}) : std::string{});
    if (validate_index_cache_envelope(path, document, "video-frame-pts-cache-v2").valid()) {
      const auto payload = index_payload_from_cache(document);
      if (payload.find("\"schema\":\"video-frame-pts-index-v2\"") != std::string::npos &&
          payload.find("\"indexer_revision\":2") != std::string::npos)
        return payload;
    }
  }
  const auto index = build_video_frame_pts_index(path, stream_index);
  if (index.find("\"error\"") != std::string::npos) return index;
  if (!directory_error)
    write_index_cache_envelope_atomic(path, cache, "video-frame-pts-cache-v2", index);
  return index;
#endif
}

}  // namespace

std::optional<IndexedFramePtsRange> indexed_frame_pts_range(
    const std::filesystem::path& path, std::uint64_t first_frame,
    std::uint64_t end_frame, std::size_t stream_index) {
  if (end_frame <= first_frame ||
      end_frame - first_frame > std::numeric_limits<std::size_t>::max())
    return std::nullopt;
  const auto index = load_or_build_video_frame_pts_index(path, stream_index);
  if (index.find("\"schema\":\"video-frame-pts-index-v2\"") == std::string::npos ||
      index.find("\"indexer_revision\":2") == std::string::npos)
    return std::nullopt;
  const auto frame_count_marker = index.find("\"frame_count\":");
  const auto pts_marker = index.find("\"pts\":[");
  if (frame_count_marker == std::string::npos || pts_marker == std::string::npos)
    return std::nullopt;

  const auto parse_u64 = [&](std::size_t begin, std::size_t end) -> std::optional<std::uint64_t> {
    if (begin >= end || end > index.size()) return std::nullopt;
    std::uint64_t value = 0;
    const auto* first = index.data() + begin;
    const auto* last = index.data() + end;
    const auto parsed = std::from_chars(first, last, value);
    if (parsed.ec != std::errc{} || parsed.ptr != last) return std::nullopt;
    return value;
  };
  const auto parse_i64 = [&](std::size_t begin, std::size_t end)
      -> std::optional<std::int64_t> {
    if (begin >= end || end > index.size()) return std::nullopt;
    std::int64_t value = 0;
    const auto* first = index.data() + begin;
    const auto* last = index.data() + end;
    const auto parsed = std::from_chars(first, last, value);
    if (parsed.ec != std::errc{} || parsed.ptr != last) return std::nullopt;
    return value;
  };
  const auto frame_count_begin = frame_count_marker + std::string_view("\"frame_count\":").size();
  const auto frame_count_end = index.find(',', frame_count_begin);
  const auto frame_count = parse_u64(frame_count_begin, frame_count_end);
  if (!frame_count || end_frame > *frame_count ||
      *frame_count > std::numeric_limits<std::size_t>::max())
    return std::nullopt;

  IndexedFramePtsRange result;
  result.first_frame = first_frame;
  const auto requested_count = static_cast<std::size_t>(end_frame - first_frame);
  result.pts.reserve(requested_count);
  std::size_t cursor = pts_marker + std::string_view("\"pts\":[").size();
  std::int64_t previous_pts = std::numeric_limits<std::int64_t>::min();
  for (std::uint64_t frame_number = 0; frame_number < *frame_count; ++frame_number) {
    const auto delimiter = frame_number + 1 == *frame_count ? ']' : ',';
    const auto value_end = index.find(delimiter, cursor);
    if (value_end == std::string::npos) return std::nullopt;
    const auto pts = parse_i64(cursor, value_end);
    if (!pts) return std::nullopt;
    if (*pts == std::numeric_limits<std::int64_t>::min() ||
        (frame_number != 0 && *pts <= previous_pts))
      return std::nullopt;
    previous_pts = *pts;
    if (frame_number >= first_frame && frame_number < end_frame)
      result.pts.push_back(*pts);
    cursor = value_end + 1;
  }
  return result.pts.size() == requested_count ? std::optional<IndexedFramePtsRange>(result)
                                               : std::nullopt;
}

std::optional<std::int64_t> indexed_frame_pts(const std::filesystem::path& path,
                                              std::size_t display_frame,
                                              std::size_t stream_index) {
  const auto index = load_or_build_video_frame_index(path, stream_index);
  const auto frame_marker = "\"display_frame\":" + std::to_string(display_frame) + ",";
  const auto frame_position = index.find(frame_marker);
  if (frame_position == std::string::npos) return std::nullopt;
  const auto pts_marker = index.rfind("\"pts\":", frame_position);
  if (pts_marker == std::string::npos) return std::nullopt;
  const auto value_begin = pts_marker + 6;
  const auto value_end = index.find(',', value_begin);
  try { return std::stoll(index.substr(value_begin, value_end - value_begin)); }
  catch (...) { return std::nullopt; }
}

}  // namespace smartcut::media
