#include "media/stream_probe.hpp"

#include <sstream>
#include <string>
#include <vector>
#include <chrono>

#include "core/source_identity.hpp"
#include "core/json.hpp"
#include "media/pcm_normalize.hpp"

#ifdef SMARTCUT_HAS_FFMPEG
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/dovi_meta.h>
#include <libavutil/pixdesc.h>
}
#include "media/ffmpeg_raii.hpp"
#endif

namespace smartcut::media {
std::string dovi_config_report(const std::filesystem::path& path) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; return "{\"schema\":\"dovi-config-v1\",\"status\":\"FAILED\",\"present\":false}";
#else
  auto format = ffmpeg::open_input(path); if (!format || avformat_find_stream_info(format.get(), nullptr) < 0) return "{\"schema\":\"dovi-config-v1\",\"status\":\"FAILED\",\"present\":false}";
  for (unsigned i = 0; i < format->nb_streams; ++i) {
    const auto* parameters = format->streams[i]->codecpar; if (parameters->codec_type != AVMEDIA_TYPE_VIDEO) continue;
    const auto* side = av_packet_side_data_get(parameters->coded_side_data, parameters->nb_coded_side_data, AV_PKT_DATA_DOVI_CONF);
    if (!side || side->size < sizeof(AVDOVIDecoderConfigurationRecord)) break;
    const auto* config = reinterpret_cast<const AVDOVIDecoderConfigurationRecord*>(side->data); std::ostringstream output;
    output << "{\"schema\":\"dovi-config-v1\",\"status\":\"PASSED\",\"present\":true,\"profile\":" << static_cast<unsigned>(config->dv_profile) << ",\"level\":" << static_cast<unsigned>(config->dv_level) << ",\"rpu_present\":" << (config->rpu_present_flag ? "true" : "false") << ",\"el_present\":" << (config->el_present_flag ? "true" : "false") << ",\"bl_present\":" << (config->bl_present_flag ? "true" : "false") << ",\"bl_compatibility_id\":" << static_cast<unsigned>(config->dv_bl_signal_compatibility_id) << '}'; return output.str();
  }
  return "{\"schema\":\"dovi-config-v1\",\"status\":\"FAILED\",\"present\":false}";
#endif
}

#ifdef SMARTCUT_HAS_FFMPEG
namespace {
struct ProbeDeadline { std::chrono::steady_clock::time_point expires; };
int interrupt_expired(void* opaque) {
  return std::chrono::steady_clock::now() >= static_cast<ProbeDeadline*>(opaque)->expires;
}
ffmpeg::FormatInput open_probe_input(const std::filesystem::path& path, ProbeDeadline& deadline) {
  AVFormatContext* raw = avformat_alloc_context();
  if (!raw) return {};
  raw->interrupt_callback = {interrupt_expired, &deadline};
  if (avformat_open_input(&raw, path.string().c_str(), nullptr, nullptr) < 0) { avformat_free_context(raw); return {}; }
  return ffmpeg::FormatInput(raw);
}
std::string json_escape(const char* value) {
  return value == nullptr ? std::string{} : smartcut::core::json_escape(value);
}

void write_metadata(std::ostringstream& output, const AVDictionary* metadata) {
  output << '[';
  const AVDictionaryEntry* entry = nullptr;
  bool first = true;
  while ((entry = av_dict_get(metadata, "", entry, AV_DICT_IGNORE_SUFFIX)) != nullptr) {
    if (!first) output << ',';
    first = false;
    output << "{\"key\":\"" << json_escape(entry->key) << "\",\"value\":\""
           << json_escape(entry->value) << "\"}";
  }
  output << ']';
}

const AVPacketSideData* find_side_data(const AVCodecParameters* codec, AVPacketSideDataType type) {
  for (int i = 0; i < codec->nb_coded_side_data; ++i)
    if (codec->coded_side_data[i].type == type) return &codec->coded_side_data[i];
  return nullptr;
}
}  // namespace
#endif

std::string probe_container(const std::filesystem::path& path) {
  const auto identity = core::SourceIdentity::from_file(path);
#ifndef SMARTCUT_HAS_FFMPEG
  return "{\"error\":\"FFmpeg support is not enabled; configure with SMARTCUT_MEDIA_ROOT\",\"source_identity\":" + identity.to_json() + "}";
#else
  ProbeDeadline deadline{std::chrono::steady_clock::now() + std::chrono::seconds(5)};
  auto format = open_probe_input(path, deadline);
  if (!format) return "{\"error\":\"FFmpeg could not open input\"}";
  if (avformat_find_stream_info(format.get(), nullptr) < 0) return "{\"error\":\"FFmpeg could not read stream info\"}";
  std::ostringstream output;
  output << "{\"schema\":\"probe-v1\",\"source_identity\":" << identity.to_json()
         << ",\"format\":\"" << json_escape(format->iformat == nullptr ? "" : format->iformat->name)
         << "\",\"format_long_name\":\"" << json_escape(format->iformat == nullptr ? "" : format->iformat->long_name)
         << "\",\"start_time\":" << format->start_time << ",\"duration\":" << format->duration
         << ",\"bit_rate\":" << format->bit_rate << ",\"stream_count\":" << format->nb_streams
         << ",\"chapter_count\":" << format->nb_chapters << ",\"metadata\":";
  write_metadata(output, format->metadata);
  output << ",\"streams\":[";
  for (unsigned i = 0; i < format->nb_streams; ++i) {
    if (i != 0) output << ',';
    const auto* stream = format->streams[i];
    const auto* codec = stream->codecpar;
    const char* codec_name = avcodec_get_name(codec->codec_id);
    const auto* descriptor = avcodec_descriptor_get(codec->codec_id);
    const char* pixel_name = codec->format >= 0 ? av_get_pix_fmt_name(static_cast<AVPixelFormat>(codec->format)) : "";
    char channel_layout[256]{};
    if (codec->ch_layout.nb_channels > 0) av_channel_layout_describe(&codec->ch_layout, channel_layout, sizeof(channel_layout));
    const auto* dovi = find_side_data(codec, AV_PKT_DATA_DOVI_CONF);
    const auto* dovi_config = dovi != nullptr && dovi->size >= sizeof(AVDOVIDecoderConfigurationRecord)
        ? reinterpret_cast<const AVDOVIDecoderConfigurationRecord*>(dovi->data) : nullptr;
    AVCodecID bitstream_origin = AV_CODEC_ID_NONE;
    const bool bitstream_bridge = bitstream_audio_origin(stream, &bitstream_origin);
    const bool dolby_e = codec->codec_id == AV_CODEC_ID_DOLBY_E ||
                         (bitstream_bridge && bitstream_origin == AV_CODEC_ID_DOLBY_E);
    const bool s302m = codec->codec_id == AV_CODEC_ID_S302M ||
                       (bitstream_bridge && bitstream_origin == AV_CODEC_ID_S302M);
    output << "{\"index\":" << i << ",\"type\":" << static_cast<int>(codec->codec_type)
           << ",\"codec\":\"" << (codec_name == nullptr ? "" : codec_name)
           << "\",\"codec_long_name\":\"" << json_escape(descriptor == nullptr ? "" : descriptor->long_name)
           << "\",\"profile\":" << codec->profile << ",\"level\":" << codec->level
           << ",\"codec_tag\":" << codec->codec_tag << ",\"bit_rate\":" << codec->bit_rate
           << ",\"extradata_size\":" << codec->extradata_size << ",\"disposition\":" << stream->disposition
           << ",\"width\":" << codec->width << ",\"height\":" << codec->height
           << ",\"pixel_format\":\"" << (pixel_name == nullptr ? "" : pixel_name)
           << "\",\"time_base_num\":" << stream->time_base.num << ",\"time_base_den\":" << stream->time_base.den
           << ",\"frame_rate_num\":" << stream->r_frame_rate.num << ",\"frame_rate_den\":" << stream->r_frame_rate.den
           << ",\"avg_frame_rate_num\":" << stream->avg_frame_rate.num << ",\"avg_frame_rate_den\":" << stream->avg_frame_rate.den
           << ",\"sar_num\":" << codec->sample_aspect_ratio.num << ",\"sar_den\":" << codec->sample_aspect_ratio.den
           << ",\"field_order\":" << static_cast<int>(codec->field_order)
           << ",\"start_time\":" << stream->start_time << ",\"duration\":" << stream->duration
           << ",\"sample_rate\":" << codec->sample_rate << ",\"channels\":" << codec->ch_layout.nb_channels
           << ",\"channel_layout\":\"" << json_escape(channel_layout)
           << "\",\"color_range\":" << static_cast<int>(codec->color_range)
           << ",\"color_primaries\":" << static_cast<int>(codec->color_primaries)
           << ",\"color_transfer\":" << static_cast<int>(codec->color_trc)
           << ",\"color_matrix\":" << static_cast<int>(codec->color_space)
           << ",\"hlg\":" << (codec->color_trc == AVCOL_TRC_ARIB_STD_B67 ? "true" : "false")
           << ",\"dolby_e\":" << (dolby_e ? "true" : "false")
           << ",\"s302m_carrier\":" << (s302m ? "true" : "false")
           << ",\"bitstream_audio_transport\":\""
           << (bitstream_bridge ? "lossless-matroska-bridge" :
               (dolby_e || s302m ? "smpte-337m/302m" : "")) << "\""
           << ",\"mastering_display_present\":" << (find_side_data(codec, AV_PKT_DATA_MASTERING_DISPLAY_METADATA) ? "true" : "false")
           << ",\"content_light_present\":" << (find_side_data(codec, AV_PKT_DATA_CONTENT_LIGHT_LEVEL) ? "true" : "false")
           << ",\"hdr10_plus_present\":" << (find_side_data(codec, AV_PKT_DATA_DYNAMIC_HDR10_PLUS) ? "true" : "false")
           << ",\"dovi_config_present\":" << (dovi_config ? "true" : "false")
           << ",\"dovi_profile\":" << (dovi_config ? static_cast<unsigned>(dovi_config->dv_profile) : 0U)
           << ",\"dovi_level\":" << (dovi_config ? static_cast<unsigned>(dovi_config->dv_level) : 0U)
           << ",\"metadata\":";
    write_metadata(output, stream->metadata);
    output << '}';
  }
  output << "],\"chapters\":[";
  for (unsigned i = 0; i < format->nb_chapters; ++i) {
    if (i != 0) output << ',';
    const auto* chapter = format->chapters[i];
    output << "{\"id\":" << chapter->id << ",\"time_base_num\":" << chapter->time_base.num
           << ",\"time_base_den\":" << chapter->time_base.den << ",\"start\":" << chapter->start
           << ",\"end\":" << chapter->end << ",\"metadata\":";
    write_metadata(output, chapter->metadata);
    output << '}';
  }
  output << "]}";
  return output.str();
#endif
}

std::optional<std::vector<int>> probe_stream_media_types(const std::filesystem::path& path) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path;
  return std::nullopt;
#else
  ProbeDeadline deadline{std::chrono::steady_clock::now() + std::chrono::seconds(5)};
  auto format = open_probe_input(path, deadline);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0) return std::nullopt;
  std::vector<int> types;
  types.reserve(format->nb_streams);
  for (unsigned i = 0; i < format->nb_streams; ++i) {
    const auto* codec = format->streams[i] == nullptr ? nullptr : format->streams[i]->codecpar;
    if (codec == nullptr) return std::nullopt;
    types.push_back(static_cast<int>(codec->codec_type));
  }
  return types;
#endif
}

std::optional<AudioTrackInfo> primary_audio_track(const std::filesystem::path& path) {
#ifndef SMARTCUT_HAS_FFMPEG
  (void)path; return std::nullopt;
#else
  auto format = ffmpeg::open_input(path);
  if (!format || avformat_find_stream_info(format.get(), nullptr) < 0) return std::nullopt;
  for (unsigned i = 0; i < format->nb_streams; ++i) {
    const auto* stream = format->streams[i];
    if (stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO)
      return AudioTrackInfo{static_cast<std::size_t>(i), stream->time_base.num, stream->time_base.den,
                            stream->codecpar->sample_rate, stream->codecpar->ch_layout.nb_channels};
  }
  return std::nullopt;
#endif
}

}  // namespace smartcut::media
