#include "media/video_transform.hpp"

#include <algorithm>
#include <cmath>
#include <filesystem>
#include <sstream>
#include <string_view>
#include <vector>

#include "media/ffmpeg_raii.hpp"
#include "media/trial_watermark.hpp"

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavfilter/avfilter.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/avstring.h>
#include <libavutil/hwcontext.h>
#include <libavutil/opt.h>
#include <libavutil/pixdesc.h>
}
#endif

namespace smartcut::media {
namespace {

#ifdef SMARTCUT_HAS_FFMPEG
struct FilterGraph {
  AVFilterGraph* graph = nullptr;
  AVFilterContext* source = nullptr;
  AVFilterContext* sink = nullptr;
  ~FilterGraph() { avfilter_graph_free(&graph); }
};

std::string ffmpeg_error(int code) {
  char text[AV_ERROR_MAX_STRING_SIZE]{};
  av_strerror(code, text, sizeof(text));
  return text;
}

AVCodecID requested_codec_id(VideoOutputCodec codec, AVCodecID source) {
  switch (codec) {
    case VideoOutputCodec::source: return source;
    case VideoOutputCodec::mpeg2: return AV_CODEC_ID_MPEG2VIDEO;
    case VideoOutputCodec::h264: return AV_CODEC_ID_H264;
    case VideoOutputCodec::hevc: return AV_CODEC_ID_HEVC;
    case VideoOutputCodec::av1: return AV_CODEC_ID_AV1;
    case VideoOutputCodec::vp9: return AV_CODEC_ID_VP9;
    case VideoOutputCodec::ffv1: return AV_CODEC_ID_FFV1;
    case VideoOutputCodec::prores: return AV_CODEC_ID_PRORES;
  }
  return source;
}

const char* software_encoder_name(AVCodecID codec) {
  switch (codec) {
    case AV_CODEC_ID_H264: return "libx264";
    case AV_CODEC_ID_HEVC: return "libx265";
    case AV_CODEC_ID_AV1: return "libaom-av1";
    case AV_CODEC_ID_VP9: return "libvpx-vp9";
    case AV_CODEC_ID_MPEG2VIDEO: return "mpeg2video";
    case AV_CODEC_ID_FFV1: return "ffv1";
    case AV_CODEC_ID_PRORES: return "prores_ks";
    default: return nullptr;
  }
}

const char* hardware_encoder_name(AVCodecID codec, VideoHardwareEncoder hardware) {
  if (hardware == VideoHardwareEncoder::qsv) {
    if (codec == AV_CODEC_ID_H264) return "h264_qsv";
    if (codec == AV_CODEC_ID_HEVC) return "hevc_qsv";
    if (codec == AV_CODEC_ID_AV1) return "av1_qsv";
    if (codec == AV_CODEC_ID_MPEG2VIDEO) return "mpeg2_qsv";
  }
  if (hardware == VideoHardwareEncoder::nvenc) {
    if (codec == AV_CODEC_ID_H264) return "h264_nvenc";
    if (codec == AV_CODEC_ID_HEVC) return "hevc_nvenc";
    if (codec == AV_CODEC_ID_AV1) return "av1_nvenc";
  }
  if (hardware == VideoHardwareEncoder::amf) {
    if (codec == AV_CODEC_ID_H264) return "h264_amf";
    if (codec == AV_CODEC_ID_HEVC) return "hevc_amf";
    if (codec == AV_CODEC_ID_AV1) return "av1_amf";
  }
  if (hardware == VideoHardwareEncoder::vulkan) {
    if (codec == AV_CODEC_ID_FFV1) return "ffv1_vulkan";
    if (codec == AV_CODEC_ID_PRORES) return "prores_ks_vulkan";
  }
  return nullptr;
}

const char* hardware_backend_name(VideoHardwareEncoder hardware) {
  switch (hardware) {
    case VideoHardwareEncoder::automatic: return "auto";
    case VideoHardwareEncoder::software: return "software";
    case VideoHardwareEncoder::qsv: return "qsv";
    case VideoHardwareEncoder::nvenc: return "nvenc";
    case VideoHardwareEncoder::amf: return "amf";
    case VideoHardwareEncoder::vulkan: return "vulkan";
  }
  return "unknown";
}

const AVCodec* find_encoder(AVCodecID codec, VideoHardwareEncoder hardware,
                            std::string& selected_name) {
  if (hardware == VideoHardwareEncoder::qsv || hardware == VideoHardwareEncoder::nvenc ||
      hardware == VideoHardwareEncoder::amf || hardware == VideoHardwareEncoder::vulkan) {
    const auto* name = hardware_encoder_name(codec, hardware);
    const auto* encoder = name ? avcodec_find_encoder_by_name(name) : nullptr;
    if (encoder) selected_name = encoder->name;
    return encoder;
  }
  if (hardware == VideoHardwareEncoder::automatic) {
    for (const auto candidate : {VideoHardwareEncoder::vulkan, VideoHardwareEncoder::qsv,
                                 VideoHardwareEncoder::nvenc, VideoHardwareEncoder::amf}) {
      const auto* name = hardware_encoder_name(codec, candidate);
      if (const auto* encoder = name ? avcodec_find_encoder_by_name(name) : nullptr) {
        selected_name = encoder->name;
        return encoder;
      }
    }
  }
  const auto* name = software_encoder_name(codec);
  const auto* encoder = name ? avcodec_find_encoder_by_name(name) : avcodec_find_encoder(codec);
  if (encoder) selected_name = encoder->name;
  return encoder;
}

bool pixel_format_supported(const AVCodec* encoder, AVPixelFormat format) {
  const void* configuration = nullptr;
  int count = 0;
  if (avcodec_get_supported_config(nullptr, encoder, AV_CODEC_CONFIG_PIX_FORMAT, 0,
                                   &configuration, &count) < 0 || !configuration)
    return true;
  const auto* formats = static_cast<const AVPixelFormat*>(configuration);
  for (int index = 0; index < count; ++index)
    if (formats[index] == format) return true;
  return false;
}

AVPixelFormat select_pixel_format(const AVCodec* encoder, AVPixelFormat source, int requested_bit_depth) {
  if (requested_bit_depth == 0 && pixel_format_supported(encoder, source)) return source;
  if (encoder->id == AV_CODEC_ID_PRORES) {
    for (const auto preferred : {AV_PIX_FMT_YUV422P10LE, AV_PIX_FMT_YUV444P10LE,
                                 AV_PIX_FMT_YUVA444P10LE})
      if (pixel_format_supported(encoder, preferred)) return preferred;
  }
  const bool source_10_bit = requested_bit_depth == 10 ||
                             (requested_bit_depth == 0 &&
                              (source == AV_PIX_FMT_YUV420P10LE || source == AV_PIX_FMT_P010LE));
  for (const auto preferred : source_10_bit ? std::vector{AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_P010LE,
                                                         AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12}
                                                  : std::vector{AV_PIX_FMT_YUV420P, AV_PIX_FMT_NV12,
                                                                AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_P010LE})
    if (pixel_format_supported(encoder, preferred)) return preferred;
  const void* configuration = nullptr;
  int count = 0;
  if (avcodec_get_supported_config(nullptr, encoder, AV_CODEC_CONFIG_PIX_FORMAT, 0,
                                   &configuration, &count) >= 0 && configuration) {
    const auto* formats = static_cast<const AVPixelFormat*>(configuration);
    for (int index = 0; index < count; ++index) {
      const auto* descriptor = av_pix_fmt_desc_get(formats[index]);
      if (descriptor && (descriptor->flags & AV_PIX_FMT_FLAG_HWACCEL) == 0) return formats[index];
    }
  }
  return source;
}

AVPixelFormat vulkan_upload_format(AVCodecID codec, AVPixelFormat source,
                                   int requested_bit_depth) {
  if (codec == AV_CODEC_ID_PRORES) {
    if (source == AV_PIX_FMT_YUV444P10LE || source == AV_PIX_FMT_YUVA444P10LE)
      return source;
    return AV_PIX_FMT_YUV422P10LE;
  }
  // FFV1 Vulkan version 3 requires subsampled dimensions to align to large
  // slice blocks. Uploading as 4:4:4 preserves every decoded sample while
  // supporting ordinary 1080-line and non-mod128 source dimensions.
  if (codec == AV_CODEC_ID_FFV1) {
    if (source == AV_PIX_FMT_GBRP || source == AV_PIX_FMT_GBRP10LE ||
        source == AV_PIX_FMT_YUV444P || source == AV_PIX_FMT_YUV444P10LE)
      return source;
    return requested_bit_depth == 10 ? AV_PIX_FMT_YUV444P10LE : AV_PIX_FMT_YUV444P;
  }
  if (requested_bit_depth == 10) return AV_PIX_FMT_YUV420P10LE;
  if (requested_bit_depth == 8) return AV_PIX_FMT_YUV420P;
  const auto* descriptor = av_pix_fmt_desc_get(source);
  if (descriptor && !(descriptor->flags & AV_PIX_FMT_FLAG_HWACCEL)) return source;
  return AV_PIX_FMT_YUV420P;
}

struct HardwareEncodeFrames {
  AVBufferRef* device = nullptr;
  AVBufferRef* frames = nullptr;
  AVPixelFormat sw_format = AV_PIX_FMT_NONE;

  ~HardwareEncodeFrames() { reset(); }
  void reset() {
    av_buffer_unref(&frames);
    av_buffer_unref(&device);
    sw_format = AV_PIX_FMT_NONE;
  }
};

int configure_vulkan_frames(AVCodecContext* encoder, AVCodecID codec,
                            AVPixelFormat source, int requested_bit_depth,
                            HardwareEncodeFrames& hardware) {
  hardware.reset();
  int result = av_hwdevice_ctx_create(&hardware.device, AV_HWDEVICE_TYPE_VULKAN,
                                      nullptr, nullptr, 0);
  if (result < 0) return result;
  hardware.frames = av_hwframe_ctx_alloc(hardware.device);
  if (!hardware.frames) return AVERROR(ENOMEM);
  hardware.sw_format = vulkan_upload_format(codec, source, requested_bit_depth);
  auto* frames = reinterpret_cast<AVHWFramesContext*>(hardware.frames->data);
  frames->format = AV_PIX_FMT_VULKAN;
  frames->sw_format = hardware.sw_format;
  frames->width = encoder->width;
  frames->height = encoder->height;
  frames->initial_pool_size = 16;
  result = av_hwframe_ctx_init(hardware.frames);
  if (result < 0) return result;
  encoder->pix_fmt = AV_PIX_FMT_VULKAN;
  encoder->sw_pix_fmt = hardware.sw_format;
  encoder->hw_device_ctx = av_buffer_ref(hardware.device);
  encoder->hw_frames_ctx = av_buffer_ref(hardware.frames);
  if (!encoder->hw_device_ctx || !encoder->hw_frames_ctx) return AVERROR(ENOMEM);
  return 0;
}

bool encoder_runtime_available(AVCodecID codec, VideoHardwareEncoder hardware) {
  std::string selected_name;
  const auto* candidate = find_encoder(codec, hardware, selected_name);
  if (!candidate) return false;
  AVCodecContext* context = avcodec_alloc_context3(candidate);
  if (!context) return false;
  context->width = 256;
  context->height = 256;
  context->framerate = AVRational{25, 1};
  context->time_base = AVRational{1, 25};
  context->sample_aspect_ratio = AVRational{1, 1};
  context->bit_rate = 2'000'000;
  context->gop_size = codec == AV_CODEC_ID_FFV1 || codec == AV_CODEC_ID_PRORES ? 1 : 25;
  context->max_b_frames = 0;
  HardwareEncodeFrames hardware_frames;
  int setup = 0;
  if (hardware == VideoHardwareEncoder::vulkan)
    setup = configure_vulkan_frames(context, codec, AV_PIX_FMT_YUV420P, 8, hardware_frames);
  else
    context->pix_fmt = select_pixel_format(candidate, AV_PIX_FMT_YUV420P, 8);
  const bool available = setup >= 0 && avcodec_open2(context, candidate, nullptr) >= 0;
  avcodec_free_context(&context);
  return available;
}

AVRational parse_frame_rate(const std::string& text, AVRational fallback) {
  if (text.empty() || text == "No Change") return fallback;
  if (text == "23.976") return AVRational{24000, 1001};
  if (text == "29.97") return AVRational{30000, 1001};
  if (text == "59.94") return AVRational{60000, 1001};
  try {
    const auto value = std::stod(text);
    if (value > 0.0) return av_d2q(value, 1001000);
  } catch (...) {
  }
  return fallback;
}

AVRational parse_aspect(const std::string& text, AVRational fallback, int width, int height) {
  if (text.empty() || text == "No Change") return fallback;
  const auto colon = text.find(':');
  if (colon == std::string::npos || width <= 0 || height <= 0) return fallback;
  try {
    const double display = std::stod(text.substr(0, colon)) / std::stod(text.substr(colon + 1));
    return av_d2q(display * static_cast<double>(height) / static_cast<double>(width), 10000);
  } catch (...) {
    return fallback;
  }
}

bool configure_filters(FilterGraph& filters, const AVCodecContext* decoder,
                       const AVCodecContext* encoder, AVPixelFormat output_format,
                       AVRational input_time_base,
                       const VideoTransformOptions& options, std::string& error) {
  filters.graph = avfilter_graph_alloc();
  const auto* buffer = avfilter_get_by_name("buffer");
  const auto* sink = avfilter_get_by_name("buffersink");
  if (!filters.graph || !buffer || !sink) {
    error = "required FFmpeg video filters are unavailable";
    return false;
  }
  char source_args[512]{};
  const auto sar = decoder->sample_aspect_ratio.num > 0 ? decoder->sample_aspect_ratio : AVRational{1, 1};
  av_strlcpy(source_args,
             ("video_size=" + std::to_string(decoder->width) + "x" + std::to_string(decoder->height) +
              ":pix_fmt=" + std::to_string(decoder->pix_fmt) + ":time_base=" +
              std::to_string(input_time_base.num) + "/" + std::to_string(input_time_base.den) +
              ":pixel_aspect=" + std::to_string(sar.num) + "/" + std::to_string(sar.den))
                 .c_str(),
             sizeof(source_args));
  int result = avfilter_graph_create_filter(&filters.source, buffer, "in", source_args, nullptr, filters.graph);
  if (result >= 0) result = avfilter_graph_create_filter(&filters.sink, sink, "out", nullptr, nullptr, filters.graph);
  if (result < 0) {
    error = "filter endpoint setup failed: " + ffmpeg_error(result);
    return false;
  }
  std::vector<std::string> chain;
  if (options.deinterlace == "Bob") chain.emplace_back("bwdif=mode=send_field:parity=auto:deint=all");
  else if (options.deinterlace == "Auto") chain.emplace_back("bwdif=mode=send_frame:parity=auto:deint=interlaced");
  else if (options.deinterlace == "Motion Adaptive") chain.emplace_back("bwdif=mode=send_frame:parity=auto:deint=all");
  if (options.width > 0 || options.height > 0)
    chain.emplace_back("scale=" + std::to_string(options.width > 0 ? options.width : -2) + ":" +
                       std::to_string(options.height > 0 ? options.height : -2) + ":flags=lanczos");
  if (!options.frame_rate.empty() && options.frame_rate != "No Change")
    chain.emplace_back("fps=fps=" + options.frame_rate + ":round=near");
  if (!options.aspect_ratio.empty() && options.aspect_ratio != "No Change") {
    const auto sar_value = encoder->sample_aspect_ratio.num > 0 ? encoder->sample_aspect_ratio : AVRational{1, 1};
    chain.emplace_back("setsar=" + std::to_string(sar_value.num) + "/" + std::to_string(sar_value.den));
  }
  const auto* pixel_format_name = av_get_pix_fmt_name(output_format);
  if (!pixel_format_name) {
    error = "encoder pixel format is unavailable";
    return false;
  }
  chain.emplace_back(std::string("format=pix_fmts=") + pixel_format_name);
  std::string description;
  for (std::size_t index = 0; index < chain.size(); ++index) {
    if (index) description += ',';
    description += chain[index];
  }
  if (description.empty()) description = "null";
  AVFilterInOut* inputs = avfilter_inout_alloc();
  AVFilterInOut* outputs = avfilter_inout_alloc();
  if (!inputs || !outputs) {
    avfilter_inout_free(&inputs);
    avfilter_inout_free(&outputs);
    error = "filter route allocation failed";
    return false;
  }
  outputs->name = av_strdup("in");
  outputs->filter_ctx = filters.source;
  outputs->pad_idx = 0;
  outputs->next = nullptr;
  inputs->name = av_strdup("out");
  inputs->filter_ctx = filters.sink;
  inputs->pad_idx = 0;
  inputs->next = nullptr;
  result = avfilter_graph_parse_ptr(filters.graph, description.c_str(), &inputs, &outputs, nullptr);
  avfilter_inout_free(&inputs);
  avfilter_inout_free(&outputs);
  if (result >= 0) result = avfilter_graph_config(filters.graph, nullptr);
  if (result < 0) {
    error = "video filter graph failed: " + ffmpeg_error(result);
    return false;
  }
  return true;
}
#endif

}  // namespace

VideoOutputCodec video_output_codec_from_name(const std::string& name) {
  if (name == "mpeg2") return VideoOutputCodec::mpeg2;
  if (name == "h264") return VideoOutputCodec::h264;
  if (name == "hevc") return VideoOutputCodec::hevc;
  if (name == "av1") return VideoOutputCodec::av1;
  if (name == "vp9") return VideoOutputCodec::vp9;
  if (name == "ffv1") return VideoOutputCodec::ffv1;
  if (name == "prores" || name == "prores_ks") return VideoOutputCodec::prores;
  return VideoOutputCodec::source;
}

VideoHardwareEncoder video_hardware_encoder_from_name(const std::string& name) {
  if (name == "software") return VideoHardwareEncoder::software;
  if (name == "qsv") return VideoHardwareEncoder::qsv;
  if (name == "nvenc") return VideoHardwareEncoder::nvenc;
  if (name == "amf") return VideoHardwareEncoder::amf;
  if (name == "vulkan") return VideoHardwareEncoder::vulkan;
  return VideoHardwareEncoder::automatic;
}

bool video_transform_requested(const VideoTransformOptions& options) {
  return options.codec != VideoOutputCodec::source || options.width > 0 || options.height > 0 || options.bit_depth > 0 ||
         options.bitrate_kbps > 0 || options.max_bitrate_kbps > 0 ||
         (!options.aspect_ratio.empty() && options.aspect_ratio != "No Change") ||
         (!options.deinterlace.empty() && options.deinterlace != "None") ||
         (!options.frame_rate.empty() && options.frame_rate != "No Change") ||
         options.trial_watermark;
}

std::string transcode_mkv_video_range(const std::string& input, const std::string& output,
                                      std::uint64_t first_frame, std::uint64_t end_frame,
                                      const VideoTransformOptions& options) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input; (void)output; (void)first_frame; (void)end_frame; (void)options;
  return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"FFmpeg support is disabled\"}";
#else
  if (end_frame <= first_frame)
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"empty frame range\"}";
  AVFormatContext* source = nullptr;
  AVFormatContext* destination = nullptr;
  AVCodecContext* decoder = nullptr;
  AVCodecContext* encoder = nullptr;
  AVPacket* packet = av_packet_alloc();
  AVFrame* decoded = av_frame_alloc();
  AVFrame* filtered = av_frame_alloc();
  AVFrame* hardware_frame = av_frame_alloc();
  FilterGraph filters;
  HardwareEncodeFrames hardware_frames;
  AVPixelFormat encode_input_format = AV_PIX_FMT_NONE;
  VideoHardwareEncoder selected_backend = VideoHardwareEncoder::software;
  bool completed = false;
  auto cleanup = [&] {
    av_frame_free(&decoded);
    av_frame_free(&filtered);
    av_frame_free(&hardware_frame);
    av_packet_free(&packet);
    avcodec_free_context(&decoder);
    avcodec_free_context(&encoder);
    if (destination && destination->pb) avio_closep(&destination->pb);
    avformat_free_context(destination);
    avformat_close_input(&source);
    if (!completed) {
      std::error_code ignored;
      std::filesystem::remove(output, ignored);
    }
  };
  int result = avformat_open_input(&source, input.c_str(), nullptr, nullptr);
  if (result >= 0) result = avformat_find_stream_info(source, nullptr);
  const int video_stream = result >= 0 ? av_find_best_stream(source, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0) : result;
  if (result < 0 || video_stream < 0 || !packet || !decoded || !filtered || !hardware_frame) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"input setup failed\"}";
  }
  const auto* input_stream = source->streams[video_stream];
  const auto* decoder_codec = ffmpeg::preferred_video_decoder(input_stream->codecpar->codec_id);
  decoder = decoder_codec ? avcodec_alloc_context3(decoder_codec) : nullptr;
  if (!decoder || avcodec_parameters_to_context(decoder, input_stream->codecpar) < 0 ||
      avcodec_open2(decoder, decoder_codec, nullptr) < 0) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"decoder open failed\"}";
  }
  const auto codec_id = requested_codec_id(options.codec, input_stream->codecpar->codec_id);
  const auto source_rate = input_stream->avg_frame_rate.num > 0 ? input_stream->avg_frame_rate : AVRational{25, 1};
  const auto output_rate = parse_frame_rate(options.frame_rate, source_rate);
  if (avformat_alloc_output_context2(&destination, nullptr, "matroska", output.c_str()) < 0 || !destination) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"output context failed\"}";
  }
  std::string encoder_name;
  const auto configure_and_open_encoder = [&](VideoHardwareEncoder backend) {
    avcodec_free_context(&encoder);
    hardware_frames.reset();
    std::string candidate_name;
    const auto* candidate = find_encoder(codec_id, backend, candidate_name);
    if (!candidate) return AVERROR_ENCODER_NOT_FOUND;
    encoder = avcodec_alloc_context3(candidate);
    if (!encoder) return AVERROR(ENOMEM);
    encoder->width = options.width > 0 ? options.width : decoder->width;
    encoder->height = options.height > 0 ? options.height : decoder->height;
    encoder->width -= encoder->width % 2;
    encoder->height -= encoder->height % 2;
    encoder->framerate = output_rate;
    encoder->time_base = av_inv_q(output_rate);
    encoder->sample_aspect_ratio = parse_aspect(options.aspect_ratio, decoder->sample_aspect_ratio,
                                                 encoder->width, encoder->height);
    encoder->color_range = decoder->color_range;
    encoder->color_primaries = decoder->color_primaries;
    encoder->color_trc = decoder->color_trc;
    encoder->colorspace = decoder->colorspace;
    encoder->chroma_sample_location = decoder->chroma_sample_location;
    encoder->bit_rate = options.bitrate_kbps > 0 ? static_cast<std::int64_t>(options.bitrate_kbps) * 1000
                                                 : std::max<std::int64_t>(input_stream->codecpar->bit_rate, 8'000'000);
    encoder->rc_max_rate = options.max_bitrate_kbps > 0 ? static_cast<std::int64_t>(options.max_bitrate_kbps) * 1000 : 0;
    encoder->gop_size = codec_id == AV_CODEC_ID_FFV1 || codec_id == AV_CODEC_ID_PRORES
                            ? 1
                            : std::max(12, output_rate.num / std::max(1, output_rate.den));
    encoder->max_b_frames = 0;
    encoder->thread_count = 0;
    int setup_result = 0;
    if (backend == VideoHardwareEncoder::vulkan) {
      setup_result = configure_vulkan_frames(encoder, codec_id, decoder->pix_fmt,
                                             options.bit_depth, hardware_frames);
      encode_input_format = hardware_frames.sw_format;
    } else {
      encoder->pix_fmt = select_pixel_format(candidate, decoder->pix_fmt, options.bit_depth);
      encode_input_format = encoder->pix_fmt;
    }
    if (setup_result < 0) return setup_result;
    if (codec_id == AV_CODEC_ID_HEVC)
      av_opt_set(encoder->priv_data, "profile",
                 encoder->pix_fmt == AV_PIX_FMT_YUV420P10LE || encoder->pix_fmt == AV_PIX_FMT_P010LE
                     ? "main10" : "main", 0);
    if (codec_id == AV_CODEC_ID_AV1) av_opt_set(encoder->priv_data, "cpu-used", "8", 0);
    if (codec_id == AV_CODEC_ID_VP9) av_opt_set(encoder->priv_data, "deadline", "realtime", 0);
    if (destination->oformat->flags & AVFMT_GLOBALHEADER) encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
    const int open_result = avcodec_open2(encoder, candidate, nullptr);
    if (open_result >= 0) {
      encoder_name = candidate_name;
      selected_backend = backend;
    }
    return open_result;
  };
  result = AVERROR_ENCODER_NOT_FOUND;
  if (options.hardware == VideoHardwareEncoder::automatic) {
    // Encoder registration only proves that FFmpeg was built with a backend. Opening it
    // verifies that the matching driver and adapter are usable on this PC.
    for (const auto backend : {VideoHardwareEncoder::vulkan, VideoHardwareEncoder::nvenc,
                               VideoHardwareEncoder::qsv, VideoHardwareEncoder::amf,
                               VideoHardwareEncoder::software}) {
      result = configure_and_open_encoder(backend);
      if (result >= 0) break;
    }
  } else {
    result = configure_and_open_encoder(options.hardware);
  }
  if (result < 0) {
    const auto reason = ffmpeg_error(result);
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"encoder open failed: " + reason + "\"}";
  }
  std::string filter_error;
  if (!configure_filters(filters, decoder, encoder, encode_input_format,
                         input_stream->time_base, options, filter_error)) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"" + filter_error + "\"}";
  }
  auto* output_stream = avformat_new_stream(destination, nullptr);
  if (!output_stream || avcodec_parameters_from_context(output_stream->codecpar, encoder) < 0) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"output stream setup failed\"}";
  }
  output_stream->time_base = encoder->time_base;
  const int io_result = avio_open(&destination->pb, output.c_str(), AVIO_FLAG_WRITE);
  if (io_result < 0) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"output open failed: " +
           ffmpeg_error(io_result) + "\"}";
  }
  const int header_result = avformat_write_header(destination, nullptr);
  if (header_result < 0) {
    cleanup();
    return "{\"schema\":\"video-transform-v1\",\"status\":\"FAILED\",\"reason\":\"output header failed: " +
           ffmpeg_error(header_result) + "\"}";
  }
  std::uint64_t decoded_index = 0;
  std::uint64_t encoded_frames = 0;
  std::uint64_t encoded_packets = 0;
  std::int64_t first_pts = AV_NOPTS_VALUE;
  auto drain_encoder = [&](bool flush) {
    if (flush && avcodec_send_frame(encoder, nullptr) < 0) return false;
    while (true) {
      const int receive = avcodec_receive_packet(encoder, packet);
      if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) return true;
      if (receive < 0) return false;
      packet->stream_index = output_stream->index;
      av_packet_rescale_ts(packet, encoder->time_base, output_stream->time_base);
      if (av_interleaved_write_frame(destination, packet) < 0) return false;
      ++encoded_packets;
      av_packet_unref(packet);
    }
  };
  auto drain_filter = [&] {
    while (true) {
      const int receive = av_buffersink_get_frame(filters.sink, filtered);
      if (receive == AVERROR(EAGAIN) || receive == AVERROR_EOF) return true;
      if (receive < 0) return false;
      if (options.trial_watermark && !apply_trial_watermark(filtered)) return false;
      filtered->pts = av_rescale_q(filtered->pts, av_buffersink_get_time_base(filters.sink), encoder->time_base);
      if (encoded_frames == 0) filtered->pict_type = AV_PICTURE_TYPE_I;
      AVFrame* frame_to_encode = filtered;
      if (hardware_frames.frames) {
        av_frame_unref(hardware_frame);
        if (av_hwframe_get_buffer(hardware_frames.frames, hardware_frame, 0) < 0 ||
            av_hwframe_transfer_data(hardware_frame, filtered, 0) < 0 ||
            av_frame_copy_props(hardware_frame, filtered) < 0)
          return false;
        hardware_frame->pts = filtered->pts;
        hardware_frame->pict_type = filtered->pict_type;
        frame_to_encode = hardware_frame;
      }
      if (avcodec_send_frame(encoder, frame_to_encode) < 0) return false;
      ++encoded_frames;
      av_frame_unref(filtered);
      av_frame_unref(hardware_frame);
      if (!drain_encoder(false)) return false;
    }
  };
  bool processing_ok = true;
  while (av_read_frame(source, packet) >= 0 && processing_ok && decoded_index < end_frame) {
    if (packet->stream_index == video_stream) {
      if (avcodec_send_packet(decoder, packet) < 0) processing_ok = false;
      while (processing_ok && avcodec_receive_frame(decoder, decoded) >= 0) {
        if (decoded_index >= first_frame && decoded_index < end_frame) {
          const auto timestamp = decoded->best_effort_timestamp != AV_NOPTS_VALUE ? decoded->best_effort_timestamp : decoded->pts;
          if (first_pts == AV_NOPTS_VALUE) first_pts = timestamp == AV_NOPTS_VALUE ? 0 : timestamp;
          decoded->pts = timestamp == AV_NOPTS_VALUE ? static_cast<std::int64_t>(decoded_index - first_frame)
                                                     : timestamp - first_pts;
          if (av_buffersrc_add_frame_flags(filters.source, decoded, AV_BUFFERSRC_FLAG_KEEP_REF) < 0 || !drain_filter())
            processing_ok = false;
        }
        ++decoded_index;
        av_frame_unref(decoded);
        if (decoded_index >= end_frame) break;
      }
    }
    av_packet_unref(packet);
  }
  if (processing_ok && decoded_index < end_frame) {
    avcodec_send_packet(decoder, nullptr);
    while (decoded_index < end_frame && avcodec_receive_frame(decoder, decoded) >= 0) {
      if (decoded_index >= first_frame) {
        const auto timestamp = decoded->best_effort_timestamp != AV_NOPTS_VALUE ? decoded->best_effort_timestamp : decoded->pts;
        if (first_pts == AV_NOPTS_VALUE) first_pts = timestamp == AV_NOPTS_VALUE ? 0 : timestamp;
        decoded->pts = timestamp == AV_NOPTS_VALUE ? static_cast<std::int64_t>(decoded_index - first_frame) : timestamp - first_pts;
        if (av_buffersrc_add_frame_flags(filters.source, decoded, AV_BUFFERSRC_FLAG_KEEP_REF) < 0 || !drain_filter())
          processing_ok = false;
      }
      ++decoded_index;
      av_frame_unref(decoded);
    }
  }
  if (processing_ok) processing_ok = av_buffersrc_add_frame_flags(filters.source, nullptr, 0) >= 0 && drain_filter();
  if (processing_ok) processing_ok = drain_encoder(true);
  if (processing_ok) processing_ok = av_write_trailer(destination) >= 0;
  if (processing_ok && encoded_frames > 0 && encoded_packets > 0) completed = true;
  const auto* encoded_pixel_descriptor = av_pix_fmt_desc_get(encode_input_format);
  const int output_bit_depth = encoded_pixel_descriptor ? encoded_pixel_descriptor->comp[0].depth : 0;
  cleanup();
  std::ostringstream report;
  report << "{\"schema\":\"video-transform-v1\",\"status\":\"" << (completed ? "PASSED" : "FAILED")
         << "\",\"encoder\":\"" << encoder_name << "\",\"backend\":\""
         << hardware_backend_name(selected_backend) << "\",\"input_frames\":" << (end_frame - first_frame)
         << ",\"encoded_frames\":" << encoded_frames << ",\"encoded_packets\":" << encoded_packets
         << ",\"bit_depth\":" << output_bit_depth
         << ",\"width\":" << (options.width > 0 ? options.width - options.width % 2 : 0)
         << ",\"height\":" << (options.height > 0 ? options.height - options.height % 2 : 0)
         << ",\"trial_watermark\":" << (options.trial_watermark ? "true" : "false") << '}';
  return report.str();
#endif
}

std::string validate_transcoded_video(const std::string& output,
                                      const VideoTransformOptions& options) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)output; (void)options;
  return "{\"schema\":\"video-transform-validation-v1\",\"status\":\"FAILED\"}";
#else
  AVFormatContext* input = nullptr;
  AVCodecContext* decoder = nullptr;
  AVPacket* packet = av_packet_alloc();
  AVFrame* frame = av_frame_alloc();
  auto cleanup = [&] { av_frame_free(&frame); av_packet_free(&packet); avcodec_free_context(&decoder); avformat_close_input(&input); };
  int result = avformat_open_input(&input, output.c_str(), nullptr, nullptr);
  if (result >= 0) result = avformat_find_stream_info(input, nullptr);
  const int stream_index = result >= 0 ? av_find_best_stream(input, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0) : result;
  if (result < 0 || stream_index < 0 || !packet || !frame) {
    cleanup();
    return "{\"schema\":\"video-transform-validation-v1\",\"status\":\"FAILED\",\"reason\":\"open failed\"}";
  }
  const auto* stream = input->streams[stream_index];
  const auto expected_codec = requested_codec_id(options.codec, stream->codecpar->codec_id);
  const bool codec_match = stream->codecpar->codec_id == expected_codec;
  const bool dimensions_match = (options.width <= 0 || stream->codecpar->width == options.width - options.width % 2) &&
                                (options.height <= 0 || stream->codecpar->height == options.height - options.height % 2);
  const auto pixel_format = static_cast<AVPixelFormat>(stream->codecpar->format);
  const auto* pixel_descriptor = av_pix_fmt_desc_get(pixel_format);
  const bool bit_depth_match = options.bit_depth <= 0 ||
                               (pixel_descriptor && pixel_descriptor->comp[0].depth == options.bit_depth);
  const auto* codec = ffmpeg::preferred_video_decoder(stream->codecpar->codec_id);
  decoder = codec ? avcodec_alloc_context3(codec) : nullptr;
  if (!decoder || avcodec_parameters_to_context(decoder, stream->codecpar) < 0 || avcodec_open2(decoder, codec, nullptr) < 0) {
    cleanup();
    return "{\"schema\":\"video-transform-validation-v1\",\"status\":\"FAILED\",\"reason\":\"decode setup failed\"}";
  }
  std::uint64_t packets = 0;
  std::uint64_t frames = 0;
  std::uint64_t timing_violations = 0;
  std::int64_t last_dts = AV_NOPTS_VALUE;
  bool first_key = false;
  while (av_read_frame(input, packet) >= 0) {
    if (packet->stream_index == stream_index) {
      if (packets++ == 0) first_key = (packet->flags & AV_PKT_FLAG_KEY) != 0;
      if (packet->dts != AV_NOPTS_VALUE && last_dts != AV_NOPTS_VALUE && packet->dts < last_dts) ++timing_violations;
      if (packet->dts != AV_NOPTS_VALUE) last_dts = packet->dts;
      if (avcodec_send_packet(decoder, packet) < 0) ++timing_violations;
      while (avcodec_receive_frame(decoder, frame) >= 0) {
        ++frames;
        av_frame_unref(frame);
      }
    }
    av_packet_unref(packet);
  }
  avcodec_send_packet(decoder, nullptr);
  while (avcodec_receive_frame(decoder, frame) >= 0) {
    ++frames;
    av_frame_unref(frame);
  }
  const bool passed = codec_match && dimensions_match && bit_depth_match && first_key && packets > 0 && frames > 0 && timing_violations == 0;
  cleanup();
  std::ostringstream report;
  report << "{\"schema\":\"video-transform-validation-v1\",\"status\":\"" << (passed ? "PASSED" : "FAILED VALIDATION")
         << "\",\"codec_match\":" << (codec_match ? "true" : "false")
         << ",\"dimensions_match\":" << (dimensions_match ? "true" : "false")
         << ",\"bit_depth_match\":" << (bit_depth_match ? "true" : "false")
         << ",\"first_packet_keyframe\":" << (first_key ? "true" : "false")
         << ",\"packets\":" << packets << ",\"decoded_frames\":" << frames
         << ",\"timing_violations\":" << timing_violations << '}';
  return report.str();
#endif
}

std::string video_encoder_capabilities_report() {
#ifndef SMARTCUT_HAS_FFMPEG
  return "{\"schema\":\"video-encoder-capabilities-v1\",\"status\":\"FAILED\"}";
#else
  const struct { AVCodecID codec; const char* name; } codecs[] = {
      {AV_CODEC_ID_MPEG2VIDEO, "mpeg2"}, {AV_CODEC_ID_H264, "h264"}, {AV_CODEC_ID_HEVC, "hevc"},
      {AV_CODEC_ID_AV1, "av1"}, {AV_CODEC_ID_VP9, "vp9"}, {AV_CODEC_ID_FFV1, "ffv1"},
      {AV_CODEC_ID_PRORES, "prores"}};
  std::ostringstream report;
  report << "{\"schema\":\"video-encoder-capabilities-v1\",\"status\":\"PASSED\",\"encoders\":[";
  bool first = true;
  for (const auto& codec : codecs) {
    for (const auto hardware : {VideoHardwareEncoder::software, VideoHardwareEncoder::qsv,
                                VideoHardwareEncoder::nvenc, VideoHardwareEncoder::amf,
                                VideoHardwareEncoder::vulkan}) {
      const auto* name = hardware == VideoHardwareEncoder::software ? software_encoder_name(codec.codec)
                                                                    : hardware_encoder_name(codec.codec, hardware);
      if (!first) report << ',';
      first = false;
      const bool registered = name && avcodec_find_encoder_by_name(name);
      const bool available = registered && encoder_runtime_available(codec.codec, hardware);
      report << "{\"codec\":\"" << codec.name << "\",\"backend\":\""
             << hardware_backend_name(hardware)
             << "\",\"name\":\"" << (name ? name : "") << "\",\"registered\":"
             << (registered ? "true" : "false") << ",\"available\":"
             << (available ? "true" : "false") << '}';
    }
  }
  report << "]}";
  return report.str();
#endif
}

}  // namespace smartcut::media
