#include "core/model/provenance.h"

#include <cstdio>

#include "core/wire/wire.h"

// provenance implementation — pure, self-contained encoding.
//
// Fingerprint grammar: magic "rsprov1" + fixed-order length-prefixed fields
// (<len>':'<bytes>), so a value containing the separator can never shift the
// parse. Numbers render as decimal/%.17g text before prefixing (same %.17g the
// bank model uses, so doubles round-trip bit-for-bit). Trailing fields: the
// track-GUID count + that many GUIDs, then the folded fxChainIdentity — itself
// length-prefixed per entry field, so it nests safely as one more field. Any
// deviation (bad magic, short read, bad number) -> parseFingerprint returns
// nullopt.

namespace reasampler::model {

bool CaptureRecipe::operator==(const CaptureRecipe& o) const {
    return scope == o.scope && sourceMode == o.sourceMode &&
           startSeconds == o.startSeconds && endSeconds == o.endSeconds &&
           tailMode == o.tailMode && tailMs == o.tailMs &&
           sampleRate == o.sampleRate && channelCount == o.channelCount &&
           trackGuids == o.trackGuids && fxChainIdentity == o.fxChainIdentity;
}

namespace {

constexpr const char* kMagic = "rsprov1";

// The shared core/wire codec carries the field grammar + range-checked fieldInt.
// Only the %.17g double rendering stays local — this writer's convention, shared
// with the bank model's JSON doubles.
using wire::putField;
using Cursor = wire::Cursor;

std::string dblToStr(double v) {
    char buf[32];
    std::snprintf(buf, sizeof(buf), "%.17g", v);
    return buf;
}

}  // namespace

std::string fxChainIdentity(const std::vector<FxIdentityEntry>& entries) {
    std::string out;
    // Count first, then each entry's three fields length-prefixed. Order is part of
    // identity (chain order matters), so we emit in the given vector order.
    putField(out, std::to_string(entries.size()));
    for (const FxIdentityEntry& e : entries) {
        putField(out, e.name);
        putField(out, e.guid);
        putField(out, e.enabled ? "1" : "0");
    }
    return out;
}

std::string combineChainIdentities(const std::vector<std::string>& perTrack) {
    std::string out;
    putField(out, std::to_string(perTrack.size()));
    for (const std::string& id : perTrack) putField(out, id);
    return out;
}

std::string buildFingerprint(const CaptureRecipe& r) {
    std::string out(kMagic);
    putField(out, std::to_string(static_cast<int>(r.scope)));
    putField(out, std::to_string(r.sourceMode));
    putField(out, dblToStr(r.startSeconds));
    putField(out, dblToStr(r.endSeconds));
    putField(out, std::to_string(r.tailMode));
    putField(out, dblToStr(r.tailMs));
    putField(out, std::to_string(r.sampleRate));
    putField(out, std::to_string(r.channelCount));
    putField(out, std::to_string(r.trackGuids.size()));
    for (const std::string& g : r.trackGuids) putField(out, g);
    putField(out, r.fxChainIdentity);
    return out;
}

std::optional<CaptureRecipe> parseFingerprint(const std::string& fingerprint) {
    Cursor c(fingerprint);
    if (!c.literal(kMagic)) return std::nullopt;

    CaptureRecipe r;
    int scopeInt = 0;
    if (!c.fieldInt(scopeInt)) return std::nullopt;
    if (scopeInt != static_cast<int>(ProvenanceScope::Item) &&
        scopeInt != static_cast<int>(ProvenanceScope::Track))
        return std::nullopt;
    r.scope = static_cast<ProvenanceScope>(scopeInt);

    if (!c.fieldInt(r.sourceMode)) return std::nullopt;
    if (!c.fieldDouble(r.startSeconds)) return std::nullopt;
    if (!c.fieldDouble(r.endSeconds)) return std::nullopt;
    if (!c.fieldInt(r.tailMode)) return std::nullopt;
    if (!c.fieldDouble(r.tailMs)) return std::nullopt;
    if (!c.fieldInt(r.sampleRate)) return std::nullopt;
    if (!c.fieldInt(r.channelCount)) return std::nullopt;

    std::size_t guidCount = 0;
    if (!c.fieldSizeT(guidCount)) return std::nullopt;
    // Each GUID field costs at least 2 wire bytes ("0:"), so a count past size/2 is
    // provably bogus — reject BEFORE the reserve, so a corrupt/crafted persisted
    // fingerprint can never drive reserve(huge) into std::length_error / bad_alloc.
    if (guidCount > fingerprint.size() / 2u + 1u) return std::nullopt;
    r.trackGuids.reserve(guidCount);
    for (std::size_t i = 0; i < guidCount; ++i) {
        std::string g;
        if (!c.field(g)) return std::nullopt;
        r.trackGuids.push_back(std::move(g));
    }

    if (!c.field(r.fxChainIdentity)) return std::nullopt;

    // Trailing garbage means the string was not produced by our writer -> reject,
    // so a corrupt/extended blob never silently drives a partial re-capture.
    if (!c.ok() || !c.atEnd()) return std::nullopt;
    return r;
}

std::optional<std::string> detectParent(
    const std::vector<std::string>& sourceItemFiles,
    const std::vector<BankFileRef>& bankFiles) {
    if (sourceItemFiles.empty()) return std::nullopt;

    std::optional<std::string> parent;  // the single bank sample all sources point at
    for (const std::string& src : sourceItemFiles) {
        const std::string* matchedId = nullptr;
        for (const BankFileRef& ref : bankFiles) {
            if (!ref.absolutePath.empty() && ref.absolutePath == src) {
                matchedId = &ref.sampleId;
                break;
            }
        }
        if (matchedId == nullptr)
            return std::nullopt;  // a source item is NOT a bank file -> not a resample

        if (!parent) {
            parent = *matchedId;
        } else if (*parent != *matchedId) {
            return std::nullopt;  // sources span >1 bank sample -> ambiguous, no parent
        }
    }
    return parent;
}

}  // namespace reasampler::model
