#include "preview.h"

#include <cstring>
#include <iostream>

namespace mediadb {

static std::string ff_err2str(int err) {
    char buf[AV_ERROR_MAX_STRING_SIZE] = {0};
    av_strerror(err, buf, sizeof(buf));
    return std::string(buf);
}

// ---- memory-buffer AVIOContext callbacks for FFmpeg ----

struct MemBuffer {
    const uint8_t* data;
    size_t size;
    size_t pos;
};

static int mem_read_packet(void* opaque, uint8_t* buf, int buf_size) {
    auto* mb = static_cast<MemBuffer*>(opaque);
    auto remaining = static_cast<int>(mb->size - mb->pos);
    if (remaining <= 0) return AVERROR_EOF;
    int to_read = std::min(buf_size, remaining);
    std::memcpy(buf, mb->data + mb->pos, static_cast<size_t>(to_read));
    mb->pos += static_cast<size_t>(to_read);
    return to_read;
}

static int64_t mem_seek_packet(void* opaque, int64_t offset, int whence) {
    auto* mb = static_cast<MemBuffer*>(opaque);
    if (whence == AVSEEK_SIZE) return static_cast<int64_t>(mb->size);
    int64_t new_pos = 0;
    if (whence == SEEK_SET)       new_pos = offset;
    else if (whence == SEEK_CUR)  new_pos = static_cast<int64_t>(mb->pos) + offset;
    else if (whence == SEEK_END)  new_pos = static_cast<int64_t>(mb->size) + offset;
    else return AVERROR(EINVAL);
    if (new_pos < 0 || new_pos > static_cast<int64_t>(mb->size)) return AVERROR(EINVAL);
    mb->pos = static_cast<size_t>(new_pos);
    return new_pos;
}

// ---- output memory buffer for MP4 muxing ----

struct OutputBuffer {
    std::vector<uint8_t> data;
    size_t pos = 0;
};

#if LIBAVFORMAT_VERSION_MAJOR >= 61
static int mem_write_packet(void* opaque, const uint8_t* buf, int buf_size) {
#else
static int mem_write_packet(void* opaque, uint8_t* buf, int buf_size) {
#endif
    auto* ob = static_cast<OutputBuffer*>(opaque);
    const auto end_pos = ob->pos + static_cast<size_t>(buf_size);
    if (end_pos > ob->data.size()) ob->data.resize(end_pos);
    std::memcpy(ob->data.data() + ob->pos, buf, static_cast<size_t>(buf_size));
    ob->pos = end_pos;
    return buf_size;
}

static int64_t mem_seek_output(void* opaque, int64_t offset, int whence) {
    auto* ob = static_cast<OutputBuffer*>(opaque);
    if (whence == AVSEEK_SIZE) return static_cast<int64_t>(ob->data.size());
    int64_t new_pos = 0;
    if (whence == SEEK_SET)       new_pos = offset;
    else if (whence == SEEK_CUR)  new_pos = static_cast<int64_t>(ob->pos) + offset;
    else if (whence == SEEK_END)  new_pos = static_cast<int64_t>(ob->data.size()) + offset;
    else return AVERROR(EINVAL);
    if (new_pos < 0 || new_pos > static_cast<int64_t>(ob->data.size())) return AVERROR(EINVAL);
    ob->pos = static_cast<size_t>(new_pos);
    return new_pos;
}

// ---- rotation helpers ----

static double get_stream_rotation(AVStream* stream) {
    for (int i = 0; i < stream->codecpar->nb_coded_side_data; ++i) {
        if (stream->codecpar->coded_side_data[i].type == AV_PKT_DATA_DISPLAYMATRIX) {
            const auto* sd = &stream->codecpar->coded_side_data[i];
            if (sd->size >= 9 * 4) {
                double angle = -av_display_rotation_get(reinterpret_cast<const int32_t*>(sd->data));
                angle -= 360 * std::floor(angle / 360);
                if (std::isfinite(angle) && angle > 0.5) return angle;
            }
        }
    }
    const AVDictionaryEntry* tag = av_dict_get(stream->metadata, "rotate", nullptr, 0);
    if (tag && tag->value) {
        char* end = nullptr;
        double angle = std::strtod(tag->value, &end);
        if (end != tag->value) {
            angle -= 360 * std::floor(angle / 360);
            if (angle > 0.5) return angle;
        }
    }
    return 0.0;
}

static double get_frame_rotation(AVFrame* frame) {
    AVFrameSideData* sd = av_frame_get_side_data(frame, AV_FRAME_DATA_DISPLAYMATRIX);
    if (sd && sd->size >= 9 * 4) {
        double angle = -av_display_rotation_get(reinterpret_cast<const int32_t*>(sd->data));
        angle -= 360 * std::floor(angle / 360);
        if (std::isfinite(angle) && angle > 0.5) return angle;
    }
    return 0.0;
}

static bool pix_fmt_has_alpha(AVPixelFormat fmt) {
    const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(fmt);
    return desc && (desc->flags & AV_PIX_FMT_FLAG_ALPHA);
}

static AVFrame* transpose_frame(AVFrame* src, int steps) {
    if (steps == 0) return nullptr;

    AVFrame* dst = av_frame_alloc();
    if (!dst) return nullptr;

    if (steps == 2) {
        dst->width = src->width;
        dst->height = src->height;
    } else {
        dst->width = src->height;
        dst->height = src->width;
    }
    dst->format = src->format;
    if (av_frame_get_buffer(dst, 32) < 0) { av_frame_free(&dst); return nullptr; }

    const AVPixFmtDescriptor* desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(src->format));
    if (!desc) { av_frame_free(&dst); return nullptr; }
    const int num_planes = av_pix_fmt_count_planes(static_cast<AVPixelFormat>(src->format));
    const int bytes_per_pixel = av_get_padded_bits_per_pixel(desc) / 8 / num_planes;

    if (num_planes == 1 && bytes_per_pixel > 0) {
        const int sw = src->width;
        const int sh = src->height;
        const int bpp = bytes_per_pixel;
        const uint8_t* s = src->data[0];
        uint8_t* d = dst->data[0];
        for (int y = 0; y < sh; ++y) {
            for (int x = 0; x < sw; ++x) {
                int dx, dy;
                if (steps == 1) { dx = sh - 1 - y; dy = x; }
                else if (steps == 2) { dx = sw - 1 - x; dy = sh - 1 - y; }
                else { dx = y; dy = sw - 1 - x; }
                std::memcpy(d + dy * dst->linesize[0] + dx * bpp,
                            s + y * src->linesize[0] + x * bpp,
                            bpp);
            }
        }
        return dst;
    }

    if (num_planes >= 3) {
        for (int p = 0; p < num_planes; ++p) {
            const int pw = (p == 0) ? src->width : (src->width >> desc->log2_chroma_w);
            const int ph = (p == 0) ? src->height : (src->height >> desc->log2_chroma_h);
            int dpw, dph;
            if (steps == 2) { dpw = pw; dph = ph; }
            else { dpw = ph; dph = pw; }
            (void)dpw; (void)dph;
            for (int y = 0; y < ph; ++y) {
                for (int x = 0; x < pw; ++x) {
                    int dx, dy;
                    if (steps == 1) { dx = ph - 1 - y; dy = x; }
                    else if (steps == 2) { dx = pw - 1 - x; dy = ph - 1 - y; }
                    else { dx = y; dy = pw - 1 - x; }
                    dst->data[p][dy * dst->linesize[p] + dx] = src->data[p][y * src->linesize[p] + x];
                }
            }
        }
        return dst;
    }
    av_frame_free(&dst);
    return nullptr;
}

// ==================== PreviewCache ====================

PreviewCache::PreviewCache(std::size_t max_bytes) : max_bytes_(max_bytes) {
    const std::size_t estimated_entries = max_bytes / (100U * 1024U);
    if (estimated_entries > 0) {
        entries_.reserve(estimated_entries);
    }
}

std::optional<PreviewValue> PreviewCache::get(const PreviewKey& key) {
    std::lock_guard<std::mutex> lock(mutex_);
    auto it = entries_.find(key);
    if (it == entries_.end()) return std::nullopt;
    lru_.splice(lru_.begin(), lru_, it->second.lru_it);
    return it->second.value;
}

void PreviewCache::put(const PreviewKey& key, PreviewValue value) {
    if (value.size > max_bytes_) return;

    std::lock_guard<std::mutex> lock(mutex_);
    auto existing = entries_.find(key);
    if (existing != entries_.end()) {
        used_bytes_ -= existing->second.value.size;
        lru_.erase(existing->second.lru_it);
        entries_.erase(existing);
    }

    while (!lru_.empty() && used_bytes_ + value.size > max_bytes_) {
        const auto& old_key = lru_.back();
        auto old_it = entries_.find(old_key);
        if (old_it != entries_.end()) {
            used_bytes_ -= old_it->second.value.size;
            entries_.erase(old_it);
        }
        lru_.pop_back();
    }

    lru_.push_front(key);
    entries_[key] = CacheEntry{std::move(value), lru_.begin()};
    used_bytes_ += entries_[key].value.size;
}

// ==================== FFmpegPreviewer ====================

std::optional<PreviewValue> FFmpegPreviewer::render(const std::vector<std::uint8_t>& file_data, const std::string& media_kind,
                                                     int width, int height, const std::string& fmt, int t_seconds,
                                                     int duration, std::string& error_out) {
    if (file_data.empty()) {
        error_out = "empty media data";
        return std::nullopt;
    }

    MemBuffer mem_buf{file_data.data(), file_data.size(), 0};

    constexpr size_t avio_buf_size = 32768;
    auto* avio_buf = static_cast<uint8_t*>(av_malloc(avio_buf_size));
    if (!avio_buf) { error_out = "av_malloc failed"; return std::nullopt; }

    AVIOContext* avio_ctx = avio_alloc_context(avio_buf, static_cast<int>(avio_buf_size), 0,
                                               &mem_buf, mem_read_packet, nullptr, mem_seek_packet);
    if (!avio_ctx) { av_free(avio_buf); error_out = "avio_alloc_context failed"; return std::nullopt; }

    AVFormatContext* format_ctx = avformat_alloc_context();
    if (!format_ctx) { avio_context_free(&avio_ctx); error_out = "avformat_alloc_context failed"; return std::nullopt; }
    format_ctx->pb = avio_ctx;
    format_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;

    int rc = avformat_open_input(&format_ctx, nullptr, nullptr, nullptr);
    if (rc < 0) {
        error_out = "avformat_open_input failed: " + ff_err2str(rc);
        avio_context_free(&avio_ctx);
        return std::nullopt;
    }
    auto format_deleter = [&avio_ctx](AVFormatContext* ctx) {
        avformat_close_input(&ctx);
        avio_context_free(&avio_ctx);
    };
    std::unique_ptr<AVFormatContext, decltype(format_deleter)> format_guard(format_ctx, format_deleter);

    rc = avformat_find_stream_info(format_ctx, nullptr);
    if (rc < 0) {
        error_out = "avformat_find_stream_info failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    const int stream_index = av_find_best_stream(format_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, 0);
    if (stream_index < 0) {
        error_out = "no previewable video stream found";
        return std::nullopt;
    }

    AVStream* stream = format_ctx->streams[stream_index];
    const AVCodec* decoder = avcodec_find_decoder(stream->codecpar->codec_id);
    if (decoder == nullptr) {
        error_out = "no decoder available";
        return std::nullopt;
    }

    AVCodecContext* dec_ctx = avcodec_alloc_context3(decoder);
    if (dec_ctx == nullptr) {
        error_out = "avcodec_alloc_context3 failed";
        return std::nullopt;
    }
    auto dec_deleter = [](AVCodecContext* ctx) { avcodec_free_context(&ctx); };
    std::unique_ptr<AVCodecContext, decltype(dec_deleter)> dec_guard(dec_ctx, dec_deleter);

    rc = avcodec_parameters_to_context(dec_ctx, stream->codecpar);
    if (rc < 0) {
        error_out = "avcodec_parameters_to_context failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    rc = avcodec_open2(dec_ctx, decoder, nullptr);
    if (rc < 0) {
        error_out = "avcodec_open2 decoder failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    if (t_seconds > 0) {
        const auto seek_target = static_cast<int64_t>(t_seconds / av_q2d(stream->time_base));
        av_seek_frame(format_ctx, stream_index, seek_target, AVSEEK_FLAG_BACKWARD);
        avcodec_flush_buffers(dec_ctx);
    }

    const int src_w = dec_ctx->width;
    const int src_h = dec_ctx->height;
    if (src_w <= 0 || src_h <= 0) {
        error_out = "invalid source dimensions";
        return std::nullopt;
    }

    bool src_has_alpha = pix_fmt_has_alpha(dec_ctx->pix_fmt);

    // --- For video clips ---
    if (fmt == "mp4" || fmt == "av1" || fmt == "h265") {
        int out_w = width;
        int out_h = height;
        if (out_w <= 0 && out_h <= 0)      { out_w = src_w; out_h = src_h; }
        else if (out_w > 0 && out_h <= 0)   { out_h = std::max(1, src_h * out_w / src_w); }
        else if (out_h > 0 && out_w <= 0)   { out_w = std::max(1, src_w * out_h / src_h); }
        out_w = (out_w + 1) & ~1; out_h = (out_h + 1) & ~1;

        AVPixelFormat encode_fmt = AV_PIX_FMT_YUV420P;
        AVFrame* scaled_vc = av_frame_alloc();
        if (!scaled_vc) { error_out = "frame alloc failed"; return std::nullopt; }
        auto vc_deleter = [](AVFrame* f) { av_frame_free(&f); };
        std::unique_ptr<AVFrame, decltype(vc_deleter)> vc_guard(scaled_vc, vc_deleter);
        scaled_vc->format = encode_fmt; scaled_vc->width = out_w; scaled_vc->height = out_h;
        rc = av_frame_get_buffer(scaled_vc, 32);
        if (rc < 0) { error_out = "av_frame_get_buffer failed"; return std::nullopt; }
        SwsContext* sws_vc = sws_getContext(src_w, src_h, dec_ctx->pix_fmt, out_w, out_h, encode_fmt,
                                            SWS_BILINEAR, nullptr, nullptr, nullptr);
        if (!sws_vc) { error_out = "sws_getContext failed"; return std::nullopt; }
        std::unique_ptr<SwsContext, decltype(&sws_freeContext)> sws_vc_guard(sws_vc, &sws_freeContext);

        if (fmt == "mp4")
            return render_video_clip(format_ctx, stream_index, dec_ctx,
                                     src_w, src_h, out_w, out_h, sws_vc, scaled_vc, duration, "mp4", error_out);
        if (fmt == "av1")
            return render_video_clip(format_ctx, stream_index, dec_ctx,
                                     src_w, src_h, out_w, out_h, sws_vc, scaled_vc, duration, "av1", error_out);
        return render_video_clip(format_ctx, stream_index, dec_ctx,
                                 src_w, src_h, out_w, out_h, sws_vc, scaled_vc, duration, "h265", error_out);
    }

    // --- Still image path ---
    AVPacket* packet = av_packet_alloc();
    AVFrame* decoded = av_frame_alloc();
    if (packet == nullptr || decoded == nullptr) {
        error_out = "packet/frame allocation failed";
        av_packet_free(&packet);
        av_frame_free(&decoded);
        return std::nullopt;
    }
    auto pkt_deleter = [](AVPacket* p) { av_packet_free(&p); };
    auto frame_deleter = [](AVFrame* f) { av_frame_free(&f); };
    std::unique_ptr<AVPacket, decltype(pkt_deleter)> packet_guard(packet, pkt_deleter);
    std::unique_ptr<AVFrame, decltype(frame_deleter)> decoded_guard(decoded, frame_deleter);

    bool have_frame = false;
    while (av_read_frame(format_ctx, packet) >= 0) {
        if (packet->stream_index != stream_index) {
            av_packet_unref(packet);
            continue;
        }
        rc = avcodec_send_packet(dec_ctx, packet);
        av_packet_unref(packet);
        if (rc < 0) continue;
        while ((rc = avcodec_receive_frame(dec_ctx, decoded)) >= 0) {
            have_frame = true;
            break;
        }
        if (have_frame) break;
    }
    if (!have_frame) {
        error_out = "could not decode a frame";
        return std::nullopt;
    }

    double rotation = get_frame_rotation(decoded);
    if (rotation < 0.5) rotation = get_stream_rotation(stream);
    int rot_steps = 0;
    if (rotation > 45 && rotation < 135)        rot_steps = 1;
    else if (rotation >= 135 && rotation < 225)  rot_steps = 2;
    else if (rotation >= 225 && rotation < 315)  rot_steps = 3;

    const int effective_src_w = (rot_steps == 1 || rot_steps == 3) ? src_h : src_w;
    const int effective_src_h = (rot_steps == 1 || rot_steps == 3) ? src_w : src_h;

    int out_w = width;
    int out_h = height;
    if (out_w <= 0 && out_h <= 0) {
        out_w = effective_src_w;
        out_h = effective_src_h;
    } else if (out_w > 0 && out_h <= 0) {
        out_h = std::max(1, effective_src_h * out_w / effective_src_w);
    } else if (out_h > 0 && out_w <= 0) {
        out_w = std::max(1, effective_src_w * out_h / effective_src_h);
    }

    AVPixelFormat encode_fmt;
    if (fmt == "png") {
        encode_fmt = src_has_alpha ? AV_PIX_FMT_RGBA : AV_PIX_FMT_RGB24;
    } else if (fmt == "webp") {
        encode_fmt = src_has_alpha ? AV_PIX_FMT_BGRA : AV_PIX_FMT_YUV420P;
    } else {
        encode_fmt = AV_PIX_FMT_YUV420P;
    }

    int scale_w = out_w;
    int scale_h = out_h;
    if (rot_steps == 1 || rot_steps == 3) {
        scale_w = out_h;
        scale_h = out_w;
    }

    AVFrame* scaled = av_frame_alloc();
    if (!scaled) { error_out = "frame alloc failed"; return std::nullopt; }
    std::unique_ptr<AVFrame, decltype(frame_deleter)> scaled_guard(scaled, frame_deleter);
    scaled->format = encode_fmt;
    scaled->width = scale_w;
    scaled->height = scale_h;
    rc = av_frame_get_buffer(scaled, 32);
    if (rc < 0) {
        error_out = "av_frame_get_buffer failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    SwsContext* sws = sws_getContext(src_w, src_h, dec_ctx->pix_fmt, scale_w, scale_h, encode_fmt, SWS_BILINEAR, nullptr,
                                     nullptr, nullptr);
    if (sws == nullptr) {
        error_out = "sws_getContext failed";
        return std::nullopt;
    }
    std::unique_ptr<SwsContext, decltype(&sws_freeContext)> sws_guard(sws, &sws_freeContext);

    sws_scale(sws, decoded->data, decoded->linesize, 0, src_h, scaled->data, scaled->linesize);

    AVFrame* output_frame = scaled;
    std::unique_ptr<AVFrame, decltype(frame_deleter)> rotated_guard(nullptr, frame_deleter);
    if (rot_steps != 0) {
        AVFrame* rotated = transpose_frame(scaled, rot_steps);
        if (rotated) {
            rotated_guard.reset(rotated);
            output_frame = rotated;
        }
    }

    return encode_frame(output_frame, fmt, error_out);
}

std::optional<PreviewValue> FFmpegPreviewer::render_video_clip(
    AVFormatContext* format_ctx, int stream_index, AVCodecContext* dec_ctx,
    int src_w, int /*src_h*/, int out_w, int out_h,
    SwsContext* sws, AVFrame* scaled,
    int duration_seconds, const std::string& codec_tag, std::string& error_out)
{
    const AVCodec* encoder = nullptr;
    std::string container_fmt;
    std::string mime;
    if (codec_tag == "av1") {
        encoder = avcodec_find_encoder_by_name("libaom-av1");
        if (!encoder) encoder = avcodec_find_encoder(AV_CODEC_ID_AV1);
        if (!encoder) { error_out = "AV1 encoder not available"; return std::nullopt; }
        container_fmt = "mp4";
        mime = "video/mp4";
    } else if (codec_tag == "h265") {
        encoder = avcodec_find_encoder_by_name("libx265");
        if (!encoder) encoder = avcodec_find_encoder(AV_CODEC_ID_HEVC);
        if (!encoder) { error_out = "H.265/HEVC encoder not available"; return std::nullopt; }
        container_fmt = "mp4";
        mime = "video/mp4";
    } else {
        encoder = avcodec_find_encoder_by_name("libx264");
        if (!encoder) encoder = avcodec_find_encoder(AV_CODEC_ID_H264);
        if (!encoder) { error_out = "H.264 encoder not available"; return std::nullopt; }
        container_fmt = "mp4";
        mime = "video/mp4";
    }

    AVCodecContext* enc_ctx = avcodec_alloc_context3(encoder);
    if (!enc_ctx) { error_out = "encoder alloc failed"; return std::nullopt; }
    auto enc_del = [](AVCodecContext* c) { avcodec_free_context(&c); };
    std::unique_ptr<AVCodecContext, decltype(enc_del)> enc_guard(enc_ctx, enc_del);

    AVStream* stream = format_ctx->streams[stream_index];
    AVRational src_fps = stream->avg_frame_rate;
    if (src_fps.num <= 0 || src_fps.den <= 0) src_fps = {25, 1};
    if (av_q2d(src_fps) > 30.0) src_fps = {30, 1};

    enc_ctx->width = out_w;
    enc_ctx->height = out_h;
    enc_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
    enc_ctx->time_base = av_inv_q(src_fps);
    enc_ctx->framerate = src_fps;
    enc_ctx->gop_size = 12;
    enc_ctx->max_b_frames = 2;
    enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

    if (codec_tag == "av1") {
        enc_ctx->bit_rate = 0;
        av_opt_set(enc_ctx->priv_data, "crf", "35", 0);
        av_opt_set(enc_ctx->priv_data, "cpu-used", "8", 0);
        av_opt_set(enc_ctx->priv_data, "usage", "realtime", 0);
        av_opt_set(enc_ctx->priv_data, "tiles", "2x2", 0);
    } else if (codec_tag == "h265") {
        enc_ctx->bit_rate = 0;
        av_opt_set(enc_ctx->priv_data, "crf", "28", 0);
        av_opt_set(enc_ctx->priv_data, "preset", "ultrafast", 0);
    } else {
        enc_ctx->bit_rate = 1000000;
        av_opt_set(enc_ctx->priv_data, "preset", "ultrafast", 0);
    }

    int rc = avcodec_open2(enc_ctx, encoder, nullptr);
    if (rc < 0) { error_out = "encoder open failed: " + ff_err2str(rc); return std::nullopt; }

    OutputBuffer out_buf;
    constexpr size_t out_avio_size = 32768;
    auto* out_avio_buf = static_cast<uint8_t*>(av_malloc(out_avio_size));
    if (!out_avio_buf) { error_out = "av_malloc output failed"; return std::nullopt; }

    AVIOContext* out_avio = avio_alloc_context(
        out_avio_buf, static_cast<int>(out_avio_size), 1,
        &out_buf, nullptr, mem_write_packet, mem_seek_output);
    if (!out_avio) { av_free(out_avio_buf); error_out = "output avio failed"; return std::nullopt; }

    AVFormatContext* out_fmt = nullptr;
    rc = avformat_alloc_output_context2(&out_fmt, nullptr, container_fmt.c_str(), nullptr);
    if (rc < 0 || !out_fmt) {
        avio_context_free(&out_avio);
        error_out = "avformat output alloc failed";
        return std::nullopt;
    }
    auto out_fmt_del = [&out_avio](AVFormatContext* c) {
        avformat_free_context(c);
        avio_context_free(&out_avio);
    };
    std::unique_ptr<AVFormatContext, decltype(out_fmt_del)> out_fmt_guard(out_fmt, out_fmt_del);

    out_fmt->pb = out_avio;
    out_fmt->flags |= AVFMT_FLAG_CUSTOM_IO;

    AVStream* out_stream = avformat_new_stream(out_fmt, nullptr);
    if (!out_stream) { error_out = "new stream failed"; return std::nullopt; }
    avcodec_parameters_from_context(out_stream->codecpar, enc_ctx);
    out_stream->time_base = enc_ctx->time_base;

    int audio_stream_index = av_find_best_stream(format_ctx, AVMEDIA_TYPE_AUDIO, -1, -1, nullptr, 0);
    AVStream* out_audio_stream = nullptr;
    int64_t audio_max_pts = AV_NOPTS_VALUE;

    if (audio_stream_index >= 0) {
        AVStream* audio_stream = format_ctx->streams[audio_stream_index];
        AVCodecID audio_codec_id = audio_stream->codecpar->codec_id;
        if (audio_codec_id == AV_CODEC_ID_AAC ||
            audio_codec_id == AV_CODEC_ID_MP3 ||
            audio_codec_id == AV_CODEC_ID_AC3 ||
            audio_codec_id == AV_CODEC_ID_EAC3 ||
            audio_codec_id == AV_CODEC_ID_OPUS ||
            audio_codec_id == AV_CODEC_ID_FLAC) {
            out_audio_stream = avformat_new_stream(out_fmt, nullptr);
            if (out_audio_stream) {
                avcodec_parameters_copy(out_audio_stream->codecpar, audio_stream->codecpar);
                out_audio_stream->time_base = audio_stream->time_base;
                out_audio_stream->codecpar->codec_tag = 0;
                if (audio_stream->time_base.den > 0) {
                    audio_max_pts = static_cast<int64_t>(
                        static_cast<double>(duration_seconds) / av_q2d(audio_stream->time_base));
                }
            }
        }
    }

    AVDictionary* opts = nullptr;
    av_dict_set(&opts, "movflags", "frag_keyframe+empty_moov+default_base_moof", 0);
    rc = avformat_write_header(out_fmt, &opts);
    av_dict_free(&opts);
    if (rc < 0) { error_out = "write header failed: " + ff_err2str(rc); return std::nullopt; }

    AVPacket* pkt = av_packet_alloc();
    AVFrame* decoded = av_frame_alloc();
    AVPacket* out_pkt = av_packet_alloc();
    if (!pkt || !decoded || !out_pkt) {
        av_packet_free(&pkt); av_frame_free(&decoded); av_packet_free(&out_pkt);
        error_out = "alloc failed"; return std::nullopt;
    }
    auto pd = [](AVPacket* p) { av_packet_free(&p); };
    auto fd = [](AVFrame* f) { av_frame_free(&f); };
    std::unique_ptr<AVPacket, decltype(pd)> pkt_guard(pkt, pd);
    std::unique_ptr<AVFrame, decltype(fd)> dec_guard(decoded, fd);
    std::unique_ptr<AVPacket, decltype(pd)> opkt_guard(out_pkt, pd);

    const int fps = std::max(1, static_cast<int>(av_q2d(src_fps) + 0.5));
    int max_frames = duration_seconds * fps;
    if (max_frames <= 0) max_frames = 5 * 25;
    int frame_count = 0;
    int64_t audio_first_pts = AV_NOPTS_VALUE;

    while (av_read_frame(format_ctx, pkt) >= 0 && frame_count < max_frames) {
        if (pkt->stream_index == stream_index) {
            rc = avcodec_send_packet(dec_ctx, pkt);
            av_packet_unref(pkt);
            if (rc < 0) continue;

            while (avcodec_receive_frame(dec_ctx, decoded) >= 0 && frame_count < max_frames) {
                sws_scale(sws, decoded->data, decoded->linesize, 0, dec_ctx->height,
                          scaled->data, scaled->linesize);
                scaled->pts = frame_count++;
                rc = avcodec_send_frame(enc_ctx, scaled);
                if (rc < 0) continue;
                while (avcodec_receive_packet(enc_ctx, out_pkt) >= 0) {
                    av_packet_rescale_ts(out_pkt, enc_ctx->time_base, out_stream->time_base);
                    out_pkt->stream_index = out_stream->index;
                    av_interleaved_write_frame(out_fmt, out_pkt);
                }
            }
        } else if (out_audio_stream && pkt->stream_index == audio_stream_index) {
            if (audio_max_pts != AV_NOPTS_VALUE && pkt->pts != AV_NOPTS_VALUE) {
                if (audio_first_pts == AV_NOPTS_VALUE)
                    audio_first_pts = pkt->pts;
                if (pkt->pts - audio_first_pts > audio_max_pts) {
                    av_packet_unref(pkt);
                    continue;
                }
            }
            pkt->stream_index = out_audio_stream->index;
            av_packet_rescale_ts(pkt, format_ctx->streams[audio_stream_index]->time_base,
                                 out_audio_stream->time_base);
            if (audio_first_pts != AV_NOPTS_VALUE) {
                int64_t offset = av_rescale_q(audio_first_pts,
                    format_ctx->streams[audio_stream_index]->time_base,
                    out_audio_stream->time_base);
                if (pkt->pts != AV_NOPTS_VALUE) pkt->pts -= offset;
                if (pkt->dts != AV_NOPTS_VALUE) pkt->dts -= offset;
            }
            av_interleaved_write_frame(out_fmt, pkt);
        } else {
            av_packet_unref(pkt);
        }
    }

    avcodec_send_frame(enc_ctx, nullptr);
    while (avcodec_receive_packet(enc_ctx, out_pkt) >= 0) {
        av_packet_rescale_ts(out_pkt, enc_ctx->time_base, out_stream->time_base);
        out_pkt->stream_index = out_stream->index;
        av_interleaved_write_frame(out_fmt, out_pkt);
    }

    av_write_trailer(out_fmt);

    if (frame_count == 0 || out_buf.data.empty()) {
        error_out = "could not produce video clip";
        return std::nullopt;
    }

    PreviewValue out;
    out.mime = mime;
    out.bytes = std::move(out_buf.data);
    out.size = out.bytes.size();
    out.etag = "W/\"" + std::to_string(out.size) + "-" + std::to_string(out_w) + "x" +
               std::to_string(out_h) + "-" + codec_tag + "\"";
    return out;
}

std::optional<PreviewValue> FFmpegPreviewer::encode_frame(AVFrame* frame, const std::string& fmt, std::string& error_out) {
    const AVCodec* encoder = nullptr;
    std::string mime = "image/jpeg";
    AVPixelFormat encoder_pix_fmt = static_cast<AVPixelFormat>(frame->format);

    if (fmt == "webp") {
        encoder = avcodec_find_encoder(AV_CODEC_ID_WEBP);
        mime = "image/webp";
    } else if (fmt == "png") {
        encoder = avcodec_find_encoder(AV_CODEC_ID_PNG);
        mime = "image/png";
    } else if (fmt == "avif") {
        encoder = avcodec_find_encoder_by_name("libaom-av1");
        mime = "image/avif";
        encoder_pix_fmt = AV_PIX_FMT_YUV420P;
    } else {
        encoder = avcodec_find_encoder(AV_CODEC_ID_MJPEG);
        mime = "image/jpeg";
        encoder_pix_fmt = AV_PIX_FMT_YUVJ420P;
    }

    if (encoder == nullptr) {
        error_out = "requested encoder not available";
        return std::nullopt;
    }

    AVCodecContext* enc_ctx = avcodec_alloc_context3(encoder);
    if (enc_ctx == nullptr) {
        error_out = "avcodec_alloc_context3 encoder failed";
        return std::nullopt;
    }
    auto enc_deleter = [](AVCodecContext* ctx) { avcodec_free_context(&ctx); };
    std::unique_ptr<AVCodecContext, decltype(enc_deleter)> enc_guard(enc_ctx, enc_deleter);

    enc_ctx->width = frame->width;
    enc_ctx->height = frame->height;
    enc_ctx->pix_fmt = encoder_pix_fmt;
    enc_ctx->time_base = AVRational{1, 25};

    if (fmt == "avif") {
        enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        enc_ctx->bit_rate = 0;
        av_opt_set(enc_ctx->priv_data, "crf", "30", 0);
        av_opt_set(enc_ctx->priv_data, "cpu-used", "6", 0);
        av_opt_set(enc_ctx->priv_data, "usage", "allintra", 0);
        av_opt_set(enc_ctx->priv_data, "tiles", "2x2", 0);
        av_opt_set(enc_ctx->priv_data, "still-picture", "1", 0);
    }

    int rc = avcodec_open2(enc_ctx, encoder, nullptr);
    if (rc < 0) {
        error_out = "avcodec_open2 encoder failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    AVPacket* out_packet = av_packet_alloc();
    if (out_packet == nullptr) {
        error_out = "av_packet_alloc encoder packet failed";
        return std::nullopt;
    }
    auto out_pkt_deleter = [](AVPacket* p) { av_packet_free(&p); };
    std::unique_ptr<AVPacket, decltype(out_pkt_deleter)> out_packet_guard(out_packet, out_pkt_deleter);

    rc = avcodec_send_frame(enc_ctx, frame);
    if (rc < 0) {
        error_out = "avcodec_send_frame failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    avcodec_send_frame(enc_ctx, nullptr);

    rc = avcodec_receive_packet(enc_ctx, out_packet);
    if (rc == AVERROR(EAGAIN)) {
        rc = avcodec_receive_packet(enc_ctx, out_packet);
    }
    if (rc < 0) {
        error_out = "avcodec_receive_packet failed: " + ff_err2str(rc);
        return std::nullopt;
    }

    PreviewValue out;
    out.mime = mime;
    out.bytes.assign(out_packet->data, out_packet->data + out_packet->size);
    out.size = out.bytes.size();
    out.etag = "W/\"" + std::to_string(out.size) + "-" + std::to_string(frame->width) + "x" + std::to_string(frame->height) +
               "-" + mime + "\"";
    return out;
}

// ==================== PreviewService ====================

PreviewService::PreviewService(PreviewCache& cache, MediaBackendApi& db) : cache_(cache), db_(db) {}

std::optional<PreviewValue> PreviewService::cache_get(const PreviewKey& key) {
    return cache_.get(key);
}

std::optional<PreviewValue> PreviewService::get_or_create(const MediaRecord& media,
                                                           const std::vector<std::uint8_t>& file_data,
                                                           int width, int height, const std::string& fmt,
                                                           int t_seconds, int duration, std::string& error_out) {
    PreviewKey key{media.id, width, height, fmt, t_seconds, duration};
    if (auto hit = cache_.get(key); hit.has_value()) {
        return hit;
    }

    auto preview = ffmpeg_.render(file_data, media.media_kind, width, height, fmt, t_seconds, duration, error_out);
    if (!preview.has_value()) {
        return std::nullopt;
    }

    cache_.put(key, *preview);
    return preview;
}

} // namespace mediadb
