#include "preview.h"

#include <cstring>
#include <deque>
#include <future>
#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);
}

// ---- streaming AVIOContext: reads from backend with parallel prefetch ----

/*
 * PrefetchBuffer — issues N async block reads ahead so cluster I/O
 * overlaps with FFmpeg decode.  Fetched blocks are stored in the shared
 * BlobCache (type=prefetch) so other requests for the same media
 * benefit from previously fetched data.
 */
struct PrefetchBuffer {
    static constexpr std::size_t BLOCK_SIZE   = 64 * 1024;   // 64 KB — small blocks for fast initial decode
    static constexpr std::size_t AHEAD_BLOCKS = 4;           // 4 blocks ahead = 256 KB look-ahead
    // Files up to this size are fetched in a single cluster round-trip
    // instead of many small range requests (avoids repeated parity reconstruction).
    static constexpr std::size_t BULK_FETCH_LIMIT = 64ULL * 1024 * 1024; // 64 MB

    MediaBackendApi* db;
    ThreadPool*      io_pool;   // bounded I/O pool (separate from render pool)
    BlobCache*       cache;
    std::string      media_id;
    std::uint64_t    total_size;
    std::uint64_t    pos = 0;
    bool             premature_eof = false;  // set when read returns empty before total_size
    std::vector<uint8_t> bulk_data_;         // pre-fetched full file (empty if not used)

    // prefetch ring — holds in-flight async reads
    struct PendingBlock {
        std::uint64_t                       offset;
        std::uint64_t                       length;
        std::future<std::vector<uint8_t>>   fut;
        std::vector<uint8_t>                data;
    };
    std::deque<PendingBlock> ring;

    ~PrefetchBuffer() {
        // Wait for all in-flight io_pool tasks before destroying members
        // they reference (db, media_id, cache).  Destroying a std::future
        // does NOT block, so without this the tasks would access freed memory.
        for (auto& b : ring) {
            if (b.fut.valid()) {
                try { b.fut.wait(); } catch (...) {}
            }
        }
    }

    // Fetch the entire file using chunked range reads (same fast path
    // as /raw).  The old read_media_data() → retrieve() path retries
    // each failing stripe with exponential backoff (~30 s total), while
    // read_media_data_range() → retrieve_range() fails immediately per
    // stripe — avoiding the long stall that blocks preview generation.
    void bulk_prefetch() {
        if (total_size == 0 || total_size > BULK_FETCH_LIMIT) return;
        static constexpr std::uint64_t CHUNK = 4 * 1024 * 1024; // 4 MB
        bulk_data_.reserve(static_cast<std::size_t>(total_size));
        for (std::uint64_t off = 0; off < total_size; off += CHUNK) {
            std::uint64_t len = std::min<std::uint64_t>(CHUNK, total_size - off);
            auto chunk = db->read_media_data_range(media_id, off, len);
            if (chunk.empty()) {
                bulk_data_.clear();
                return;
            }
            bulk_data_.insert(bulk_data_.end(), chunk.begin(), chunk.end());
        }
        if (!bulk_data_.empty()) {
            total_size = bulk_data_.size();
        }
    }

    static std::string make_key(const std::string& mid, std::uint64_t off, std::uint64_t len) {
        return "prefetch:" + mid + ":" + std::to_string(off) + ":" + std::to_string(len);
    }

    // Try to serve a block from the BlobCache; returns empty vector on miss.
    std::vector<uint8_t> cache_get(std::uint64_t off, std::uint64_t len) {
        if (!cache) return {};
        auto hit = cache->get(make_key(media_id, off, len));
        if (hit) return hit->data;
        return {};
    }

    // Store a block in the BlobCache.
    void cache_put(std::uint64_t off, std::uint64_t len, const std::vector<uint8_t>& bytes) {
        if (!cache || bytes.empty()) return;
        BlobValue bv;
        bv.data = bytes;
        bv.type = BlobType::prefetch;
        cache->put(make_key(media_id, off, len), std::move(bv));
    }

    void kick_prefetch() {
        std::uint64_t next = pos;
        if (!ring.empty()) {
            auto& last = ring.back();
            next = last.offset + last.length;
        }
        while (ring.size() < AHEAD_BLOCKS && next < total_size) {
            std::uint64_t len = std::min<std::uint64_t>(BLOCK_SIZE, total_size - next);
            std::uint64_t off = next;

            // check cache first — if hit, wrap in a ready future
            auto cached = cache_get(off, len);
            if (!cached.empty()) {
                std::promise<std::vector<uint8_t>> p;
                p.set_value(cached);
                ring.push_back(PendingBlock{off, len, p.get_future(), std::vector<uint8_t>()});
            } else {
                // Capture members by value — not `this` — so the lambda
                // remains safe even if PrefetchBuffer is destroyed while
                // the task is still queued / running in io_pool.
                MediaBackendApi* pdb = db;
                BlobCache* pcache = cache;
                std::string pmid = media_id;
                ring.push_back(PendingBlock{
                    off, len,
                    io_pool->submit([pdb, pcache, pmid, off, len]() {
                        auto bytes = pdb->read_media_data_range(pmid, off, len);
                        if (pcache && !bytes.empty()) {
                            BlobValue bv;
                            bv.data = bytes;
                            bv.type = BlobType::prefetch;
                            pcache->put(make_key(pmid, off, len), std::move(bv));
                        }
                        return bytes;
                    }),
                    std::vector<uint8_t>()
                });
            }
            next += len;
        }
    }

    int read(uint8_t* buf, int want) {
        if (pos >= total_size) return AVERROR_EOF;

        // Fast path: serve from bulk-prefetched data
        if (!bulk_data_.empty()) {
            std::uint64_t avail = bulk_data_.size() - pos;
            std::uint64_t n = std::min<std::uint64_t>(want, avail);
            std::memcpy(buf, bulk_data_.data() + pos, n);
            pos += n;
            return static_cast<int>(n);
        }

        kick_prefetch();

        // drop blocks entirely before pos (after a seek)
        while (!ring.empty() && ring.front().offset + ring.front().length <= pos) {
            if (ring.front().fut.valid()) ring.front().fut.wait();
            ring.pop_front();
        }

        // fallback: nothing in the ring covers pos
        if (ring.empty() || ring.front().offset > pos) {
            std::uint64_t remaining = total_size - pos;
            std::uint64_t to_read = std::min<std::uint64_t>(want, remaining);
            auto data = db->read_media_data_range(media_id, pos, to_read);
            if (data.empty()) { premature_eof = (pos < total_size); return AVERROR_EOF; }
            std::memcpy(buf, data.data(), data.size());
            pos += data.size();
            return static_cast<int>(data.size());
        }

        auto& front = ring.front();
        if (front.data.empty() && front.fut.valid()) {
            front.data = front.fut.get();
        }

        if (front.data.empty()) {
            premature_eof = (pos < total_size);
            ring.pop_front();
            return AVERROR_EOF;
        }

        std::uint64_t offset_in_block = pos - front.offset;
        std::uint64_t avail = front.data.size() - offset_in_block;
        std::uint64_t n = std::min<std::uint64_t>(want, avail);
        std::memcpy(buf, front.data.data() + offset_in_block, n);
        pos += n;

        if (pos >= front.offset + front.length) {
            ring.pop_front();
        }

        kick_prefetch();
        return static_cast<int>(n);
    }

    void seek_to(std::uint64_t new_pos) {
        while (!ring.empty()) {
            auto& b = ring.back();
            if (b.offset <= new_pos && new_pos < b.offset + b.length) break;
            if (b.offset > new_pos) {
                if (b.fut.valid()) b.fut.wait();
                ring.pop_back();
            } else {
                break;
            }
        }
        while (!ring.empty() && ring.front().offset + ring.front().length <= new_pos) {
            if (ring.front().fut.valid()) ring.front().fut.wait();
            ring.pop_front();
        }
        pos = new_pos;
    }
};

static int streaming_read_packet(void* opaque, uint8_t* buf, int buf_size) {
    return static_cast<PrefetchBuffer*>(opaque)->read(buf, buf_size);
}

static int64_t streaming_seek_packet(void* opaque, int64_t offset, int whence) {
    auto* pb = static_cast<PrefetchBuffer*>(opaque);
    if (whence == AVSEEK_SIZE) return static_cast<int64_t>(pb->total_size);
    int64_t new_pos = 0;
    if (whence == SEEK_SET)       new_pos = offset;
    else if (whence == SEEK_CUR)  new_pos = static_cast<int64_t>(pb->pos) + offset;
    else if (whence == SEEK_END)  new_pos = static_cast<int64_t>(pb->total_size) + offset;
    else return AVERROR(EINVAL);
    if (new_pos < 0 || new_pos > static_cast<int64_t>(pb->total_size)) return AVERROR(EINVAL);
    pb->seek_to(static_cast<uint64_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];
        const int src_stride = src->linesize[0];
        const int dst_stride = dst->linesize[0];
        const int row_bytes = sw * bpp;

        if (steps == 2) {
            // 180°: reverse rows and reverse pixels within each row
            for (int y = 0; y < sh; ++y) {
                const uint8_t* src_row = s + y * src_stride;
                uint8_t* dst_row = d + (sh - 1 - y) * dst_stride;
                if (bpp == 1) {
                    for (int x = 0; x < sw; ++x)
                        dst_row[sw - 1 - x] = src_row[x];
                } else {
                    for (int x = 0; x < sw; ++x)
                        std::memcpy(dst_row + (sw - 1 - x) * bpp, src_row + x * bpp, bpp);
                }
            }
        } else {
            // 90° (steps==1) or 270° (steps==3): per-pixel transpose
            // Process in tiles to improve cache behavior
            constexpr int TILE = 32;
            for (int ty = 0; ty < sh; ty += TILE) {
                for (int tx = 0; tx < sw; tx += TILE) {
                    const int ye = std::min(ty + TILE, sh);
                    const int xe = std::min(tx + TILE, sw);
                    for (int y = ty; y < ye; ++y) {
                        for (int x = tx; x < xe; ++x) {
                            int dx, dy;
                            if (steps == 1) { dx = sh - 1 - y; dy = x; }
                            else             { dx = y; dy = sw - 1 - x; }
                            std::memcpy(d + dy * dst_stride + dx * bpp,
                                        s + y * src_stride + x * bpp,
                                        bpp);
                        }
                    }
                }
            }
        }
        return dst;
    }

    if (num_planes >= 3) {
        constexpr int TILE = 32;
        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);
            const int src_stride = src->linesize[p];
            const int dst_stride = dst->linesize[p];

            if (steps == 2) {
                // 180°: reverse rows and pixels
                for (int y = 0; y < ph; ++y) {
                    const uint8_t* sr = src->data[p] + y * src_stride;
                    uint8_t* dr = dst->data[p] + (ph - 1 - y) * dst_stride;
                    for (int x = 0; x < pw; ++x)
                        dr[pw - 1 - x] = sr[x];
                }
            } else {
                for (int ty = 0; ty < ph; ty += TILE) {
                    for (int tx = 0; tx < pw; tx += TILE) {
                        const int ye = std::min(ty + TILE, ph);
                        const int xe = std::min(tx + TILE, pw);
                        for (int y = ty; y < ye; ++y) {
                            for (int x = tx; x < xe; ++x) {
                                int dx, dy;
                                if (steps == 1) { dx = ph - 1 - y; dy = x; }
                                else             { dx = y; dy = pw - 1 - x; }
                                dst->data[p][dy * dst_stride + dx] = src->data[p][y * src_stride + x];
                            }
                        }
                    }
                }
            }
        }
        return dst;
    }
    av_frame_free(&dst);
    return nullptr;
}

// ==================== PreviewCache (now LruByteCache<> alias, see lru_cache.h) ==========

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

std::optional<PreviewResult> 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;
    }

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

std::optional<PreviewResult> 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);

    bool is_webp = false;
    if (fmt == "webp") {
        encoder = avcodec_find_encoder(AV_CODEC_ID_WEBP);
        mime = "image/webp";
        is_webp = true;
    } 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 (is_webp) {
        // Fastest compression — quality 80 is visually fine for previews.
        av_opt_set_int(enc_ctx->priv_data, "compression_level", 0, 0);
        av_opt_set_int(enc_ctx->priv_data, "quality", 80, 0);
        av_opt_set(enc_ctx->priv_data, "preset", "photo", 0);
    } else if (fmt == "avif") {
        enc_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
        enc_ctx->bit_rate = 0;
        av_opt_set(enc_ctx->priv_data, "crf", "32", 0);
        av_opt_set(enc_ctx->priv_data, "cpu-used", "8", 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;
    }

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

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

PreviewService::PreviewService(BlobCache& cache, MediaBackendApi& db,
                               std::size_t render_threads, PreviewCache* l2)
    : cache_(cache), db_(db), l2_(l2),
      io_pool_(std::max(4u, std::min(16u, std::thread::hardware_concurrency()))),
      render_sem_(render_threads > 0 ? render_threads
                      : std::max(1u, std::thread::hardware_concurrency())) {}

std::string PreviewService::make_key(const std::string& media_id,
                                      int width, int height, const std::string& fmt,
                                      int t_seconds, int duration) {
    return "preview:" + media_id + ":" + std::to_string(width) + ":" +
           std::to_string(height) + ":" + fmt + ":" +
           std::to_string(t_seconds) + ":" + std::to_string(duration);
}

std::shared_ptr<const BlobValue> PreviewService::cache_get(const std::string& key) {
    return cache_.get(key);
}

std::shared_ptr<const BlobValue> PreviewService::get_or_create_streaming(const MediaRecord& media,
                                                           int width, int height, const std::string& fmt,
                                                           int t_seconds, int duration, std::string& error_out) {
    auto key = make_key(media.id, width, height, fmt, t_seconds, duration);

    // L1 check
    if (auto hit = cache_.get(key)) {
        return hit;
    }

    // L2 check (cluster-shared preview cache)
    if (l2_) {
        if (auto hit = l2_->get(key, cache_)) {
            return hit;
        }
    }

    {
        std::unique_lock<std::mutex> lk(inflight_mutex_);
        auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);

        // Wait for a duplicate in-flight render to finish.
        // Uses predicate form so notifications between unlock/relock are not lost.
        if (inflight_.count(key)) {
            std::cerr << "[PREVIEW] coalescing: waiting for in-flight render of " << key << "\n";
            if (!inflight_cv_.wait_until(lk, deadline,
                    [&]{ return inflight_.count(key) == 0; })) {
                error_out = "timeout waiting for in-flight preview";
                return nullptr;
            }
        }

        // Check caches — another thread/node may have finished rendering.
        if (auto hit = cache_.get(key)) return hit;
        if (l2_) {
            if (auto hit = l2_->get(key, cache_)) return hit;
        }
        inflight_.insert(key);
    }

    struct InflightGuard {
        PreviewService* self;
        std::string key;
        ~InflightGuard() {
            std::lock_guard<std::mutex> lk(self->inflight_mutex_);
            self->inflight_.erase(key);
            self->inflight_cv_.notify_all();
        }
    } inflight_guard{this, key};

    // Acquire render semaphore with timeout — limits concurrent FFmpeg
    // processes to avoid overloading the CPU.
    if (!render_sem_.try_acquire_for(std::chrono::seconds(30))) {
        error_out = "preview render queue full (semaphore timeout)";
        return nullptr;
    }

    struct SemGuard {
        PreviewService* self;
        ~SemGuard() { self->render_sem_.release(); }
    } sem_guard{this};

    // Run FFmpeg inline on the HTTP worker thread.  The semaphore above
    // limits concurrency; io_pool_ handles prefetch I/O independently.
    std::optional<PreviewResult> result;
    try {
        result = ffmpeg_.render_streaming(db_, io_pool_, cache_,
                                          media.id, media.size_bytes, media.media_kind,
                                          width, height, fmt, t_seconds, duration, error_out);
    } catch (const std::exception& e) {
        error_out = std::string("render exception: ") + e.what();
    } catch (...) {
        error_out = "render crashed";
    }

    if (!result.has_value()) {
        return nullptr;
    }

    BlobValue blob;
    blob.data = std::move(result->bytes);
    blob.content_type = std::move(result->mime);
    blob.etag = std::move(result->etag);
    blob.type = BlobType::preview;

    // Store to L2 (cluster-shared) — fragments automatically if > 10 MB
    if (l2_) {
        l2_->put(key, blob, cache_);
    }

    cache_.put(key, std::move(blob));
    return cache_.get(key);
}

// ==================== FFmpegPreviewer::render_streaming ====================

std::optional<PreviewResult> FFmpegPreviewer::render_streaming(MediaBackendApi& db, ThreadPool& io_pool,
                                                               BlobCache& cache,
                                                               const std::string& media_id,
                                                               std::uint64_t media_size, const std::string& media_kind,
                                                               int width, int height, const std::string& fmt, int t_seconds,
                                                               int duration, std::string& error_out) {
    if (media_size == 0) {
        error_out = "empty media data";
        return std::nullopt;
    }

    PrefetchBuffer prefetch_buf;
    prefetch_buf.db         = &db;
    prefetch_buf.io_pool    = &io_pool;
    prefetch_buf.cache      = &cache;
    prefetch_buf.media_id   = media_id;
    prefetch_buf.total_size = media_size;
    prefetch_buf.bulk_prefetch();  // single round-trip for files ≤ 64 MB

    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,
                                               &prefetch_buf, streaming_read_packet, nullptr, streaming_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;
    // Limit probing *before* open so format detection reads less data
    // over cluster I/O.
    format_ctx->probesize = 256 * 1024;
    format_ctx->max_analyze_duration = 1 * AV_TIME_BASE;

    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) { error_out = "no decoder available"; return std::nullopt; }

    AVCodecContext* dec_ctx = avcodec_alloc_context3(decoder);
    if (!dec_ctx) { 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"; return std::nullopt; }

    // Use multi-threaded decoding — significant speedup for H.264/H.265
    dec_ctx->thread_count = 0;  // auto-detect (usually hardware_concurrency)
    dec_ctx->thread_type = FF_THREAD_FRAME | FF_THREAD_SLICE;

    // For JPEG/MJPEG: use lowres to decode at reduced resolution when the
    // target is much smaller than the source.  Avoids decoding a 30 MP
    // photo at full resolution just to scale it down to 1280px.
    if (decoder->id == AV_CODEC_ID_MJPEG || decoder->id == AV_CODEC_ID_JPEG2000) {
        const int src_dim = std::max(stream->codecpar->width, stream->codecpar->height);
        const int tgt_dim = std::max(width, height);
        if (tgt_dim > 0 && src_dim > tgt_dim * 4)
            dec_ctx->lowres = (src_dim > tgt_dim * 8) ? 3 : 2;
        else if (tgt_dim > 0 && src_dim > tgt_dim * 2)
            dec_ctx->lowres = 1;
    }

    rc = avcodec_open2(dec_ctx, decoder, nullptr);
    if (rc < 0) { error_out = "avcodec_open2 decoder failed"; 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; }

    // --- Video clip path ---
    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);

        return render_video_clip(format_ctx, stream_index, dec_ctx,
                                 src_w, src_h, out_w, out_h, sws_vc, scaled_vc, duration, fmt, error_out);
    }

    // --- Still image path (streaming) ---
    auto frame_deleter = [](AVFrame* f) { av_frame_free(&f); };

    AVPacket* packet = av_packet_alloc();
    AVFrame* decoded = av_frame_alloc();
    if (!packet || !decoded) {
        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); };
    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;
    }
    // Flush decoder — some codecs (notably WebP with extended format /
    // VP8X chunks) buffer the single frame until they receive an EOF
    // signal via a NULL packet.
    if (!have_frame) {
        avcodec_send_packet(dec_ctx, nullptr);
        while (avcodec_receive_frame(dec_ctx, decoded) >= 0) {
            have_frame = true;
            break;
        }
    }
    if (!have_frame) {
        if (prefetch_buf.premature_eof)
            error_out = "could not decode a frame: cluster data timed out";
        else
            error_out = "could not decode a frame";
        return std::nullopt;
    }

    bool src_has_alpha = pix_fmt_has_alpha(dec_ctx->pix_fmt);

    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"; 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) { 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);
}

} // namespace mediadb
