#include "media/edit_export.hpp"

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <sstream>
#include <system_error>

#include "media/media_joiner.hpp"
#include "media/audio_transform.hpp"
#include "media/boundary_encode.hpp"
#include "media/container_mux.hpp"
#include "media/video_transform.hpp"
#include "media/stream_probe.hpp"
#include "media/mkv_copy.hpp"
#include "media/mkv_timed_range.hpp"

namespace smartcut::media {
namespace {

bool passed(const std::string& report) {
  const auto marker = report.find("\"status\":\"");
  return marker != std::string::npos && report.compare(marker + 10, 6, "PASSED") == 0;
}

std::filesystem::path temporary_sibling(const std::filesystem::path& output,
                                        const std::string& label,
                                        std::size_t index = 0,
                                        const std::string& forced_extension = {}) {
  const auto nonce = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  auto name = output.stem().string() + ".smartcut-" + std::to_string(nonce) + "-" + label;
  if (index != 0) name += "-" + std::to_string(index);
  name += !forced_extension.empty() ? forced_extension :
          (output.extension().empty() ? ".mkv" : output.extension().string());
  return output.parent_path() / name;
}

void remove_file(const std::filesystem::path& path) {
  std::error_code ignored;
  std::filesystem::remove(path, ignored);
}

bool promote_validated_output(const std::filesystem::path& staged,
                              const std::filesystem::path& output) {
  const auto backup = temporary_sibling(output, "previous");
  std::error_code error;
  const bool had_output = std::filesystem::exists(output, error) && !error;
  if (had_output) {
    std::filesystem::rename(output, backup, error);
    if (error) return false;
  }
  error.clear();
  std::filesystem::rename(staged, output, error);
  if (error) {
    if (had_output) {
      std::error_code restore_error;
      std::filesystem::rename(backup, output, restore_error);
    }
    return false;
  }
  if (had_output) remove_file(backup);
  return true;
}

std::string range_array(const std::vector<EditRange>& ranges) {
  std::ostringstream output;
  output << '[';
  for (std::size_t index = 0; index < ranges.size(); ++index) {
    if (index != 0) output << ',';
    output << "{\"first_frame\":" << ranges[index].first
           << ",\"end_frame_exclusive\":" << ranges[index].second << '}';
  }
  output << ']';
  return output.str();
}

std::string stream_array(const std::vector<std::size_t>& streams) {
  std::ostringstream output;
  output << '[';
  for (std::size_t index = 0; index < streams.size(); ++index) {
    if (index != 0) output << ',';
    output << streams[index];
  }
  output << ']';
  return output.str();
}

const char* mode_name(EditExportMode mode) {
  if (mode == EditExportMode::keyframe_copy) return "keyframe_copy";
  if (mode == EditExportMode::full_transcode) return "full_transcode";
  return "smart_render";
}

std::vector<EditRange> keyframe_aligned_ranges(const std::string& input, std::size_t frame_count,
                                               const std::vector<EditRange>& requested) {
  auto keyframes = mkv_random_access_frames(input, 0, frame_count);
  if (keyframes.empty() || keyframes.front() != 0) keyframes.insert(keyframes.begin(), 0);
  keyframes.push_back(frame_count);
  std::sort(keyframes.begin(), keyframes.end());
  keyframes.erase(std::unique(keyframes.begin(), keyframes.end()), keyframes.end());
  std::vector<EditRange> aligned;
  for (const auto& range : requested) {
    const auto first = std::lower_bound(keyframes.begin(), keyframes.end(), range.first);
    const auto end = std::lower_bound(keyframes.begin(), keyframes.end(), range.second);
    if (first != keyframes.end() && end != keyframes.end() && *first < *end) aligned.emplace_back(*first, *end);
  }
  if (aligned.empty()) return aligned;
  std::vector<EditRange> merged;
  for (const auto& range : aligned) { if (merged.empty() || range.first > merged.back().second) merged.push_back(range); else merged.back().second = std::max(merged.back().second, range.second); }
  return merged;
}

std::string failure_report(const std::string& reason,
                           const std::vector<EditRange>& removed,
                           const std::vector<EditRange>& kept,
                           const std::vector<std::size_t>& streams,
                           const std::string& detail = {}) {
  std::ostringstream report;
  report << "{\"schema\":\"edit-export-v1\",\"status\":\"FAILED\",\"reason\":\""
         << reason << "\",\"removed_ranges\":" << range_array(removed)
         << ",\"kept_ranges\":" << range_array(kept)
         << ",\"selected_streams\":" << stream_array(streams);
  if (!detail.empty()) report << ",\"detail\":" << detail;
  report << '}';
  return report.str();
}

}  // namespace

std::vector<EditRange> normalize_removed_ranges(std::size_t frame_count,
                                                std::vector<EditRange> ranges) {
  std::vector<EditRange> clamped;
  clamped.reserve(ranges.size());
  for (auto [first, end] : ranges) {
    first = std::min(first, frame_count);
    end = std::min(end, frame_count);
    if (first < end) clamped.emplace_back(first, end);
  }
  std::sort(clamped.begin(), clamped.end());
  std::vector<EditRange> normalized;
  for (const auto& range : clamped) {
    if (normalized.empty() || range.first > normalized.back().second) normalized.push_back(range);
    else normalized.back().second = std::max(normalized.back().second, range.second);
  }
  return normalized;
}

std::vector<EditRange> kept_ranges_for_edits(std::size_t frame_count,
                                             const std::vector<EditRange>& removed_ranges) {
  const auto removed = normalize_removed_ranges(frame_count, removed_ranges);
  std::vector<EditRange> kept;
  std::size_t cursor = 0;
  for (const auto& range : removed) {
    if (cursor < range.first) kept.emplace_back(cursor, range.first);
    cursor = std::max(cursor, range.second);
  }
  if (cursor < frame_count) kept.emplace_back(cursor, frame_count);
  return kept;
}

std::string export_mkv_edits(const std::string& input, const std::string& output,
                             std::size_t frame_count,
                             const std::vector<EditRange>& removed_ranges,
                             const std::vector<std::size_t>& selected_streams,
                             EditExportMode mode, std::int64_t audio_offset_ms,
                             const AudioTransformOptions& audio,
                             const VideoTransformOptions& video,
                             const MpegTsMuxOptions& transport_stream) {
  if (video_transform_requested(video)) mode = EditExportMode::full_transcode;
  const auto removed = normalize_removed_ranges(frame_count, removed_ranges);
  const auto requested_kept = kept_ranges_for_edits(frame_count, removed);
  const auto kept = mode == EditExportMode::keyframe_copy ? keyframe_aligned_ranges(input, frame_count, requested_kept) : requested_kept;
  if (frame_count == 0) return failure_report("source frame count is zero", removed, kept, selected_streams);
  if (kept.empty()) return failure_report("all source frames are cut", removed, kept, selected_streams);

  std::error_code path_error;
  if (std::filesystem::equivalent(std::filesystem::path(input), std::filesystem::path(output), path_error) && !path_error)
    return failure_report("input and output paths are identical", removed, kept, selected_streams);

  const std::filesystem::path destination(output);
  const auto output_container = output_container_for_path(output);
  if (output_container == OutputContainer::unsupported)
    return failure_report("unsupported output extension", removed, kept, selected_streams);
  std::vector<std::filesystem::path> temporary_files;
  const auto cleanup = [&] { for (const auto& file : temporary_files) remove_file(file); };
  const auto finish_output = [&](const std::filesystem::path& rendered,
                                 std::string& container_report) {
    if (output_container == OutputContainer::matroska)
      return promote_validated_output(rendered, destination);
    if (output_container == OutputContainer::elementary) {
      container_report = demultiplex_elementary_streams(rendered.string(), destination.string());
      return passed(container_report);
    }
    const auto delivery = temporary_sibling(destination, "delivery", 0, destination.extension().string());
    temporary_files.push_back(delivery);
    container_report = remux_media_container(rendered.string(), delivery.string(), transport_stream);
    return passed(container_report) && promote_validated_output(delivery, destination);
  };
  const auto apply_audio_profile = [&](const std::filesystem::path& rendered,
                                       std::filesystem::path& profiled,
                                       std::string& audio_report) {
    profiled = rendered;
    if (!audio_transform_requested(audio)) return true;
    const auto transformed = temporary_sibling(destination, "audio-profile", 0, ".mkv");
    temporary_files.push_back(transformed);
    audio_report = transform_primary_audio_mkv(rendered.string(), transformed.string(), audio);
    if (!passed(audio_report)) return false;
    profiled = transformed;
    return true;
  };
  std::vector<std::filesystem::path> segments;
  std::vector<std::string> segment_reports;

  if (removed.empty() && mode != EditExportMode::full_transcode) {
    const auto staged = temporary_sibling(destination, "complete", 0, ".mkv");
    temporary_files.push_back(staged);
    auto export_report = selected_streams.empty()
        ? export_mkv(input, staged.string())
        : export_mkv_selected_streams(input, staged.string(), selected_streams);
    // Some program streams (notably DVD MPEG-2/LPCM) omit presentation
    // timestamps on reordered video packets. A whole-file packet remux cannot
    // represent those directly in Matroska, while the timed range path repairs
    // them using the same audited logic used by edited exports.
    if (!passed(export_report) && selected_streams.empty()) {
      remove_file(staged);
      export_report = export_mkv_range(input, staged.string(), 0, frame_count);
    }
    if (!passed(export_report)) { cleanup(); return failure_report("complete-source export failed", removed, kept, selected_streams); }
    auto promoted = staged; std::string audio_shift_report;
    if (audio_offset_ms != 0 && primary_audio_track(staged.string())) { const auto adjusted = temporary_sibling(destination, "audio-shift", 0, ".mkv"); temporary_files.push_back(adjusted); audio_shift_report = shift_mkv_audio_timestamps(staged.string(), adjusted.string(), audio_offset_ms); if (!passed(audio_shift_report)) { cleanup(); return failure_report("audio synchronization adjustment failed", removed, kept, selected_streams); } promoted = adjusted; }
    std::filesystem::path profiled; std::string audio_profile_report;
    if (!apply_audio_profile(promoted, profiled, audio_profile_report)) { cleanup(); return failure_report("audio profile transform failed", removed, kept, selected_streams); }
    std::string container_report;
    if (!finish_output(profiled, container_report)) { cleanup(); return failure_report("container delivery or destination replacement failed", removed, kept, selected_streams, container_report); }
    cleanup();
    temporary_files.clear();
    std::ostringstream report;
    report << "{\"schema\":\"edit-export-v1\",\"status\":\"PASSED\",\"mode\":\"" << mode_name(mode) << "\",\"frame_count\":"
           << frame_count << ",\"removed_ranges\":[],\"kept_ranges\":" << range_array(kept)
           << ",\"selected_streams\":" << stream_array(selected_streams)
           << ",\"audio_offset_ms\":" << audio_offset_ms << ",\"export_report\":" << export_report;
    if (!audio_shift_report.empty()) report << ",\"audio_shift_report\":" << audio_shift_report;
    if (!audio_profile_report.empty()) report << ",\"audio_profile_report\":" << audio_profile_report;
    if (!container_report.empty()) report << ",\"container_report\":" << container_report;
    report << '}';
    return report.str();
  }

  for (std::size_t index = 0; index < kept.size(); ++index) {
    const auto raw = temporary_sibling(destination, "range", index + 1, ".mkv");
    temporary_files.push_back(raw);
    std::string range_report;
    if (mode == EditExportMode::smart_render) range_report = export_mkv_range(input, raw.string(), kept[index].first, kept[index].second);
    else if (mode == EditExportMode::keyframe_copy) {
      const auto copy = copy_mkv_timed_range(input, raw.string(), kept[index].first, kept[index].second);
      const auto validation = copy.find("\"error\"") == std::string::npos ? validate_mkv_timed_range(input, raw.string(), kept[index].first, kept[index].second) : std::string{"{\"status\":\"FAILED VALIDATION\"}"};
      range_report = std::string("{\"schema\":\"keyframe-copy-range-v1\",\"status\":\"") + (passed(validation) ? "PASSED" : "FAILED VALIDATION") + "\",\"copy_report\":" + copy + ",\"validation_report\":" + validation + "}";
    } else {
      const auto encoded = temporary_sibling(destination, "transcoded-video", index + 1, ".mkv"); temporary_files.push_back(encoded);
      const bool transform_video = video_transform_requested(video);
      const auto encode = transform_video
                              ? transcode_mkv_video_range(input, encoded.string(), kept[index].first,
                                                          kept[index].second, video)
                              : encode_mkv_boundary(input, encoded.string(), kept[index].first,
                                                    kept[index].second);
      const bool encode_ok = transform_video ? passed(encode) : encode.find("\"error\"") == std::string::npos;
      const auto mux = encode_ok ? mux_mkv_boundary_timed_range(input, encoded.string(), raw.string(), kept[index].first, kept[index].second) : std::string{"{\"error\":\"encode stage failed\"}"};
      const auto boundary_validation = mux.find("\"error\"") == std::string::npos
                                           ? (transform_video
                                                  ? validate_transcoded_video(raw.string(), video)
                                                  : validate_mkv_boundary(input, raw.string(),
                                                                          kept[index].second - kept[index].first))
                                           : std::string{"{\"status\":\"FAILED VALIDATION\"}"};
      const auto stream_validation = mux.find("\"error\"") == std::string::npos ? validate_mkv_boundary_timed_range(input, encoded.string(), raw.string(), kept[index].first, kept[index].second) : std::string{"{\"status\":\"FAILED VALIDATION\"}"};
      const bool valid = passed(boundary_validation) && passed(stream_validation);
      range_report = std::string("{\"schema\":\"full-transcode-range-v1\",\"status\":\"") + (valid ? "PASSED" : "FAILED VALIDATION") + "\",\"encode_report\":" + encode + ",\"mux_report\":" + mux + ",\"boundary_validation\":" + boundary_validation + ",\"stream_validation\":" + stream_validation + "}";
    }
    if (!passed(range_report)) { cleanup(); return failure_report("kept-range export failed", removed, kept, selected_streams, range_report); }
    segment_reports.push_back(range_report);
    if (selected_streams.empty()) {
      segments.push_back(raw);
      continue;
    }
    const auto filtered = temporary_sibling(destination, "streams", index + 1, ".mkv");
    temporary_files.push_back(filtered);
    const auto selection_report = export_mkv_selected_streams(raw.string(), filtered.string(), selected_streams);
    segment_reports.push_back(selection_report);
    if (!passed(selection_report)) { cleanup(); return failure_report("stream selection failed", removed, kept, selected_streams); }
    segments.push_back(filtered);
  }

  const auto staged = temporary_sibling(destination, "joined", 0, ".mkv");
  temporary_files.push_back(staged);
  std::string join_report;
  if (segments.size() == 1) {
    std::error_code copy_error;
    std::filesystem::copy_file(segments.front(), staged, std::filesystem::copy_options::overwrite_existing, copy_error);
    if (copy_error) { cleanup(); return failure_report("single kept segment staging failed", removed, kept, selected_streams); }
    join_report = "{\"schema\":\"media-joiner-v1\",\"status\":\"PASSED\",\"inputs\":1}";
  } else {
    join_report = join_media_files(segments, staged);
    if (!passed(join_report)) { cleanup(); return failure_report("kept segment join failed", removed, kept, selected_streams, join_report); }
  }
  auto promoted = staged; std::string audio_shift_report;
  if (audio_offset_ms != 0 && primary_audio_track(staged.string())) { const auto adjusted = temporary_sibling(destination, "audio-shift", 0, ".mkv"); temporary_files.push_back(adjusted); audio_shift_report = shift_mkv_audio_timestamps(staged.string(), adjusted.string(), audio_offset_ms); if (!passed(audio_shift_report)) { cleanup(); return failure_report("audio synchronization adjustment failed", removed, kept, selected_streams); } promoted = adjusted; }
  std::filesystem::path profiled; std::string audio_profile_report;
  if (!apply_audio_profile(promoted, profiled, audio_profile_report)) { cleanup(); return failure_report("audio profile transform failed", removed, kept, selected_streams); }
  std::string container_report;
  if (!finish_output(profiled, container_report)) { cleanup(); return failure_report("container delivery or destination replacement failed", removed, kept, selected_streams, container_report); }
  cleanup();

  std::ostringstream report;
  report << "{\"schema\":\"edit-export-v1\",\"status\":\"PASSED\",\"mode\":\"" << mode_name(mode) << "\",\"frame_count\":"
         << frame_count << ",\"removed_ranges\":" << range_array(removed) << ",\"requested_kept_ranges\":" << range_array(requested_kept)
         << ",\"kept_ranges\":" << range_array(kept)
         << ",\"selected_streams\":" << stream_array(selected_streams) << ",\"audio_offset_ms\":" << audio_offset_ms
         << ",\"segment_reports\":[";
  for (std::size_t index = 0; index < segment_reports.size(); ++index) {
    if (index != 0) report << ',';
    report << segment_reports[index];
  }
  report << "],\"join_report\":" << join_report;
  if (!audio_shift_report.empty()) report << ",\"audio_shift_report\":" << audio_shift_report;
  if (!audio_profile_report.empty()) report << ",\"audio_profile_report\":" << audio_profile_report;
  if (!container_report.empty()) report << ",\"container_report\":" << container_report;
  report << '}';
  return report.str();
}

}  // namespace smartcut::media
