#include "media/hardware_self_test.hpp"

#include "media/frame_decode.hpp"
#include "media/preview_conversion.hpp"
#include "media/stream_probe.hpp"
#include "media/video_transform.hpp"

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

namespace smartcut::media {
namespace {

struct BenchmarkResult {
  bool passed{};
  bool active{};
  std::size_t frames{};
  int width{};
  int height{};
  double seconds{};
  std::string backend{"software"};
};

std::int64_t json_integer(const std::string& text, std::string_view key,
                          std::int64_t fallback = 0) {
  const auto marker = std::string{"\""} + std::string(key) + "\":";
  const auto position = text.find(marker);
  if (position == std::string::npos) return fallback;
  try {
    return std::stoll(text.substr(position + marker.size()));
  } catch (...) {
    return fallback;
  }
}

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

bool passed(const std::string& report) {
  return json_string(report, "status") == "PASSED";
}

double speedup(const BenchmarkResult& software, const BenchmarkResult& hardware) {
  if (!software.passed || !hardware.passed || !hardware.active ||
      software.seconds <= 0.0 || hardware.seconds <= 0.0)
    return 0.0;
  return software.seconds / hardware.seconds;
}

bool recommend_hardware(const BenchmarkResult& software,
                        const BenchmarkResult& hardware) {
  if (!hardware.passed || !hardware.active) return false;
  if (!software.passed) return true;
  return speedup(software, hardware) >= 1.10;
}

std::string result_json(const BenchmarkResult& result) {
  const auto fps = result.seconds > 0.0
      ? static_cast<double>(result.frames) / result.seconds : 0.0;
  std::ostringstream output;
  output << "{\"status\":\"" << (result.passed ? "PASSED" : "FAILED")
         << "\",\"active\":" << (result.active ? "true" : "false")
         << ",\"backend\":\"" << result.backend << "\",\"frames\":"
         << result.frames << ",\"width\":" << result.width
         << ",\"height\":" << result.height << ",\"seconds\":"
         << result.seconds << ",\"fps\":" << fps << '}';
  return output.str();
}

BenchmarkResult benchmark_decode(const std::string& input,
                                 std::size_t start_frame,
                                 std::int64_t start_pts,
                                 std::size_t requested_frames,
                                 bool allow_hardware) {
  BenchmarkResult result;
  const auto started = std::chrono::steady_clock::now();
  bool conversion_failed = false;
  const auto report = decode_preview_stream(
      input, start_frame, start_pts,
      [&](std::shared_ptr<core::PreviewFrame> source, std::uint64_t) {
        auto converted = convert_preview_to_bgra(std::move(source));
        if (!converted) {
          conversion_failed = true;
          return false;
        }
        if (result.frames == 0) {
          result.width = converted->width;
          result.height = converted->height;
        }
        ++result.frames;
        return result.frames < requested_frames;
      },
      allow_hardware);
  result.seconds = std::chrono::duration<double>(
      std::chrono::steady_clock::now() - started).count();
  result.passed = report.completed && !conversion_failed && result.frames > 0;
  result.active = report.hardware_accelerated;
  result.backend = report.backend;
  return result;
}

std::pair<int, int> benchmark_dimensions(const std::string& probe) {
  int width = static_cast<int>(json_integer(probe, "width"));
  int height = static_cast<int>(json_integer(probe, "height"));
  if (width <= 0 || height <= 0) return {0, 0};
  const double scale = std::min({1.0, 1280.0 / static_cast<double>(width),
                                720.0 / static_cast<double>(height)});
  width = std::max(2, static_cast<int>(std::floor(width * scale)));
  height = std::max(2, static_cast<int>(std::floor(height * scale)));
  width -= width % 2;
  height -= height % 2;
  return {width, height};
}

BenchmarkResult benchmark_encode(const std::string& input,
                                 const std::filesystem::path& output,
                                 std::size_t first_frame,
                                 std::size_t requested_frames,
                                 int width, int height,
                                 VideoOutputCodec codec,
                                 VideoHardwareEncoder hardware,
                                 int bit_depth) {
  BenchmarkResult result;
  result.width = width;
  result.height = height;
  VideoTransformOptions options;
  options.codec = codec;
  options.hardware = hardware;
  options.width = width;
  options.height = height;
  options.bit_depth = bit_depth;
  options.frame_rate = "No Change";
  const auto started = std::chrono::steady_clock::now();
  const auto transform = transcode_mkv_video_range(
      input, output.string(), first_frame, first_frame + requested_frames, options);
  result.seconds = std::chrono::duration<double>(
      std::chrono::steady_clock::now() - started).count();
  const auto validation = passed(transform)
      ? validate_transcoded_video(output.string(), options)
      : std::string{"{\"status\":\"FAILED\"}"};
  result.passed = passed(transform) && passed(validation);
  result.backend = json_string(transform, "backend");
  if (result.backend.empty()) result.backend = "unavailable";
  result.active = result.passed && result.backend != "software";
  result.frames = static_cast<std::size_t>(std::max<std::int64_t>(
      0, json_integer(transform, "encoded_frames")));
  std::error_code ignored;
  std::filesystem::remove(output, ignored);
  return result;
}

std::string comparison_json(std::string_view name,
                            const BenchmarkResult& software,
                            const BenchmarkResult& hardware) {
  const auto measured_speedup = speedup(software, hardware);
  const bool recommended = recommend_hardware(software, hardware);
  std::ostringstream output;
  output << "{\"name\":\"" << name << "\",\"software\":"
         << result_json(software) << ",\"hardware\":" << result_json(hardware)
         << ",\"speedup\":" << measured_speedup
         << ",\"recommend_hardware\":" << (recommended ? "true" : "false")
         << '}';
  return output.str();
}

}  // namespace

std::string run_hardware_self_test(const std::string& input,
                                   std::size_t start_frame,
                                   std::int64_t start_pts,
                                   std::size_t requested_frames) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input;
  (void)start_frame;
  (void)start_pts;
  (void)requested_frames;
  return "{\"schema\":\"hardware-self-test-v1\",\"status\":\"UNAVAILABLE\",\"reason\":\"media runtime unavailable\"}";
#else
  requested_frames = std::clamp<std::size_t>(requested_frames, 1, 120);
  const auto probe = probe_container(input);
  const auto [width, height] = benchmark_dimensions(probe);
  if (width <= 0 || height <= 0) {
    return "{\"schema\":\"hardware-self-test-v1\",\"status\":\"FAILED\",\"reason\":\"video dimensions unavailable\"}";
  }

  const auto decode_software = benchmark_decode(
      input, start_frame, start_pts, requested_frames, false);
  auto decode_hardware = benchmark_decode(
      input, start_frame, start_pts, requested_frames, true);
  if (decode_software.passed && decode_hardware.passed &&
      (decode_software.frames != decode_hardware.frames ||
       decode_software.width != decode_hardware.width ||
       decode_software.height != decode_hardware.height)) {
    decode_hardware.passed = false;
  }

  static std::atomic_uint64_t sequence{};
  const auto nonce = static_cast<std::uint64_t>(
      std::chrono::steady_clock::now().time_since_epoch().count()) ^ ++sequence;
  std::error_code path_error;
  const auto root = std::filesystem::temp_directory_path(path_error) /
      ("SmartCut64-HardwareSelfTest-" + std::to_string(nonce));
  if (path_error || !std::filesystem::create_directory(root, path_error)) {
    return "{\"schema\":\"hardware-self-test-v1\",\"status\":\"FAILED\",\"reason\":\"temporary directory unavailable\"}";
  }

  struct EncoderComparison {
    const char* name;
    VideoOutputCodec codec;
    int bit_depth;
    VideoHardwareEncoder hardware;
    BenchmarkResult software;
    BenchmarkResult accelerated;
  };
  std::vector<EncoderComparison> encoders{
      {"MPEG-2 (DVD)", VideoOutputCodec::mpeg2, 8, VideoHardwareEncoder::automatic, {}, {}},
      {"H.264", VideoOutputCodec::h264, 8, VideoHardwareEncoder::automatic, {}, {}},
      {"HEVC", VideoOutputCodec::hevc, 10, VideoHardwareEncoder::automatic, {}, {}},
      {"FFV1", VideoOutputCodec::ffv1, 8, VideoHardwareEncoder::vulkan, {}, {}},
      {"ProRes", VideoOutputCodec::prores, 10, VideoHardwareEncoder::vulkan, {}, {}}};
  for (std::size_t index = 0; index < encoders.size(); ++index) {
    auto& item = encoders[index];
    item.software = benchmark_encode(
        input, root / (std::to_string(index) + "-software.mkv"), start_frame,
        requested_frames, width, height, item.codec,
        VideoHardwareEncoder::software, item.bit_depth);
    item.accelerated = benchmark_encode(
        input, root / (std::to_string(index) + "-hardware.mkv"), start_frame,
        requested_frames, width, height, item.codec, item.hardware, item.bit_depth);
  }
  std::filesystem::remove(root, path_error);

  const bool preview_recommended = recommend_hardware(decode_software, decode_hardware);
  bool full_transcode_recommended = false;
  bool all_software_passed = decode_software.passed;
  for (const auto& encoder : encoders) {
    full_transcode_recommended = full_transcode_recommended ||
        recommend_hardware(encoder.software, encoder.accelerated);
    all_software_passed = all_software_passed && encoder.software.passed;
  }

  std::ostringstream output;
  output << "{\"schema\":\"hardware-self-test-v1\",\"status\":\""
         << (all_software_passed ? "PASSED" : "FAILED")
         << "\",\"sample_frames\":" << requested_frames
         << ",\"benchmark_width\":" << width
         << ",\"benchmark_height\":" << height
         << ",\"decode\":"
         << comparison_json("Preview decode", decode_software, decode_hardware)
         << ",\"encoders\":[";
  for (std::size_t index = 0; index < encoders.size(); ++index) {
    if (index != 0) output << ',';
    output << comparison_json(encoders[index].name, encoders[index].software,
                              encoders[index].accelerated);
  }
  output << "],\"recommendations\":{\"preview_hardware\":"
         << (preview_recommended ? "true" : "false")
         << ",\"full_transcode_hardware\":"
         << (full_transcode_recommended ? "true" : "false")
         << ",\"minimum_speedup\":1.1}}";
  return output.str();
#endif
}

}  // namespace smartcut::media
