#include "media/container_mux.hpp"

#include "media/mkv_copy.hpp"
#include "media/pcm_normalize.hpp"
#include "media/s337m_bridge.hpp"

#include <algorithm>
#include <chrono>
#include <cctype>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <limits>
#include <sstream>
#include <system_error>
#include <vector>

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavcodec/bsf.h>
#include <libavformat/avformat.h>
}
#endif

namespace smartcut::media {
namespace {

std::string lower_extension(const std::string& path) {
  auto extension = std::filesystem::path(path).extension().string();
  std::transform(extension.begin(), extension.end(), extension.begin(),
                 [](unsigned char value) { return static_cast<char>(std::tolower(value)); });
  return extension;
}

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_mkv(const std::filesystem::path& output, const char* label) {
  const auto nonce = std::chrono::high_resolution_clock::now().time_since_epoch().count();
  return output.parent_path() /
         (output.stem().string() + ".smartcut-" + label + "-" + std::to_string(nonce) + ".mkv");
}

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

std::string unsupported_report(const std::string& reason) {
  return "{\"schema\":\"container-remux-v1\",\"status\":\"FAILED\",\"reason\":\"" + reason + "\"}";
}

#ifdef SMARTCUT_HAS_FFMPEG
const char* muxer_name(OutputContainer container) {
  switch (container) {
    case OutputContainer::matroska: return "matroska";
    case OutputContainer::mp4: return "mp4";
    case OutputContainer::mov: return "mov";
    case OutputContainer::mpeg_ps: return "mpeg";
    case OutputContainer::mpeg_ts: return "mpegts";
    case OutputContainer::wtv: return "wtv";
    case OutputContainer::dvr_ms: return "asf";
    case OutputContainer::topfield_rec: return "mpegts";
    case OutputContainer::mxf: return "mxf";
    case OutputContainer::elementary: return nullptr;
    default: return nullptr;
  }
}

bool copy_chapters_supported(OutputContainer container) {
  return container == OutputContainer::matroska || container == OutputContainer::mp4 ||
         container == OutputContainer::mov;
}

bool transport_stream_container(OutputContainer container) {
  return container == OutputContainer::mpeg_ts || container == OutputContainer::topfield_rec;
}

void copy_stream_attributes(AVStream* output, const AVStream* input) {
  output->disposition = input->disposition;
  output->avg_frame_rate = input->avg_frame_rate;
  output->sample_aspect_ratio = input->sample_aspect_ratio;
  av_dict_copy(&output->metadata, input->metadata, 0);
}

void copy_chapters(AVFormatContext* output, const AVFormatContext* input) {
  for (unsigned index = 0; index < input->nb_chapters; ++index) {
    const auto* chapter = input->chapters[index];
    auto* copied = static_cast<AVChapter*>(av_mallocz(sizeof(AVChapter)));
    if (!copied) continue;
    copied->id = chapter->id;
    copied->time_base = chapter->time_base;
    copied->start = chapter->start;
    copied->end = chapter->end;
    av_dict_copy(&copied->metadata, chapter->metadata, 0);
    auto** resized = static_cast<AVChapter**>(
        av_realloc_array(output->chapters, output->nb_chapters + 1, sizeof(*output->chapters)));
    if (!resized) {
      av_dict_free(&copied->metadata);
      av_free(copied);
      continue;
    }
    output->chapters = resized;
    output->chapters[output->nb_chapters++] = copied;
  }
}

std::string codec_name(AVCodecID codec) {
  const auto* name = avcodec_get_name(codec);
  return name ? name : "unknown";
}

bool text_subtitle_codec(AVCodecID codec) {
  return codec == AV_CODEC_ID_SUBRIP || codec == AV_CODEC_ID_ASS ||
         codec == AV_CODEC_ID_WEBVTT || codec == AV_CODEC_ID_TEXT ||
         codec == AV_CODEC_ID_MOV_TEXT;
}

AVCodecID automatic_subtitle_codec(OutputContainer container, AVCodecID source) {
  if ((container == OutputContainer::mp4 || container == OutputContainer::mov) &&
      text_subtitle_codec(source)) return AV_CODEC_ID_MOV_TEXT;
  return source;
}

std::string restore_dolby_e_s337m(const std::string& input, const std::string& output) {
  AVFormatContext* source = nullptr;
  AVPacket* packet = av_packet_alloc();
  int dolby_stream = -1;
  bool completed = false;
  std::ofstream destination;
  auto cleanup = [&] {
    destination.close(); av_packet_free(&packet); avformat_close_input(&source);
    if (!completed) remove_file(output);
  };
  if (!packet || avformat_open_input(&source, input.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(source, nullptr) < 0) {
    cleanup(); return unsupported_report("Dolby E bridge input open failed");
  }
  for (unsigned index = 0; index < source->nb_streams; ++index) {
    AVCodecID origin = AV_CODEC_ID_NONE;
    if (source->streams[index]->codecpar->codec_id == AV_CODEC_ID_DOLBY_E ||
        (bitstream_audio_origin(source->streams[index], &origin) && origin == AV_CODEC_ID_DOLBY_E)) {
      dolby_stream = static_cast<int>(index); break;
    }
  }
  if (dolby_stream < 0) {
    cleanup(); return unsupported_report("no Dolby E/SMPTE 337M bridge stream found");
  }
  destination.open(output, std::ios::binary | std::ios::trunc);
  if (!destination) {
    cleanup(); return unsupported_report("Dolby E elementary output open failed");
  }
  std::size_t packets = 0;
  std::size_t payload_bytes = 0;
  std::uint64_t expected_hash = 14695981039346656037ULL;
  while (av_read_frame(source, packet) >= 0) {
    if (packet->stream_index != dolby_stream) { av_packet_unref(packet); continue; }
    constexpr std::uint8_t carrier_preamble[]{0x72, 0xf8, 0x1f, 0x4e};
    const bool opaque_carrier = packet->size >= 12 &&
        std::memcmp(packet->data, carrier_preamble, sizeof(carrier_preamble)) == 0;
    if (opaque_carrier) {
      destination.write(reinterpret_cast<const char*>(packet->data), packet->size);
      for (int offset = 8; offset + 1 < packet->size; offset += 2) {
        expected_hash ^= packet->data[offset + 1]; expected_hash *= 1099511628211ULL;
        expected_hash ^= packet->data[offset]; expected_hash *= 1099511628211ULL;
        payload_bytes += 2;
      }
    } else {
      const unsigned first_header = packet->size >= 3
          ? (static_cast<unsigned>(packet->data[0]) << 16U) |
            (static_cast<unsigned>(packet->data[1]) << 8U) |
            static_cast<unsigned>(packet->data[2]) : 0U;
      if ((first_header & 0xfffe00U) != 0x078e00U || (packet->size & 1) != 0) {
        const auto size = packet->size;
        av_packet_unref(packet); cleanup();
        return unsupported_report("Dolby E bridge packet is neither an opaque SMPTE 337M burst nor a legacy decoded burst; bytes=" +
                                  std::to_string(size));
      }
      constexpr std::uint8_t carrier_header[]{0x72, 0xF8, 0x1F, 0x4E, 0x1C, 0x00, 0x00, 0xE4};
      destination.write(reinterpret_cast<const char*>(carrier_header), sizeof(carrier_header));
      for (int offset = 0; offset + 1 < packet->size; offset += 2) {
        destination.put(static_cast<char>(packet->data[offset + 1]));
        destination.put(static_cast<char>(packet->data[offset]));
        expected_hash ^= packet->data[offset]; expected_hash *= 1099511628211ULL;
        expected_hash ^= packet->data[offset + 1]; expected_hash *= 1099511628211ULL;
        payload_bytes += 2;
      }
    }
    ++packets;
    av_packet_unref(packet);
  }
  destination.close();
  avformat_close_input(&source);
  std::vector<S337mFrame> validation_frames;
  std::string validation_reason;
  const bool parsed = parse_s337m_16le(output, &validation_frames, &validation_reason);
  const auto actual_hash = parsed ? s337m_payload_hash(validation_frames) : 0;
  const auto validation_bytes = parsed ? s337m_payload_bytes(validation_frames) : 0;
  const bool valid = packets > 0 && parsed && packets == validation_frames.size() &&
      payload_bytes == validation_bytes && expected_hash == actual_hash;
  completed = valid; cleanup();
  std::ostringstream report;
  report << "{\"schema\":\"dolby-e-s337m-bridge-v1\",\"status\":\""
         << (valid ? "PASSED" : "FAILED VALIDATION") << "\",\"lossless\":true,\"packets\":"
         << packets << ",\"payload_bytes\":" << payload_bytes
         << ",\"payload_hash_match\":" << (expected_hash == actual_hash ? "true" : "false")
         << ",\"carrier_validation\":\"" << (parsed ? "parsed-16le" : "failed") << "\"}";
  return report.str();
}
#endif

template <typename Exporter>
std::string export_through_matroska(const std::string& input, const std::string& output,
                                    Exporter exporter,
                                    const MpegTsMuxOptions& transport_stream = {}) {
  const auto container = output_container_for_path(output);
  if (container == OutputContainer::unsupported)
    return unsupported_report("unsupported output extension");
  if (container == OutputContainer::matroska) return exporter(input, output);

  const std::filesystem::path destination(output);
  const auto intermediate = temporary_mkv(destination, "container-stage");
  const auto stage_report = exporter(input, intermediate.string());
  if (!passed(stage_report)) {
    remove_file(intermediate);
    return "{\"schema\":\"container-export-v1\",\"status\":\"FAILED\",\"reason\":\"Matroska render stage failed\",\"stage_report\":" + stage_report + "}";
  }
  const auto remux_report = container == OutputContainer::elementary
                                ? demultiplex_elementary_streams(intermediate.string(), output)
                                : remux_media_container(intermediate.string(), output, transport_stream);
  remove_file(intermediate);
  std::ostringstream report;
  report << "{\"schema\":\"container-export-v1\",\"status\":\""
         << (passed(remux_report) ? "PASSED" : "FAILED") << "\",\"container\":\""
         << output_container_name(container) << "\",\"smart_copy\":true,\"stage_report\":"
         << stage_report << ",\"remux_report\":" << remux_report << '}';
  return report.str();
}

}  // namespace

OutputContainer output_container_for_path(const std::string& path) {
  const auto extension = lower_extension(path);
  if (extension == ".mkv") return OutputContainer::matroska;
  if (extension == ".mp4" || extension == ".m4v") return OutputContainer::mp4;
  if (extension == ".mov") return OutputContainer::mov;
  if (extension == ".mpg" || extension == ".mpeg" || extension == ".vob") return OutputContainer::mpeg_ps;
  if (extension == ".ts" || extension == ".m2ts" || extension == ".mts") return OutputContainer::mpeg_ts;
  if (extension == ".wtv") return OutputContainer::wtv;
  if (extension == ".dvr-ms" || extension == ".dvrms") return OutputContainer::dvr_ms;
  if (extension == ".rec") return OutputContainer::topfield_rec;
  if (extension == ".mxf") return OutputContainer::mxf;
  if (extension == ".m2v" || extension == ".mpv" || extension == ".h264" ||
      extension == ".264" || extension == ".avc" || extension == ".hevc" ||
      extension == ".h265" || extension == ".265" || extension == ".obu" ||
      extension == ".ivf" || extension == ".aac" || extension == ".ac3" ||
      extension == ".eac3" || extension == ".mp2" || extension == ".mp3" ||
      extension == ".opus" || extension == ".flac" || extension == ".wav" ||
      extension == ".dts" || extension == ".thd") return OutputContainer::elementary;
  if (extension == ".s337m" || extension == ".dolbye") return OutputContainer::elementary;
  return OutputContainer::unsupported;
}

const char* output_container_name(OutputContainer container) {
  switch (container) {
    case OutputContainer::matroska: return "matroska";
    case OutputContainer::mp4: return "mp4";
    case OutputContainer::mov: return "mov";
    case OutputContainer::mpeg_ps: return "mpeg-ps";
    case OutputContainer::mpeg_ts: return "mpegts";
    case OutputContainer::wtv: return "wtv";
    case OutputContainer::dvr_ms: return "dvr-ms";
    case OutputContainer::topfield_rec: return "topfield-rec";
    case OutputContainer::mxf: return "mxf";
    case OutputContainer::elementary: return "elementary";
    default: return "unsupported";
  }
}

std::string remux_media_container(const std::string& input, const std::string& output,
                                  const MpegTsMuxOptions& transport_stream,
                                  const ContainerMetadata& metadata) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input;
  (void)output;
  (void)transport_stream;
  (void)metadata;
  return unsupported_report("FFmpeg support is not enabled");
#else
  const auto container = output_container_for_path(output);
  const auto* format = muxer_name(container);
  if (!format) return unsupported_report("unsupported output extension");
  std::error_code equivalent_error;
  if (std::filesystem::equivalent(input, output, equivalent_error) && !equivalent_error)
    return unsupported_report("input and output paths are identical");

  AVFormatContext* source = nullptr;
  AVFormatContext* destination = nullptr;
  AVPacket* packet = av_packet_alloc();
  std::vector<AVCodecContext*> subtitle_decoders;
  std::vector<AVCodecContext*> subtitle_encoders;
  bool completed = false;
  auto cleanup = [&] {
    for (auto*& context : subtitle_decoders) avcodec_free_context(&context);
    for (auto*& context : subtitle_encoders) avcodec_free_context(&context);
    av_packet_free(&packet);
    avformat_close_input(&source);
    if (destination && destination->pb) avio_closep(&destination->pb);
    avformat_free_context(destination);
    if (!completed) remove_file(output);
  };
  if (!packet || avformat_open_input(&source, input.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(source, nullptr) < 0) {
    cleanup();
    return unsupported_report("input open failed");
  }
  if (avformat_alloc_output_context2(&destination, nullptr, format, output.c_str()) < 0 || !destination) {
    cleanup();
    return unsupported_report("output context failed");
  }

  std::vector<int> stream_map(source->nb_streams, -1);
  subtitle_decoders.resize(source->nb_streams, nullptr);
  subtitle_encoders.resize(source->nb_streams, nullptr);
  std::vector<AVCodecID> expected_codecs;
  const bool m2ts_delivery = transport_stream_container(container) &&
      (transport_stream.m2ts_mode || lower_extension(output) == ".m2ts");
  int m2ts_video_pid = 0x1011;
  int m2ts_audio_pid = 0x1100;
  int m2ts_subtitle_pid = 0x1200;
  std::vector<bool> restore_bluray_pcm(source->nb_streams, false);
  std::vector<bool> restore_bitstream_audio(source->nb_streams, false);
  std::vector<AVCodecID> restored_bitstream_codec(source->nb_streams, AV_CODEC_ID_NONE);
  std::vector<bool> private_mpegts_subtitle(source->nb_streams, false);
  std::vector<int> restore_bluray_depth(source->nb_streams, 0);
  std::vector<int> restore_bluray_layout(source->nb_streams, 0);
  std::vector<int> source_for_output;
  std::size_t dropped_attachments = 0;
  std::size_t converted_subtitle_streams = 0;
  for (unsigned index = 0; index < source->nb_streams; ++index) {
    const auto* input_stream = source->streams[index];
    const auto codec = input_stream->codecpar->codec_id;
    const auto media_type = input_stream->codecpar->codec_type;
    if (media_type == AVMEDIA_TYPE_ATTACHMENT) {
      if (container != OutputContainer::matroska) {
        ++dropped_attachments;
        continue;
      }
      const auto reason = "container " + std::string(output_container_name(container)) +
                          " does not support stream " + std::to_string(index) + " codec " + codec_name(codec);
      cleanup();
      return unsupported_report(reason);
    }
    if (media_type == AVMEDIA_TYPE_SUBTITLE &&
        (container == OutputContainer::mp4 || container == OutputContainer::mov) &&
        !text_subtitle_codec(codec)) {
      const auto reason = "subtitle stream " + std::to_string(index) + " codec " + codec_name(codec) +
          " is bitmap/graphics based and cannot be represented in MP4/MOV; choose Matroska MKV "
          "to preserve it, deselect the track, or convert it with OCR to a text subtitle first";
      cleanup();
      return unsupported_report(reason);
    }
    auto* output_stream = avformat_new_stream(destination, nullptr);
    if (!output_stream || avcodec_parameters_copy(output_stream->codecpar, input_stream->codecpar) < 0) {
      cleanup();
      return unsupported_report("stream setup failed");
    }
    const auto subtitle_codec = media_type == AVMEDIA_TYPE_SUBTITLE
        ? automatic_subtitle_codec(container, codec) : codec;
    if (media_type == AVMEDIA_TYPE_SUBTITLE && subtitle_codec != codec) {
      const AVCodec* decoder_codec = avcodec_find_decoder(codec);
      const AVCodec* encoder_codec = avcodec_find_encoder(subtitle_codec);
      auto*& decoder = subtitle_decoders[index];
      auto*& encoder = subtitle_encoders[index];
      decoder = decoder_codec ? avcodec_alloc_context3(decoder_codec) : nullptr;
      encoder = encoder_codec ? avcodec_alloc_context3(encoder_codec) : nullptr;
      if (!decoder || !encoder || avcodec_parameters_to_context(decoder, input_stream->codecpar) < 0) {
        cleanup();
        return unsupported_report("required subtitle decoder/encoder is unavailable");
      }
      decoder->pkt_timebase = input_stream->time_base;
      encoder->time_base = input_stream->time_base;
      encoder->width = input_stream->codecpar->width;
      encoder->height = input_stream->codecpar->height;
      const int decoder_open = avcodec_open2(decoder, decoder_codec, nullptr);
      if (decoder_open >= 0 && decoder->subtitle_header && decoder->subtitle_header_size > 0) {
        encoder->subtitle_header = static_cast<std::uint8_t*>(
            av_memdup(decoder->subtitle_header, decoder->subtitle_header_size));
        encoder->subtitle_header_size = encoder->subtitle_header
            ? decoder->subtitle_header_size : 0;
      }
      const int encoder_open = decoder_open >= 0 ? avcodec_open2(encoder, encoder_codec, nullptr)
                                                  : AVERROR_UNKNOWN;
      const int parameters_copy = encoder_open >= 0
          ? avcodec_parameters_from_context(output_stream->codecpar, encoder) : AVERROR_UNKNOWN;
      if (decoder_open < 0 || encoder_open < 0 || parameters_copy < 0) {
        char decoder_error[AV_ERROR_MAX_STRING_SIZE]{};
        char encoder_error[AV_ERROR_MAX_STRING_SIZE]{};
        char parameters_error[AV_ERROR_MAX_STRING_SIZE]{};
        av_strerror(decoder_open, decoder_error, sizeof(decoder_error));
        av_strerror(encoder_open, encoder_error, sizeof(encoder_error));
        av_strerror(parameters_copy, parameters_error, sizeof(parameters_error));
        const auto reason = "subtitle conversion setup failed for stream " + std::to_string(index) +
            " from " + codec_name(codec) + " to " + codec_name(subtitle_codec) +
            "; decoder=" + decoder_error + "; encoder=" + encoder_error +
            "; parameters=" + parameters_error;
        cleanup();
        return unsupported_report(reason);
      }
      ++converted_subtitle_streams;
    }
    restore_bluray_pcm[index] = m2ts_delivery && pcm_bluray_origin(
        input_stream, &restore_bluray_depth[index], &restore_bluray_layout[index]);
    restore_bitstream_audio[index] = bitstream_audio_origin(
        input_stream, &restored_bitstream_codec[index]);
    if (restore_bitstream_audio[index]) {
      if (restored_bitstream_codec[index] == AV_CODEC_ID_S302M &&
          transport_stream_container(container)) {
        output_stream->codecpar->codec_id = AV_CODEC_ID_S302M;
      } else {
        const auto reason = restored_bitstream_codec[index] == AV_CODEC_ID_DOLBY_E
            ? "Dolby E is preserved as a lossless SMPTE 337M editing bridge; choose .s337m/.dolbye "
              "elementary output, or a transport workflow that explicitly carries SMPTE 302M"
            : "SMPTE 302M audio requires MPEG-TS/REC delivery; choose Transport Stream";
        cleanup();
        return unsupported_report(reason);
      }
    }
    private_mpegts_subtitle[index] = transport_stream_container(container) &&
        codec == AV_CODEC_ID_DVD_SUBTITLE;
    if (restore_bluray_pcm[index]) {
      output_stream->codecpar->codec_id = AV_CODEC_ID_PCM_BLURAY;
      output_stream->codecpar->codec_tag = 0;
      output_stream->codecpar->bits_per_coded_sample = restore_bluray_depth[index];
      output_stream->codecpar->bits_per_raw_sample = restore_bluray_depth[index];
      output_stream->codecpar->block_align = 0;
      if (!set_pcm_bluray_channel_layout(output_stream->codecpar, restore_bluray_layout[index])) {
        cleanup();
        return unsupported_report("invalid preserved Blu-ray LPCM channel layout");
      }
    }
    output_stream->codecpar->codec_tag = 0;
    if (m2ts_delivery) {
      if (media_type == AVMEDIA_TYPE_VIDEO) output_stream->id = m2ts_video_pid++;
      else if (media_type == AVMEDIA_TYPE_AUDIO) output_stream->id = m2ts_audio_pid++;
      else if (media_type == AVMEDIA_TYPE_SUBTITLE) output_stream->id = m2ts_subtitle_pid++;
    }
    output_stream->time_base = input_stream->time_base;
    copy_stream_attributes(output_stream, input_stream);
    stream_map[index] = output_stream->index;
    if (source_for_output.size() <= static_cast<std::size_t>(output_stream->index))
      source_for_output.resize(static_cast<std::size_t>(output_stream->index) + 1U, -1);
    source_for_output[static_cast<std::size_t>(output_stream->index)] = static_cast<int>(index);
    expected_codecs.push_back(output_stream->codecpar->codec_id);
  }
  av_dict_copy(&destination->metadata, source->metadata, 0);
  for (const auto& [key, value] : metadata) {
    if (key.empty()) continue;
    av_dict_set(&destination->metadata, key.c_str(), value.empty() ? nullptr : value.c_str(), 0);
  }
  if (transport_stream_container(container)) {
    av_dict_set(&destination->metadata, "service_name", transport_stream.service_name.c_str(), 0);
    av_dict_set(&destination->metadata, "service_provider", transport_stream.service_provider.c_str(), 0);
  }
  if (copy_chapters_supported(container)) copy_chapters(destination, source);
  if (!(destination->oformat->flags & AVFMT_NOFILE) &&
      avio_open(&destination->pb, output.c_str(), AVIO_FLAG_WRITE) < 0) {
    cleanup();
    return unsupported_report("output open failed");
  }
  AVDictionary* options = nullptr;
  if (container == OutputContainer::mp4 || container == OutputContainer::mov)
    av_dict_set(&options, "movflags", "+faststart", 0);
  if (transport_stream_container(container)) {
    const auto set_integer = [&options](const char* name, std::int64_t value) {
      av_dict_set(&options, name, std::to_string(value).c_str(), 0);
    };
    const auto set_decimal = [&options](const char* name, double value) {
      std::ostringstream text;
      text << value;
      av_dict_set(&options, name, text.str().c_str(), 0);
    };
    set_integer("mpegts_transport_stream_id", transport_stream.transport_stream_id);
    set_integer("mpegts_original_network_id", transport_stream.original_network_id);
    set_integer("mpegts_service_id", transport_stream.service_id);
    set_integer("mpegts_pmt_start_pid", transport_stream.pmt_start_pid);
    set_integer("mpegts_start_pid", transport_stream.elementary_start_pid);
    set_integer("mpegts_m2ts_mode", transport_stream.m2ts_mode || lower_extension(output) == ".m2ts" ? 1 : 0);
    if (transport_stream.mux_rate > 0) set_integer("muxrate", transport_stream.mux_rate);
    if (transport_stream.pcr_period_ms >= 0) set_integer("pcr_period", transport_stream.pcr_period_ms);
    if (transport_stream.pat_period_seconds > 0.0) set_decimal("pat_period", transport_stream.pat_period_seconds);
    if (transport_stream.sdt_period_seconds > 0.0) set_decimal("sdt_period", transport_stream.sdt_period_seconds);
    std::string flags;
    const auto add_flag = [&flags](const char* value) { if (!flags.empty()) flags += '+'; flags += value; };
    if (transport_stream.resend_headers) add_flag("resend_headers");
    if (transport_stream.system_b) add_flag("system_b");
    if (transport_stream.emit_nit) add_flag("nit");
    if (!flags.empty()) av_dict_set(&options, "mpegts_flags", flags.c_str(), 0);
  }
  const int header_result = avformat_write_header(destination, &options);
  av_dict_free(&options);
  if (header_result < 0) {
    char error_text[AV_ERROR_MAX_STRING_SIZE]{};
    av_strerror(header_result, error_text, sizeof(error_text));
    cleanup();
    return unsupported_report(std::string("header write failed; one or more streams are incompatible with the selected container: ") + error_text);
  }

  std::vector<std::size_t> input_packets(source->nb_streams, 0);
  std::vector<std::int64_t> last_output_dts(destination->nb_streams, AV_NOPTS_VALUE);
  struct PcmAudit {
    std::size_t packets{};
    std::size_t bytes{};
    std::uint64_t hash{14695981039346656037ULL};
    void add(const std::uint8_t* data, std::size_t size) {
      ++packets;
      bytes += size;
      for (std::size_t i = 0; i < size; ++i) { hash ^= data[i]; hash *= 1099511628211ULL; }
    }
    void add(const std::vector<std::uint8_t>& data) { add(data.data(), data.size()); }
  };
  std::vector<PcmAudit> expected_pcm(source->nb_streams);
  std::vector<PcmAudit> expected_bitstream_audio(source->nb_streams);
  std::vector<PcmAudit> expected_private_subtitle(source->nb_streams);
  std::size_t converted_subtitle_packets = 0;
  int result = 0;
  int failed_stream = -1;
  int failed_packet_size = 0;
  std::uint32_t failed_packet_prefix = 0;
  const char* failed_phase = "none";
  while (av_read_frame(source, packet) >= 0) {
    const auto source_index = static_cast<unsigned>(packet->stream_index);
    const int target_index = stream_map[source_index];
    if (target_index < 0) {
      av_packet_unref(packet);
      continue;
    }
    const auto* input_stream = source->streams[source_index];
    auto* output_stream = destination->streams[target_index];
    if (subtitle_decoders[source_index] && subtitle_encoders[source_index]) {
      AVSubtitle subtitle{};
      int got_subtitle = 0;
      const int decoded = avcodec_decode_subtitle2(
          subtitle_decoders[source_index], &subtitle, &got_subtitle, packet);
      if (decoded < 0 || !got_subtitle) {
        avsubtitle_free(&subtitle);
        result = decoded < 0 ? decoded : AVERROR_INVALIDDATA;
        failed_stream = static_cast<int>(source_index);
        failed_phase = "subtitle-decode";
        av_packet_unref(packet);
        break;
      }
      std::vector<std::uint8_t> encoded_payload(
          std::max<std::size_t>(1U << 20U, static_cast<std::size_t>(packet->size) * 8U + 4096U));
      const int encoded = avcodec_encode_subtitle(subtitle_encoders[source_index],
          encoded_payload.data(), static_cast<int>(encoded_payload.size()), &subtitle);
      avsubtitle_free(&subtitle);
      if (encoded < 0) {
        result = encoded;
        failed_stream = static_cast<int>(source_index);
        failed_phase = "subtitle-encode";
        av_packet_unref(packet);
        break;
      }
      const auto pts = packet->pts;
      const auto dts = packet->dts;
      const auto duration = packet->duration;
      const auto flags = packet->flags;
      av_packet_unref(packet);
      if (av_new_packet(packet, encoded) < 0) {
        result = AVERROR(ENOMEM);
        failed_stream = static_cast<int>(source_index);
        failed_phase = "subtitle-packet-allocation";
        break;
      }
      std::memcpy(packet->data, encoded_payload.data(), static_cast<std::size_t>(encoded));
      packet->pts = pts;
      packet->dts = dts;
      packet->duration = duration;
      packet->flags = flags;
      packet->stream_index = static_cast<int>(source_index);
      ++converted_subtitle_packets;
    }
    if (private_mpegts_subtitle[source_index])
      expected_private_subtitle[source_index].add(packet->data, static_cast<std::size_t>(packet->size));
    if (restore_bitstream_audio[source_index])
      expected_bitstream_audio[source_index].add(packet->data, static_cast<std::size_t>(packet->size));
    if (restore_bluray_pcm[source_index]) {
      expected_pcm[source_index].add(packet->data, static_cast<std::size_t>(packet->size));
      if (!restore_pcm_bluray_packet(packet, input_stream->codecpar,
                                      restore_bluray_layout[source_index])) {
        result = AVERROR_INVALIDDATA;
        failed_stream = static_cast<int>(source_index);
        failed_packet_size = packet->size;
        failed_phase = "pcm-bluray-repack";
        av_packet_unref(packet);
        break;
      }
    }
    av_packet_rescale_ts(packet, input_stream->time_base, output_stream->time_base);
    const auto output_index = static_cast<std::size_t>(target_index);
    const auto step = std::max<std::int64_t>(packet->duration, 1);
    if (packet->dts == AV_NOPTS_VALUE) packet->dts = last_output_dts[output_index] == AV_NOPTS_VALUE ? 0 : last_output_dts[output_index] + step;
    if (last_output_dts[output_index] != AV_NOPTS_VALUE && packet->dts <= last_output_dts[output_index]) {
      const auto replacement_dts = last_output_dts[output_index] + step;
      if (packet->pts != AV_NOPTS_VALUE) packet->pts += replacement_dts - packet->dts;
      packet->dts = replacement_dts;
    }
    if (packet->pts == AV_NOPTS_VALUE) packet->pts = packet->dts;
    // Program-stream and several legacy muxers reject the B-frame boundary
    // timestamps that Matroska reconstructs when DTS briefly exceeds PTS.
    // FFmpeg's command-line frontend sanitises this condition before invoking
    // the muxer; do the equivalent here while leaving packet payloads intact.
    if (packet->dts != AV_NOPTS_VALUE && packet->pts < packet->dts)
      packet->pts = packet->dts;
    last_output_dts[output_index] = packet->dts;
    packet->stream_index = target_index;
    packet->pos = -1;
    failed_stream = target_index;
    failed_packet_size = packet->size;
    failed_packet_prefix = packet->size >= 4
        ? (static_cast<std::uint32_t>(packet->data[0]) << 24) |
          (static_cast<std::uint32_t>(packet->data[1]) << 16) |
          (static_cast<std::uint32_t>(packet->data[2]) << 8) |
          static_cast<std::uint32_t>(packet->data[3])
        : 0;
    failed_phase = "packet-write";
    result = av_interleaved_write_frame(destination, packet);
    av_packet_unref(packet);
    if (result < 0) break;
    ++input_packets[source_index];
  }
  if (result >= 0) result = av_write_trailer(destination);
  if (result < 0) {
    char error_text[AV_ERROR_MAX_STRING_SIZE]{};
    av_strerror(result, error_text, sizeof(error_text));
    cleanup();
    std::ostringstream reason;
    reason << "packet or trailer write failed: " << error_text
           << "; phase=" << failed_phase << "; stream=" << failed_stream
           << "; size=" << failed_packet_size << "; prefix=" << failed_packet_prefix;
    return unsupported_report(reason.str());
  }
  if (destination && destination->pb) avio_closep(&destination->pb);
  avformat_free_context(destination);
  destination = nullptr;
  avformat_close_input(&source);

  AVFormatContext* validation = nullptr;
  AVPacket* validation_packet = av_packet_alloc();
  if (!validation_packet || avformat_open_input(&validation, output.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(validation, nullptr) < 0) {
    av_packet_free(&validation_packet);
    avformat_close_input(&validation);
    cleanup();
    return unsupported_report("output validation open failed");
  }
  bool stream_match = true;
  std::vector<AVCodecID> actual_codecs;
  std::size_t validated_media_stream = 0;
  for (unsigned index = 0; index < validation->nb_streams; ++index) {
    const auto actual = validation->streams[index]->codecpar->codec_id;
    actual_codecs.push_back(actual);
    const bool chapter_data_track = validation->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_DATA &&
        actual == AV_CODEC_ID_BIN_DATA && validation->nb_chapters > 0;
    if (chapter_data_track) continue;
    if (validated_media_stream >= expected_codecs.size()) { stream_match = false; continue; }
    const auto expected = expected_codecs[validated_media_stream++];
    const bool dvd_lpcm_delivery = container == OutputContainer::mpeg_ps &&
        expected == AV_CODEC_ID_PCM_S16BE && actual == AV_CODEC_ID_PCM_DVD;
    const bool private_dvd_subtitle_delivery = transport_stream_container(container) &&
        expected == AV_CODEC_ID_DVD_SUBTITLE && actual == AV_CODEC_ID_BIN_DATA;
    stream_match = stream_match && (expected == actual || dvd_lpcm_delivery || private_dvd_subtitle_delivery);
  }
  stream_match = stream_match && validated_media_stream == expected_codecs.size();
  std::vector<std::size_t> output_packets(validation->nb_streams, 0);
  std::vector<std::int64_t> last_dts(validation->nb_streams, AV_NOPTS_VALUE);
  std::vector<PcmAudit> actual_pcm(input_packets.size());
  std::vector<PcmAudit> actual_bitstream_audio(input_packets.size());
  std::vector<PcmAudit> actual_private_subtitle(input_packets.size());
  std::size_t timing_violations = 0;
  std::size_t tolerated_mpeg12_boundary_dts_regressions = 0;
  while (av_read_frame(validation, validation_packet) >= 0) {
    const auto index = static_cast<std::size_t>(validation_packet->stream_index);
    ++output_packets[index];
    const auto source_index = index < source_for_output.size() ? source_for_output[index] : -1;
    if (source_index >= 0 && static_cast<std::size_t>(source_index) < restore_bluray_pcm.size() &&
        restore_bluray_pcm[static_cast<std::size_t>(source_index)]) {
      const auto normalized = matroska_normalized_payload(
          validation_packet, validation->streams[index]->codecpar);
      if (!normalized.empty()) actual_pcm[static_cast<std::size_t>(source_index)].add(normalized);
    }
    if (source_index >= 0 && static_cast<std::size_t>(source_index) < private_mpegts_subtitle.size() &&
        private_mpegts_subtitle[static_cast<std::size_t>(source_index)] &&
        validation->streams[index]->codecpar->codec_id == AV_CODEC_ID_BIN_DATA)
      actual_private_subtitle[static_cast<std::size_t>(source_index)].add(
           validation_packet->data, static_cast<std::size_t>(validation_packet->size));
    if (source_index >= 0 && static_cast<std::size_t>(source_index) < restore_bitstream_audio.size() &&
        restore_bitstream_audio[static_cast<std::size_t>(source_index)])
      actual_bitstream_audio[static_cast<std::size_t>(source_index)].add(
          validation_packet->data, static_cast<std::size_t>(validation_packet->size));
    if (validation_packet->dts != AV_NOPTS_VALUE && last_dts[index] != AV_NOPTS_VALUE &&
        validation_packet->dts < last_dts[index]) {
      const auto codec = validation->streams[index]->codecpar->codec_id;
      const bool mpeg12 = codec == AV_CODEC_ID_MPEG1VIDEO || codec == AV_CODEC_ID_MPEG2VIDEO;
      const auto regression = last_dts[index] - validation_packet->dts;
      const auto frame_duration = std::max<std::int64_t>(validation_packet->duration, 1);
      if (mpeg12 && (container == OutputContainer::mpeg_ps || regression <= frame_duration * 4))
        ++tolerated_mpeg12_boundary_dts_regressions;
      else
        ++timing_violations;
    }
    if (validation_packet->dts != AV_NOPTS_VALUE) last_dts[index] = validation_packet->dts;
    av_packet_unref(validation_packet);
  }
  std::size_t packet_count_mismatches = 0;
  std::size_t allowed_packet_rechunks = 0;
  std::size_t pcm_sample_mismatches = 0;
  std::size_t bitstream_audio_payload_mismatches = 0;
  std::size_t private_subtitle_payload_mismatches = 0;
  for (std::size_t output_index = 0; output_index < source_for_output.size(); ++output_index) {
    const int source_index = source_for_output[output_index];
    if (source_index < 0 || static_cast<std::size_t>(source_index) >= input_packets.size() ||
        output_index >= output_packets.size()) continue;
    if (input_packets[static_cast<std::size_t>(source_index)] != output_packets[output_index]) {
      const auto codec = output_index < expected_codecs.size() ? expected_codecs[output_index] : AV_CODEC_ID_NONE;
      const bool pcm = codec == AV_CODEC_ID_PCM_S16LE || codec == AV_CODEC_ID_PCM_S24LE ||
                       codec == AV_CODEC_ID_PCM_S16BE || codec == AV_CODEC_ID_PCM_S24BE;
      const bool mxf_pcm_rechunk = container == OutputContainer::mxf && output_packets[output_index] > 0 &&
           pcm;
      const bool program_stream_lpcm_rechunk = container == OutputContainer::mpeg_ps &&
          output_packets[output_index] > 0 && pcm;
      const bool subtitle_conversion_rechunk = subtitle_decoders[static_cast<std::size_t>(source_index)] != nullptr &&
          output_packets[output_index] > 0;
      if (mxf_pcm_rechunk || program_stream_lpcm_rechunk || subtitle_conversion_rechunk) ++allowed_packet_rechunks;
      else ++packet_count_mismatches;
    }
  }
  for (std::size_t index = 0; index < restore_bluray_pcm.size() && index < actual_pcm.size(); ++index) {
    if (!restore_bluray_pcm[index]) continue;
    const auto& expected = expected_pcm[index];
    const auto& actual = actual_pcm[index];
    if (expected.packets != actual.packets || expected.bytes != actual.bytes ||
        expected.hash != actual.hash) ++pcm_sample_mismatches;
  }
  for (std::size_t index = 0; index < restore_bitstream_audio.size() &&
       index < actual_bitstream_audio.size(); ++index) {
    if (!restore_bitstream_audio[index]) continue;
    const auto& expected = expected_bitstream_audio[index];
    const auto& actual = actual_bitstream_audio[index];
    if (expected.packets != actual.packets || expected.bytes != actual.bytes ||
        expected.hash != actual.hash) ++bitstream_audio_payload_mismatches;
  }
  for (std::size_t index = 0; index < private_mpegts_subtitle.size() && index < actual_private_subtitle.size(); ++index) {
    if (!private_mpegts_subtitle[index]) continue;
    const auto& expected = expected_private_subtitle[index];
    const auto& actual = actual_private_subtitle[index];
    if (expected.packets != actual.packets || expected.bytes != actual.bytes ||
        expected.hash != actual.hash) ++private_subtitle_payload_mismatches;
  }
  const auto output_streams = validation->nb_streams;
  const auto output_chapters = validation->nb_chapters;
  const auto output_programs = validation->nb_programs;
  int service_id = -1;
  int pmt_pid = -1;
  int pcr_pid = -1;
  std::size_t metadata_matches = 0;
  for (const auto& [key, value] : metadata) {
    if (key.empty()) continue;
    const auto* entry = av_dict_get(validation->metadata, key.c_str(), nullptr, 0);
    if ((value.empty() && entry == nullptr) ||
        (!value.empty() && entry != nullptr && value == entry->value))
      ++metadata_matches;
  }
  if (transport_stream_container(container) && validation->nb_programs > 0) {
    service_id = validation->programs[0]->id;
    pmt_pid = validation->programs[0]->pmt_pid;
    pcr_pid = validation->programs[0]->pcr_pid;
  }
  av_packet_free(&validation_packet);
  avformat_close_input(&validation);
  const bool valid = stream_match && packet_count_mismatches == 0 && pcm_sample_mismatches == 0 &&
                     bitstream_audio_payload_mismatches == 0 &&
                     private_subtitle_payload_mismatches == 0 && timing_violations == 0 &&
                     metadata_matches == metadata.size();
  completed = valid;
  cleanup();
  std::ostringstream report;
  report << "{\"schema\":\"container-remux-v1\",\"status\":\""
         << (valid ? "PASSED" : "FAILED VALIDATION") << "\",\"container\":\""
         << output_container_name(container) << "\",\"smart_copy\":true,\"streams\":"
         << output_streams << ",\"chapters\":" << output_chapters
         << ",\"chapter_policy\":\""
         << (copy_chapters_supported(container) ? "preserve" : "container-does-not-support")
         << "\",\"packet_count_mismatches\":" << packet_count_mismatches
         << ",\"allowed_packet_rechunks\":" << allowed_packet_rechunks
         << ",\"pcm_sample_mismatches\":" << pcm_sample_mismatches
         << ",\"bitstream_audio_payload_mismatches\":" << bitstream_audio_payload_mismatches
         << ",\"private_subtitle_payload_mismatches\":" << private_subtitle_payload_mismatches
         << ",\"subtitle_streams_converted\":" << converted_subtitle_streams
         << ",\"subtitle_packets_converted\":" << converted_subtitle_packets
         << ",\"attachments_dropped\":" << dropped_attachments
          << ",\"timing_violations\":" << timing_violations
          << ",\"tolerated_mpeg12_boundary_dts_regressions\":" << tolerated_mpeg12_boundary_dts_regressions
          << ",\"metadata_requested\":" << metadata.size()
          << ",\"metadata_matched\":" << metadata_matches
         << ",\"programs\":" << output_programs << ",\"service_id\":" << service_id
         << ",\"pmt_pid\":" << pmt_pid << ",\"pcr_pid\":" << pcr_pid;
  report << ",\"stream_match\":" << (stream_match ? "true" : "false") << ",\"expected_codecs\":[";
  for (std::size_t index = 0; index < expected_codecs.size(); ++index) { if (index) report << ','; report << '\"' << codec_name(expected_codecs[index]) << '\"'; }
  report << "],\"actual_codecs\":[";
  for (std::size_t index = 0; index < actual_codecs.size(); ++index) { if (index) report << ','; report << '\"' << codec_name(actual_codecs[index]) << '\"'; }
  report << ']';
  if (transport_stream_container(container))
    report << ",\"transport_stream_id\":" << transport_stream.transport_stream_id
           << ",\"original_network_id\":" << transport_stream.original_network_id
           << ",\"elementary_start_pid\":" << transport_stream.elementary_start_pid
           << ",\"mux_rate\":" << transport_stream.mux_rate;
  report << '}';
  return report.str();
#endif
}

std::string multiplex_elementary_streams(const std::vector<std::string>& inputs,
                                         const std::string& output,
                                         const MpegTsMuxOptions& transport_stream) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)inputs;
  (void)output;
  (void)transport_stream;
  return unsupported_report("FFmpeg support is not enabled");
#else
  const auto container = output_container_for_path(output);
  const auto* format = muxer_name(container);
  if (!format) return unsupported_report("unsupported output extension");
  if (inputs.size() < 2) return unsupported_report("at least two input streams are required");

  std::vector<AVFormatContext*> sources(inputs.size(), nullptr);
  std::vector<AVPacket*> packets(inputs.size(), nullptr);
  std::vector<bool> buffered(inputs.size(), false);
  std::vector<bool> exhausted(inputs.size(), false);
  std::vector<std::vector<int>> stream_maps(inputs.size());
  std::vector<std::vector<std::int64_t>> generated_timestamps(inputs.size());
  std::vector<AVCodecID> expected_codecs;
  std::vector<std::size_t> input_packets;
  AVFormatContext* destination = nullptr;
  bool completed = false;
  auto cleanup = [&] {
    for (auto*& packet : packets) av_packet_free(&packet);
    for (auto*& source : sources) avformat_close_input(&source);
    if (destination && destination->pb) avio_closep(&destination->pb);
    avformat_free_context(destination);
    destination = nullptr;
    if (!completed) remove_file(output);
  };

  for (std::size_t input_index = 0; input_index < inputs.size(); ++input_index) {
    std::error_code equivalent_error;
    if (std::filesystem::equivalent(inputs[input_index], output, equivalent_error) && !equivalent_error) {
      cleanup();
      return unsupported_report("an input and output path are identical");
    }
    packets[input_index] = av_packet_alloc();
    if (!packets[input_index] ||
        avformat_open_input(&sources[input_index], inputs[input_index].c_str(), nullptr, nullptr) < 0 ||
        avformat_find_stream_info(sources[input_index], nullptr) < 0) {
      cleanup();
      return unsupported_report("input open failed");
    }
    stream_maps[input_index].assign(sources[input_index]->nb_streams, -1);
    generated_timestamps[input_index].assign(sources[input_index]->nb_streams, 0);
  }
  if (avformat_alloc_output_context2(&destination, nullptr, format, output.c_str()) < 0 || !destination) {
    cleanup();
    return unsupported_report("output context failed");
  }

  for (std::size_t input_index = 0; input_index < sources.size(); ++input_index) {
    auto* source = sources[input_index];
    for (unsigned stream_index = 0; stream_index < source->nb_streams; ++stream_index) {
      const auto* input_stream = source->streams[stream_index];
      const auto media_type = input_stream->codecpar->codec_type;
      if (media_type != AVMEDIA_TYPE_VIDEO && media_type != AVMEDIA_TYPE_AUDIO &&
          media_type != AVMEDIA_TYPE_SUBTITLE)
        continue;
      auto* output_stream = avformat_new_stream(destination, nullptr);
      if (!output_stream ||
          avcodec_parameters_copy(output_stream->codecpar, input_stream->codecpar) < 0) {
        cleanup();
        return unsupported_report("stream setup failed");
      }
      output_stream->codecpar->codec_tag = 0;
      output_stream->time_base = input_stream->time_base;
      copy_stream_attributes(output_stream, input_stream);
      stream_maps[input_index][stream_index] = output_stream->index;
      expected_codecs.push_back(input_stream->codecpar->codec_id);
      input_packets.push_back(0);
    }
  }
  if (expected_codecs.size() < 2) {
    cleanup();
    return unsupported_report("inputs did not expose at least two media streams");
  }
  if (transport_stream_container(container)) {
    av_dict_set(&destination->metadata, "service_name", transport_stream.service_name.c_str(), 0);
    av_dict_set(&destination->metadata, "service_provider", transport_stream.service_provider.c_str(), 0);
  }
  if (!(destination->oformat->flags & AVFMT_NOFILE) &&
      avio_open(&destination->pb, output.c_str(), AVIO_FLAG_WRITE) < 0) {
    cleanup();
    return unsupported_report("output open failed");
  }
  AVDictionary* options = nullptr;
  if (container == OutputContainer::mp4 || container == OutputContainer::mov)
    av_dict_set(&options, "movflags", "+faststart", 0);
  if (transport_stream_container(container)) {
    const auto set_integer = [&options](const char* name, std::int64_t value) {
      av_dict_set(&options, name, std::to_string(value).c_str(), 0);
    };
    set_integer("mpegts_transport_stream_id", transport_stream.transport_stream_id);
    set_integer("mpegts_original_network_id", transport_stream.original_network_id);
    set_integer("mpegts_service_id", transport_stream.service_id);
    set_integer("mpegts_pmt_start_pid", transport_stream.pmt_start_pid);
    set_integer("mpegts_start_pid", transport_stream.elementary_start_pid);
    set_integer("mpegts_m2ts_mode",
                transport_stream.m2ts_mode || lower_extension(output) == ".m2ts" ? 1 : 0);
  }
  const int header_result = avformat_write_header(destination, &options);
  av_dict_free(&options);
  if (header_result < 0) {
    cleanup();
    return unsupported_report(
        "header write failed; one or more streams are incompatible with the selected container");
  }

  const auto generated_duration = [](const AVFormatContext* source, const AVStream* stream,
                                     const AVPacket* packet) {
    if (packet->duration > 0) return packet->duration;
    if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
      const auto rate = av_guess_frame_rate(const_cast<AVFormatContext*>(source),
                                            const_cast<AVStream*>(stream), nullptr);
      if (rate.num > 0 && rate.den > 0)
        return std::max<std::int64_t>(1, av_rescale_q(1, av_inv_q(rate), stream->time_base));
    }
    if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO && stream->codecpar->sample_rate > 0) {
      int samples = 1024;
      if (stream->codecpar->codec_id == AV_CODEC_ID_MP2 ||
          stream->codecpar->codec_id == AV_CODEC_ID_MP3)
        samples = 1152;
      else if (stream->codecpar->codec_id == AV_CODEC_ID_AC3 ||
               stream->codecpar->codec_id == AV_CODEC_ID_EAC3)
        samples = 1536;
      return std::max<std::int64_t>(1, av_rescale_q(samples,
                                                    AVRational{1, stream->codecpar->sample_rate},
                                                    stream->time_base));
    }
    return std::int64_t{1};
  };
  const auto read_next = [&](std::size_t input_index) {
    auto* source = sources[input_index];
    auto* packet = packets[input_index];
    while (av_read_frame(source, packet) >= 0) {
      const auto stream_index = static_cast<unsigned>(packet->stream_index);
      if (stream_index >= stream_maps[input_index].size() ||
          stream_maps[input_index][stream_index] < 0) {
        av_packet_unref(packet);
        continue;
      }
      const auto* stream = source->streams[stream_index];
      auto& next = generated_timestamps[input_index][stream_index];
      const auto duration = generated_duration(source, stream, packet);
      if (packet->dts == AV_NOPTS_VALUE && packet->pts == AV_NOPTS_VALUE)
        packet->dts = packet->pts = next;
      else if (packet->dts == AV_NOPTS_VALUE)
        packet->dts = packet->pts;
      else if (packet->pts == AV_NOPTS_VALUE)
        packet->pts = packet->dts;
      if (packet->duration <= 0) packet->duration = duration;
      next = std::max(next, packet->dts + std::max<std::int64_t>(packet->duration, 1));
      buffered[input_index] = true;
      return;
    }
    exhausted[input_index] = true;
    buffered[input_index] = false;
  };

  for (std::size_t index = 0; index < sources.size(); ++index) read_next(index);
  std::size_t written_packets = 0;
  int write_result = 0;
  while (write_result >= 0) {
    std::size_t selected = inputs.size();
    std::int64_t selected_time = 0;
    for (std::size_t index = 0; index < inputs.size(); ++index) {
      if (!buffered[index]) continue;
      const auto* packet = packets[index];
      const auto* stream = sources[index]->streams[packet->stream_index];
      const auto timestamp = packet->dts != AV_NOPTS_VALUE ? packet->dts : packet->pts;
      const auto common_time = av_rescale_q(timestamp, stream->time_base, AV_TIME_BASE_Q);
      if (selected == inputs.size() || common_time < selected_time) {
        selected = index;
        selected_time = common_time;
      }
    }
    if (selected == inputs.size()) break;
    auto* packet = packets[selected];
    const auto source_stream_index = static_cast<unsigned>(packet->stream_index);
    const auto target_stream_index = stream_maps[selected][source_stream_index];
    const auto* input_stream = sources[selected]->streams[source_stream_index];
    auto* output_stream = destination->streams[target_stream_index];
    av_packet_rescale_ts(packet, input_stream->time_base, output_stream->time_base);
    packet->stream_index = target_stream_index;
    packet->pos = -1;
    write_result = av_interleaved_write_frame(destination, packet);
    if (write_result >= 0) {
      ++input_packets[static_cast<std::size_t>(target_stream_index)];
      ++written_packets;
    }
    av_packet_unref(packet);
    buffered[selected] = false;
    if (!exhausted[selected]) read_next(selected);
  }
  if (write_result >= 0) write_result = av_write_trailer(destination);
  if (write_result < 0 || written_packets == 0) {
    cleanup();
    return unsupported_report("packet interleave or trailer write failed");
  }
  if (destination->pb) avio_closep(&destination->pb);
  avformat_free_context(destination);
  destination = nullptr;
  for (auto*& source : sources) avformat_close_input(&source);

  AVFormatContext* validation = nullptr;
  AVPacket* validation_packet = av_packet_alloc();
  if (!validation_packet || avformat_open_input(&validation, output.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(validation, nullptr) < 0) {
    av_packet_free(&validation_packet);
    avformat_close_input(&validation);
    cleanup();
    return unsupported_report("output validation open failed");
  }
  bool stream_match = validation->nb_streams == expected_codecs.size();
  for (unsigned index = 0; stream_match && index < validation->nb_streams; ++index)
    stream_match = validation->streams[index]->codecpar->codec_id == expected_codecs[index];
  std::vector<std::size_t> output_packets(validation->nb_streams, 0);
  std::vector<std::int64_t> last_dts(validation->nb_streams, AV_NOPTS_VALUE);
  std::size_t timing_violations = 0;
  while (av_read_frame(validation, validation_packet) >= 0) {
    const auto index = static_cast<std::size_t>(validation_packet->stream_index);
    ++output_packets[index];
    if (validation_packet->dts != AV_NOPTS_VALUE && last_dts[index] != AV_NOPTS_VALUE &&
        validation_packet->dts < last_dts[index])
      ++timing_violations;
    if (validation_packet->dts != AV_NOPTS_VALUE) last_dts[index] = validation_packet->dts;
    av_packet_unref(validation_packet);
  }
  bool packet_match = input_packets.size() == output_packets.size();
  for (std::size_t index = 0; packet_match && index < input_packets.size(); ++index)
    packet_match = input_packets[index] > 0 && output_packets[index] == input_packets[index];
  const auto output_streams = validation->nb_streams;
  av_packet_free(&validation_packet);
  avformat_close_input(&validation);
  const bool valid = stream_match && packet_match && timing_violations == 0;
  completed = valid;
  cleanup();
  std::ostringstream report;
  report << "{\"schema\":\"elementary-multiplex-v1\",\"status\":\""
         << (valid ? "PASSED" : "FAILED VALIDATION") << "\",\"container\":\""
         << output_container_name(container) << "\",\"packet_copy\":true,\"inputs\":"
         << inputs.size() << ",\"streams\":" << output_streams << ",\"packets\":"
         << written_packets << ",\"timing_violations\":" << timing_violations << '}';
  return report.str();
#endif
}

std::string demultiplex_elementary_streams(const std::string& input,
                                           const std::string& output) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)input;
  (void)output;
  return unsupported_report("FFmpeg support is not enabled");
#else
  const auto requested_extension = lower_extension(output);
  if (requested_extension == ".s337m" || requested_extension == ".dolbye")
    return restore_dolby_e_s337m(input, output);
  struct ElementaryTarget {
    unsigned source_stream{};
    AVCodecID codec{AV_CODEC_ID_NONE};
    AVFormatContext* context{};
    AVBSFContext* filter{};
    std::filesystem::path staged;
    std::filesystem::path final;
    std::size_t packets{};
  };
  const auto format_for_codec = [](AVCodecID codec) -> std::pair<const char*, const char*> {
    switch (codec) {
      case AV_CODEC_ID_MPEG2VIDEO: return {"mpeg2video", ".m2v"};
      case AV_CODEC_ID_H264: return {"h264", ".h264"};
      case AV_CODEC_ID_HEVC: return {"hevc", ".hevc"};
      case AV_CODEC_ID_AV1: return {"obu", ".obu"};
      case AV_CODEC_ID_VP9: return {"ivf", ".ivf"};
      case AV_CODEC_ID_AAC: return {"adts", ".aac"};
      case AV_CODEC_ID_AC3: return {"ac3", ".ac3"};
      case AV_CODEC_ID_EAC3: return {"eac3", ".eac3"};
      case AV_CODEC_ID_MP2: return {"mp2", ".mp2"};
      case AV_CODEC_ID_MP3: return {"mp3", ".mp3"};
      case AV_CODEC_ID_FLAC: return {"flac", ".flac"};
      case AV_CODEC_ID_OPUS: return {"opus", ".opus"};
      case AV_CODEC_ID_PCM_S16LE:
      case AV_CODEC_ID_PCM_S24LE: return {"wav", ".wav"};
      case AV_CODEC_ID_DTS: return {"dts", ".dts"};
      case AV_CODEC_ID_TRUEHD: return {"truehd", ".thd"};
      default: return {nullptr, nullptr};
    }
  };
  const auto bsf_for_codec = [](AVCodecID codec) -> const char* {
    if (codec == AV_CODEC_ID_H264) return "h264_mp4toannexb";
    if (codec == AV_CODEC_ID_HEVC) return "hevc_mp4toannexb";
    return nullptr;
  };

  AVFormatContext* source = nullptr;
  AVPacket* packet = av_packet_alloc();
  AVPacket* filtered = av_packet_alloc();
  std::vector<ElementaryTarget> targets;
  bool completed = false;
  const auto cleanup = [&] {
    av_packet_free(&packet);
    av_packet_free(&filtered);
    for (auto& target : targets) {
      av_bsf_free(&target.filter);
      if (target.context && target.context->pb) avio_closep(&target.context->pb);
      avformat_free_context(target.context);
      target.context = nullptr;
      if (!completed) remove_file(target.staged);
    }
    avformat_close_input(&source);
  };
  if (!packet || !filtered || avformat_open_input(&source, input.c_str(), nullptr, nullptr) < 0 ||
      avformat_find_stream_info(source, nullptr) < 0) {
    cleanup();
    return unsupported_report("input open failed");
  }

  const std::filesystem::path requested(output);
  const auto parent = requested.parent_path();
  const auto stem = requested.stem().string();
  std::size_t video_number = 0;
  std::size_t audio_number = 0;
  std::size_t skipped = 0;
  bool primary_assigned = false;
  for (unsigned index = 0; index < source->nb_streams; ++index) {
    const auto* stream = source->streams[index];
    const auto media_type = stream->codecpar->codec_type;
    if (media_type != AVMEDIA_TYPE_VIDEO && media_type != AVMEDIA_TYPE_AUDIO) {
      ++skipped;
      continue;
    }
    const auto [format, extension] = format_for_codec(stream->codecpar->codec_id);
    if (!format || !extension) {
      ++skipped;
      continue;
    }
    const bool video = media_type == AVMEDIA_TYPE_VIDEO;
    auto& number = video ? video_number : audio_number;
    ++number;
    std::filesystem::path final;
    if (!primary_assigned && (video || video_number == 0)) {
      final = requested;
      primary_assigned = true;
    } else {
      const auto label = video ? "-video" : "-audio";
      final = parent / (stem + label + std::to_string(number) + extension);
    }
    const auto nonce = std::chrono::high_resolution_clock::now().time_since_epoch().count();
    const auto staged = final.parent_path() /
                        (final.stem().string() + ".smartcut-elementary-" +
                         std::to_string(nonce) + final.extension().string());
    AVFormatContext* destination = nullptr;
    if (avformat_alloc_output_context2(&destination, nullptr, format, staged.string().c_str()) < 0 ||
        !destination) {
      cleanup();
      return unsupported_report("elementary output context failed for codec " + codec_name(stream->codecpar->codec_id));
    }
    ElementaryTarget target;
    target.source_stream = index;
    target.codec = stream->codecpar->codec_id;
    target.context = destination;
    target.staged = staged;
    target.final = final;
    if (const auto* filter_name = bsf_for_codec(target.codec)) {
      const auto* filter = av_bsf_get_by_name(filter_name);
      if (!filter || av_bsf_alloc(filter, &target.filter) < 0 ||
          avcodec_parameters_copy(target.filter->par_in, stream->codecpar) < 0) {
        targets.push_back(std::move(target));
        cleanup();
        return unsupported_report("elementary bitstream-filter setup failed");
      }
      target.filter->time_base_in = stream->time_base;
      if (av_bsf_init(target.filter) < 0) {
        targets.push_back(std::move(target));
        cleanup();
        return unsupported_report("elementary bitstream-filter initialization failed");
      }
    }
    auto* output_stream = avformat_new_stream(destination, nullptr);
    const auto* parameters = target.filter ? target.filter->par_out : stream->codecpar;
    if (!output_stream || avcodec_parameters_copy(output_stream->codecpar, parameters) < 0) {
      targets.push_back(std::move(target));
      cleanup();
      return unsupported_report("elementary stream setup failed");
    }
    output_stream->codecpar->codec_tag = 0;
    output_stream->time_base = stream->time_base;
    if (!(destination->oformat->flags & AVFMT_NOFILE) &&
        avio_open(&destination->pb, staged.string().c_str(), AVIO_FLAG_WRITE) < 0) {
      targets.push_back(std::move(target));
      cleanup();
      return unsupported_report("elementary output open failed");
    }
    if (avformat_write_header(destination, nullptr) < 0) {
      targets.push_back(std::move(target));
      cleanup();
      return unsupported_report("elementary header write failed");
    }
    targets.push_back(std::move(target));
  }
  if (targets.empty()) {
    cleanup();
    return unsupported_report("no supported audio or video streams");
  }

  std::vector<int> target_for_stream(source->nb_streams, -1);
  for (std::size_t index = 0; index < targets.size(); ++index)
    target_for_stream[targets[index].source_stream] = static_cast<int>(index);
  int write_result = 0;
  const auto write_filtered = [&](ElementaryTarget& target, AVPacket* value) {
    const auto* source_stream = source->streams[target.source_stream];
    auto* output_stream = target.context->streams[0];
    av_packet_rescale_ts(value, source_stream->time_base, output_stream->time_base);
    value->stream_index = 0;
    value->pos = -1;
    const auto result = av_interleaved_write_frame(target.context, value);
    if (result >= 0) ++target.packets;
    return result;
  };
  while (write_result >= 0 && av_read_frame(source, packet) >= 0) {
    const auto target_index = packet->stream_index >= 0 &&
                                      static_cast<unsigned>(packet->stream_index) < source->nb_streams
                                  ? target_for_stream[static_cast<std::size_t>(packet->stream_index)]
                                  : -1;
    if (target_index >= 0) {
      auto& target = targets[static_cast<std::size_t>(target_index)];
      if (target.filter) {
        write_result = av_bsf_send_packet(target.filter, packet);
        while (write_result >= 0 && av_bsf_receive_packet(target.filter, filtered) >= 0) {
          write_result = write_filtered(target, filtered);
          av_packet_unref(filtered);
        }
        if (write_result == AVERROR(EAGAIN) || write_result == AVERROR_EOF) write_result = 0;
      } else {
        write_result = write_filtered(target, packet);
      }
    }
    av_packet_unref(packet);
  }
  for (auto& target : targets) {
    if (write_result < 0 || !target.filter) continue;
    int result = av_bsf_send_packet(target.filter, nullptr);
    while (result >= 0 && av_bsf_receive_packet(target.filter, filtered) >= 0) {
      result = write_filtered(target, filtered);
      av_packet_unref(filtered);
    }
    if (result != AVERROR_EOF && result != AVERROR(EAGAIN) && result < 0) write_result = result;
  }
  for (auto& target : targets)
    if (write_result >= 0 && av_write_trailer(target.context) < 0) write_result = -1;
  for (auto& target : targets) {
    if (target.context && target.context->pb) avio_closep(&target.context->pb);
    avformat_free_context(target.context);
    target.context = nullptr;
  }
  avformat_close_input(&source);
  if (write_result < 0) {
    cleanup();
    return unsupported_report("elementary packet or trailer write failed");
  }

  std::size_t validated_packets = 0;
  bool valid = true;
  for (const auto& target : targets) {
    AVFormatContext* validation = nullptr;
    AVPacket* validation_packet = av_packet_alloc();
    if (!validation_packet ||
        avformat_open_input(&validation, target.staged.string().c_str(), nullptr, nullptr) < 0 ||
        avformat_find_stream_info(validation, nullptr) < 0) {
      valid = false;
    } else {
      bool codec_match = false;
      for (unsigned index = 0; index < validation->nb_streams; ++index)
        codec_match = codec_match || validation->streams[index]->codecpar->codec_id == target.codec;
      std::size_t packets_read = 0;
      while (av_read_frame(validation, validation_packet) >= 0) {
        ++packets_read;
        av_packet_unref(validation_packet);
      }
      validated_packets += packets_read;
      valid = valid && codec_match && packets_read > 0 && target.packets > 0;
    }
    av_packet_free(&validation_packet);
    avformat_close_input(&validation);
  }
  if (valid) {
    for (const auto& target : targets) {
      std::error_code error;
      remove_file(target.final);
      std::filesystem::rename(target.staged, target.final, error);
      if (error) { valid = false; break; }
    }
  }
  completed = valid;
  cleanup();
  if (!valid) {
    for (const auto& target : targets) remove_file(target.final);
  }
  std::ostringstream report;
  report << "{\"schema\":\"elementary-demultiplex-v1\",\"status\":\""
         << (valid ? "PASSED" : "FAILED VALIDATION") << "\",\"packet_copy\":true,\"outputs\":"
         << targets.size() << ",\"skipped_streams\":" << skipped << ",\"packets\":"
         << validated_packets << '}';
  return report.str();
#endif
}

std::string export_media(const std::string& input, const std::string& output,
                         const MpegTsMuxOptions& transport_stream) {
  return export_through_matroska(input, output,
                                 [](const std::string& source, const std::string& destination) {
                                   return export_mkv(source, destination);
                                 }, transport_stream);
}

std::string export_media_selected_streams(const std::string& input, const std::string& output,
                                          const std::vector<std::size_t>& streams,
                                          const MpegTsMuxOptions& transport_stream) {
  return export_through_matroska(input, output,
                                 [&streams](const std::string& source, const std::string& destination) {
                                   return export_mkv_selected_streams(source, destination, streams);
                                 }, transport_stream);
}

}  // namespace smartcut::media
