#include "core/reclaim/prune_reconcile.h"

#include <unordered_set>

// prune_reconcile implementation — the one set-algebra computation, kept trivially
// auditable: build the referenced and owned lookup sets, then walk `present` once,
// keeping a path iff it is owned AND not referenced. Walking `present` (not owned)
// gives the ∩-present clause for free and yields output in folder-enumeration order.

namespace reasampler::reclaim {

std::vector<std::string> pruneOrphans(const std::vector<std::string>& present,
                                      const std::vector<std::string>& referenced,
                                      const std::vector<std::string>& owned) {
    // Exact-string membership — the model's canonical relative-path comparison
    // (Sample.relativePath / OriginLedger::contains). std::string hashes/compares
    // byte-for-byte, so no normalization creeps in.
    const std::unordered_set<std::string> referencedSet(referenced.begin(),
                                                         referenced.end());
    const std::unordered_set<std::string> ownedSet(owned.begin(), owned.end());

    std::vector<std::string> orphans;
    std::unordered_set<std::string> emitted;  // de-dup repeated spellings in `present`

    for (const std::string& path : present) {
        // (owned ∩ present) − referenced: present is the walk; owned and !referenced
        // are the two membership tests; emitted guards against a duplicate `present`.
        if (ownedSet.count(path) == 0) continue;         // not our leaving — skip
        if (referencedSet.count(path) != 0) continue;    // some bank references it
        if (!emitted.insert(path).second) continue;      // already emitted
        orphans.push_back(path);
    }
    return orphans;
}

std::vector<std::string> mergeReferenced(const std::vector<std::string>& primary,
                                         const std::vector<std::string>& extra) {
    std::vector<std::string> merged;
    merged.reserve(primary.size() + extra.size());
    std::unordered_set<std::string> seen;
    for (const std::string& p : primary) {
        if (seen.insert(p).second) merged.push_back(p);
    }
    for (const std::string& p : extra) {
        if (seen.insert(p).second) merged.push_back(p);
    }
    return merged;
}

PruneReport buildPruneReport(
    const std::vector<std::string>& orphans,
    const std::unordered_map<std::string, std::uint64_t>& sizeByPath,
    std::size_t displayCap) {
    PruneReport report;
    report.count = orphans.size();
    for (const std::string& o : orphans) {
        // Sum EVERY orphan's bytes (the exact reclaimable total), not just the displayed
        // ones. A path with no stat'd size contributes 0 — never dropped, never negative.
        const auto it = sizeByPath.find(o);
        report.totalBytes += (it != sizeByPath.end()) ? it->second : 0;
        // Display list is capped (0 = uncapped). count stays exact above regardless.
        if (displayCap == 0 || report.orphans.size() < displayCap)
            report.orphans.push_back(o);
    }
    report.truncated = report.orphans.size() < report.count;
    return report;
}

std::vector<std::string> pruneDeletePlan(const std::vector<std::string>& confirmed,
                                         const std::vector<std::string>& freshOrphans) {
    // fresh is a pure-core output (referenced/hand-dropped already excluded), so keeping a
    // confirmed path iff it is still a fresh orphan can never re-admit an unsafe file. Walk
    // `confirmed` to preserve the confirm's listing order; emitted de-dups repeats.
    const std::unordered_set<std::string> freshSet(freshOrphans.begin(), freshOrphans.end());

    std::vector<std::string> plan;
    std::unordered_set<std::string> emitted;
    for (const std::string& path : confirmed) {
        if (freshSet.count(path) == 0) continue;    // stale: vanished / now-referenced -> skip
        if (!emitted.insert(path).second) continue; // already emitted
        plan.push_back(path);
    }
    return plan;
}

} // namespace reasampler::reclaim
