// sample_usage.cpp — see sample_usage.h. Pure: standard library only.

#include "core/wire/sample_usage.h"

#include <cctype>

#include "core/wire/wire.h"

namespace reasampler::wire {

namespace {

constexpr const char* kMagic = "rsusage1";

using wire::putField;
using Cursor = wire::Cursor;

}  // namespace

std::string encodeUsageRecord(const UsageRecord& rec) {
    std::string out = kMagic;
    putField(out, rec.trackGuid);
    putField(out, rec.ownerNonce);
    putField(out, rec.unioned ? "1" : "0");
    putField(out, std::to_string(rec.holds.size()));
    for (const UsageHold& h : rec.holds) {
        putField(out, h.sampleId);
        putField(out, h.relativePath);
    }
    return out;
}

std::optional<UsageRecord> decodeUsageRecord(const std::string& wire) {
    Cursor c(wire);
    if (!c.literal(kMagic)) return std::nullopt;
    UsageRecord rec;
    if (!c.field(rec.trackGuid)) return std::nullopt;
    if (!c.field(rec.ownerNonce)) return std::nullopt;
    std::string unionedField;
    if (!c.field(unionedField)) return std::nullopt;
    if (unionedField == "1") rec.unioned = true;
    else if (unionedField == "0") rec.unioned = false;
    else return std::nullopt;  // anything else is corruption -> reject whole
    std::size_t count = 0;
    if (!c.fieldSizeT(count)) return std::nullopt;
    // Each hold needs at least 4 wire bytes ("0:0:"), so a count past wire.size()/4 is
    // provably bogus — reject before looping rather than iterating a crafted huge count.
    if (count > wire.size() / 4u + 1u) return std::nullopt;
    rec.holds.reserve(count);
    for (std::size_t i = 0; i < count; ++i) {
        UsageHold h;
        if (!c.field(h.sampleId)) return std::nullopt;
        if (!c.field(h.relativePath)) return std::nullopt;
        rec.holds.push_back(std::move(h));
    }
    if (!c.ok() || !c.atEnd()) return std::nullopt;  // trailing garbage -> reject whole
    return rec;
}

UsagePublishPlan planUsagePublish(const std::optional<std::string>& existing,
                                  const UsageRecord& mine) {
    UsagePublishPlan plan;
    UsageRecord cleanMine = mine;
    cleanMine.unioned = false;
    plan.wire = encodeUsageRecord(cleanMine);

    if (!existing || existing->empty()) {
        // NOTE (>16 MB gap): readReasamplerExtState returning nullopt for a value
        // larger than 16 MB is indistinguishable from "absent" here, so that narrow
        // case also takes this fresh-write branch rather than remint.
        return plan;  // fresh key — write mine
    }

    const std::optional<UsageRecord> theirs = decodeUsageRecord(*existing);
    if (!theirs) {
        // Corrupt value under my key: remint rather than overwrite. Overwriting
        // would clear the prune-side abort currently protecting a same-key
        // sibling's (possibly unprotected) holds; leaving the corrupt key in
        // place keeps that abort firing until the sibling republishes.
        plan.remint = true;
        return plan;
    }

    const bool nonceMatch =
        !mine.ownerNonce.empty() && theirs->ownerNonce == mine.ownerNonce;

    if (nonceMatch && !theirs->unioned) {
        // Exactly this incarnation wrote the key last and it was never unioned
        // by another writer — content is provably all mine.
        if (plan.wire == *existing) plan.skipWrite = true;  // idle reload tick
        return plan;
    }

    if (theirs->trackGuid == mine.trackGuid || (nonceMatch && theirs->unioned)) {
        // Same-track sibling, my own last-session record, or an already-unioned
        // record — no hold in it may be dropped. Union, existing-first, de-duped,
        // poisoned unioned=true so a future clean replace can never drop it.
        UsageRecord merged;
        merged.trackGuid = mine.trackGuid;
        merged.ownerNonce = mine.ownerNonce;
        merged.unioned = true;
        merged.holds = theirs->holds;
        for (const UsageHold& h : mine.holds) {
            bool dup = false;
            for (const UsageHold& e : merged.holds) {
                if (e == h) { dup = true; break; }
            }
            if (!dup) merged.holds.push_back(h);
        }
        if (theirs->unioned && merged.holds == theirs->holds) {
            // Already poisoned and the union adds nothing -> the write would only
            // flip ownerNonce; skip. A false->true unioned flip is NEVER skipped.
            plan.skipWrite = true;
        }
        plan.wire = encodeUsageRecord(merged);
        return plan;
    }

    // Foreign value from another track: a cross-track copy or move. Fresh
    // identity; never overwrite the other's record.
    plan.remint = true;
    return plan;
}

namespace {

// The liveness rule, in one place so the path fold and the attribution fold can
// never disagree about which records counted. `protectAll` is the caller's
// zero-identified net (see usageHeldPaths).
bool recordCounts(const UsageRecord& rec,
                  const std::unordered_set<std::string>& liveTrackGuids,
                  bool anyInstanceLive, bool protectAll) {
    if (protectAll) return true;
    if (rec.trackGuid.empty()) return anyInstanceLive;
    return liveTrackGuids.count(rec.trackGuid) != 0;
}

}  // namespace

std::vector<std::string> usageHeldPaths(
    const std::vector<UsageRecord>& records,
    const std::unordered_set<std::string>& liveTrackGuids,
    bool anyInstanceLive) {
    std::vector<std::string> out;
    std::unordered_set<std::string> seen;
    // FAIL-SAFE NET: records exist but not one instance was identified live anywhere —
    // indistinguishable from an identity-matcher failure, so protect EVERY record's
    // paths rather than none (zero-identified must never degrade toward delete).
    const bool protectAll = !records.empty() && !anyInstanceLive;
    for (const UsageRecord& rec : records) {
        if (!recordCounts(rec, liveTrackGuids, anyInstanceLive, protectAll)) continue;
        for (const UsageHold& h : rec.holds) {
            if (h.relativePath.empty()) continue;
            if (seen.insert(h.relativePath).second) out.push_back(h.relativePath);
        }
    }
    return out;
}

UsageFoldResult foldUsageRecords(
    const std::vector<DecodedUsage>& decoded,
    const std::unordered_set<std::string>& liveTrackGuids,
    bool anyInstanceLive) {
    UsageFoldResult result;
    for (const DecodedUsage& entry : decoded) {
        if (entry.record) continue;
        // Present-but-unreadable record: it may protect anything, so halt.
        result.abortPrune = true;
        result.offendingKeys.push_back(entry.key);
    }
    if (result.abortPrune) {
        // Belt-and-braces: return the protect-all set (every readable record's
        // paths, bypassing the liveness filter) so the fail-safe holds even if a
        // future caller forgets to check abortPrune first. `counted` stays empty —
        // attribution is exactly what an unreadable record makes unknowable.
        std::unordered_set<std::string> seen;
        for (const DecodedUsage& entry : decoded) {
            if (!entry.record) continue;
            for (const UsageHold& h : entry.record->holds) {
                if (h.relativePath.empty()) continue;
                if (seen.insert(h.relativePath).second)
                    result.heldPaths.push_back(h.relativePath);
            }
        }
        return result;
    }

    std::vector<UsageRecord> records;
    records.reserve(decoded.size());
    for (const DecodedUsage& entry : decoded) records.push_back(*entry.record);
    result.heldPaths = usageHeldPaths(records, liveTrackGuids, anyInstanceLive);

    const bool protectAll = !records.empty() && !anyInstanceLive;
    for (const DecodedUsage& entry : decoded) {
        if (!recordCounts(*entry.record, liveTrackGuids, anyInstanceLive, protectAll))
            continue;
        result.counted.push_back(CountedUsage{entry.key, *entry.record});
    }
    return result;
}

std::string toUpperAscii(const std::string& s) {
    std::string out = s;
    for (char& c : out)
        c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
    return out;
}

bool identityMatches(const std::string& identity, const std::string& uidHexUpper,
                     const std::string& nameUpper,
                     const std::string& outputNameUpper) {
    if (identity.empty()) return false;
    const std::string up = toUpperAscii(identity);
    // Class-UID byte-order in fx_ident is unverified on Windows COM layout,
    // hence the two name fallbacks below (see header for the protect-all net).
    if (!uidHexUpper.empty() && up.find(uidHexUpper) != std::string::npos) return true;
    if (!outputNameUpper.empty() && up.find(outputNameUpper) != std::string::npos)
        return true;
    return !nameUpper.empty() && up.find(nameUpper) != std::string::npos;
}

}  // namespace reasampler::wire
