#include "CandidateFiles.h"
#include "Presets/DeepPluckFactoryLibrary.h"
#include <cmath>
#include <filesystem>
#include <set>
#include <stdexcept>

namespace deeppluck::presets
{
void require (bool condition, const juce::String& message)
{
    if (! condition) throw std::runtime_error (message.toStdString());
}

juce::var object() { return juce::var (new juce::DynamicObject()); }
void set (juce::var& value, const juce::Identifier& key, const juce::var& data)
{
    require (value.isObject(), "Expected object");
    value.getDynamicObject()->setProperty (key, data);
}

juce::var readJson (const juce::File& file)
{
    require (file.existsAsFile() && file.getSize() <= 1024 * 1024, "JSON missing or exceeds 1 MiB");
    juce::var result;
    require (juce::JSON::parse (file.loadFileAsString(), result).wasOk() && result.isObject(), "Invalid JSON object");
    return result;
}

namespace
{
void rejectLinks (juce::File file)
{
    for (;;)
    {
        require (! file.isSymbolicLink(), "Links/reparse paths are not permitted in developer outputs");
        const auto parent = file.getParentDirectory();
        if (file == parent) break;
        file = parent;
    }
}

bool below (const juce::File& root, const juce::File& file)
{
    return file.getFullPathName().replaceCharacter ('\\', '/').toLowerCase().startsWith (
        root.getFullPathName().replaceCharacter ('\\', '/').toLowerCase() + "/");
}
}

juce::File containedFile (const juce::File& root, const juce::String& relative)
{
    require (relative.isNotEmpty() && ! juce::File::isAbsolutePath (relative), "Expected relative path");
    const auto parts = juce::StringArray::fromTokens (relative.replaceCharacter ('\\', '/'), "/", "");
    for (const auto& part : parts)
        require (part.isNotEmpty() && part != "." && part != ".." && ! part.containsChar (':'), "Unsafe relative path");
    const auto file = root.getChildFile (relative);
    require (below (root, file), "Path escaped root");
    rejectLinks (file);
    return file;
}

void createRunRoot (const juce::File& root)
{
    require (below (juce::File (DEEPPLUCK_CANDIDATE_OUTPUT_ROOT), root)
             || below (juce::File (DEEPPLUCK_CANDIDATE_TEST_ROOT), root),
             "Output must be below the project V0.12.0 artifact root (never Factory/User/system paths)");
    rejectLinks (root);
    require (! root.exists(), "Preserve existing run: choose a new output directory");
    require (root.getParentDirectory().createDirectory().wasOk(), "Cannot create run parent");
    std::error_code error;
    const bool created = std::filesystem::create_directory (
        std::filesystem::path (root.getFullPathName().toWideCharPointer()), error);
    require (created && ! error, "Run directory collision or creation error");
}

void writeNew (const juce::File& file, const juce::String& text)
{
    rejectLinks (file);
    require (! file.exists(), "Refusing to overwrite existing file");
    require (file.getParentDirectory().createDirectory().wasOk(), "Cannot create output directory");
    const auto temporary = file.getSiblingFile (file.getFileName() + ".writing");
    require (! temporary.exists(), "Preserve incomplete output; choose a new run");
    require (temporary.replaceWithText (text, false, false, "\n"), "Temporary write failed");
    require (temporary.moveFileTo (file), "Atomic output publication failed");
}

void writeJson (const juce::File& file, const juce::var& value)
{
    writeNew (file, juce::JSON::toString (value, false));
}

juce::String hashFile (const juce::File& file) { return juce::SHA256 (file).toHexString(); }

void generateRun (const juce::AudioProcessorValueTreeState& state,
                  const std::vector<int>& categories, int count, int startIndex,
                  std::uint64_t seed, const juce::File& root)
{
    require (! categories.empty() && count >= 1 && count <= 100 && startIndex >= 1
             && startIndex <= 1000000 - count + 1, "Invalid candidate batch");
    std::set<int> distinct;
    for (const auto category : categories)
        require (category >= 0 && category < 8 && distinct.insert (category).second, "Invalid/duplicate category");
    createRunRoot (root);
    auto manifest = object();
    set (manifest, "format", "DeepPluckCandidateRun");
    set (manifest, "schemaVersion", 1);
    set (manifest, "profile", profileVersion);
    set (manifest, "seed", seedText (seed));
    set (manifest, "writerVersion", DEEPPLUCK_PRESET_TOOL_VERSION);
    set (manifest, "countPerCategory", count);
    set (manifest, "startIndex", startIndex);
    writeJson (root.getChildFile ("run-plan.json"), manifest);
    juce::Array<juce::var> entries, ratings;
    juce::String error;
    for (const auto category : categories)
    {
        const auto& info = factoryCategories[static_cast<std::size_t> (category)];
        for (int index = startIndex; index < startIndex + count; ++index)
        {
            const auto candidate = generateCandidate (state, category, seed, index);
            const juce::String relative = juce::String ("Candidates/") + info.name + "/"
                                          + candidate.name + ".deeppluckpreset";
            const auto file = containedFile (root, relative);
            const UserPresetData data { candidate.name, "Tonetrax", DEEPPLUCK_PRESET_TOOL_VERSION,
                                        "Developer candidate; not an approved Factory preset.", candidate.values };
            juce::String json;
            require (encodePreset (state, data, json, error, true), error);
            writeNew (file, json);
            auto entry = object();
            set (entry, "id", candidate.id);
            set (entry, "name", candidate.name);
            set (entry, "category", info.id);
            set (entry, "index", index);
            set (entry, "derivedSeed", seedText (candidate.derivedSeed));
            set (entry, "exploration", candidate.exploration);
            set (entry, "path", relative);
            set (entry, "sha256", hashFile (file));
            set (entry, "audioStatus", "not-audited");
            entries.add (entry);
            auto rating = object();
            set (rating, "id", candidate.id);
            set (rating, "sha256", entry["sha256"]);
            set (rating, "rating", "unreviewed");
            set (rating, "note", "");
            ratings.add (rating);
        }
    }
    set (manifest, "entries", entries);
    set (manifest, "complete", true);
    auto review = object();
    set (review, "format", "DeepPluckCandidateRatings");
    set (review, "schemaVersion", 1);
    set (review, "entries", ratings);
    writeJson (root.getChildFile ("ratings.json"), review);
    writeJson (root.getChildFile ("candidate-manifest.json"), manifest);
}

juce::var validateRun (const juce::AudioProcessorValueTreeState& state, const juce::File& root)
{
    rejectLinks (root);
    auto manifest = readJson (root.getChildFile ("candidate-manifest.json"));
    require (manifest["format"].toString() == "DeepPluckCandidateRun"
             && manifest["schemaVersion"].isInt() && static_cast<int> (manifest["schemaVersion"]) == 1
             && manifest["complete"].isBool() && static_cast<bool> (manifest["complete"]), "Incomplete/invalid run");
    const auto* entries = manifest["entries"].getArray();
    require (entries != nullptr && ! entries->isEmpty() && entries->size() <= 800, "Invalid candidate count");
    const auto seedString = manifest["seed"].toString();
    require (manifest["profile"].toString() == profileVersion
             && seedString.length() == 18 && seedString.startsWith ("0x")
             && seedString.substring (2).containsOnly ("0123456789abcdef"), "Invalid profile/seed");
    const auto seed = static_cast<std::uint64_t> (seedString.substring (2).getHexValue64());
    const auto count = static_cast<int> (manifest["countPerCategory"]);
    const auto start = static_cast<int> (manifest["startIndex"]);
    require (manifest["countPerCategory"].isInt() && manifest["startIndex"].isInt()
             && count >= 1 && count <= 100 && start >= 1 && start <= 1000000 - count + 1,
             "Invalid batch dimensions");
    std::array<int,8> categoryCounts {};
    std::set<juce::String> ids, paths;
    const auto defaults = captureParameterDefaults (state);
    juce::String error;
    for (const auto& entry : *entries)
    {
        require (entry.isObject() && ids.insert (entry["id"].toString()).second
                 && paths.insert (entry["path"].toString().toLowerCase()).second, "Duplicate/invalid candidate entry");
        const auto file = containedFile (root, entry["path"].toString());
        require (hashFile (file) == entry["sha256"].toString(), "Candidate hash mismatch");
        int category = -1;
        for (std::size_t i = 0; i < factoryCategories.size(); ++i)
            if (entry["category"].toString() == factoryCategories[i].id) category = static_cast<int> (i);
        const auto index = static_cast<int> (entry["index"]);
        require (category >= 0 && entry["index"].isInt() && index >= start && index < start + count,
                 "Candidate category/index mismatch");
        const auto expected = generateCandidate (state, category, seed, index);
        const auto expectedPath = juce::String ("Candidates/") + factoryCategories[static_cast<std::size_t> (category)].name
                                + "/" + expected.name + ".deeppluckpreset";
        require (entry["id"].toString() == expected.id && entry["name"].toString() == expected.name
                 && entry["path"].toString() == expectedPath && entry["derivedSeed"].toString() == seedText (expected.derivedSeed)
                 && entry["exploration"].isBool() && static_cast<bool> (entry["exploration"]) == expected.exploration,
                 "Candidate provenance mismatch");
        UserPresetData expectedData { expected.name, "Tonetrax", manifest["writerVersion"].toString(),
                                      "Developer candidate; not an approved Factory preset.", expected.values };
        juce::String expectedJson;
        require (encodePreset (state, expectedData, expectedJson, error, true), error);
        require (file.loadFileAsString() == expectedJson, "Candidate does not reproduce from its recorded seed/profile");
        ++categoryCounts[static_cast<std::size_t> (category)];
        const auto parsed = readJson (file);
        require (parsed["parameters"].isObject()
                 && parsed["parameters"].getDynamicObject()->getProperties().size() == 62, "Expected all 62 values");
        UserPresetData data;
        require (decodePreset (state, defaults, file.loadFileAsString(), data, error), error);
        require (data.name == entry["name"].toString(), "Candidate name mismatch");
        require (std::abs (data.values[0] + 3.0f) < 0.0001f && data.values[1] == 440.0f, "Output/Tune must not be randomized");
        for (std::size_t i = 0; i < data.values.size(); ++i)
        {
            const auto raw = parsed["parameters"][allParameterIds[i]];
            require (raw.isInt() || raw.isInt64() || raw.isDouble(), "Generated parameter must be numeric");
            const auto number = static_cast<double> (raw);
            const auto* parameter = state.getParameter (allParameterIds[i]);
            // JSON prints decimal doubles; the frozen raw contract is float.
            // Compare in that domain (e.g. the printed 0.001f endpoint).
            const auto rawFloat = static_cast<float> (number);
            require (std::isfinite (number) && rawFloat >= parameter->getNormalisableRange().start
                     && rawFloat <= parameter->getNormalisableRange().end,
                     "Generated value outside host range: " + juce::String (allParameterIds[i]));
            if (dynamic_cast<const juce::AudioParameterBool*> (parameter) != nullptr
                || dynamic_cast<const juce::AudioParameterInt*> (parameter) != nullptr
                || dynamic_cast<const juce::AudioParameterChoice*> (parameter) != nullptr)
                require (number == std::round (number), "Non-integral discrete parameter");
            require (std::abs (parameter->convertTo0to1 (static_cast<float> (number))
                               - parameter->convertTo0to1 (data.values[i])) <= 0.000001f,
                     "Generated value changed during load");
        }
    }
    for (const auto categoryCount : categoryCounts)
        require (categoryCount == 0 || categoryCount == count, "Incomplete category");
    return manifest;
}

void prepareAudition (const juce::AudioProcessorValueTreeState& state,
                      const juce::File& input, const juce::File& output)
{
    const auto manifest = validateRun (state, input);
    createRunRoot (output);
    auto pack = object();
    set (pack, "format", "JustALilPluckPack"); set (pack, "schemaVersion", 1);
    set (pack, "packName", "Developer Candidates — NOT Factory");
    writeJson (output.getChildFile ("Candidates/pack.json"), pack);
    for (const auto& entry : *manifest["entries"].getArray())
    {
        const auto relative = entry["path"].toString();
        const auto source = containedFile (input, relative);
        writeNew (containedFile (output, relative), source.loadFileAsString());
    }
    require (output.getChildFile ("User").createDirectory().wasOk(), "Cannot create isolated User directory");
    auto receipt = object();
    set (receipt, "format", "DeepPluckDeveloperAuditionRoot");
    set (receipt, "schemaVersion", 1);
    set (receipt, "sourceManifestSha256", hashFile (input.getChildFile ("candidate-manifest.json")));
    writeJson (output.getChildFile ("candidate-manifest.json"), manifest);
    writeNew (output.getChildFile ("ratings.json"), input.getChildFile ("ratings.json").loadFileAsString());
    writeJson (output.getChildFile ("audition-root.json"), receipt);
}

void stagePromotion (const juce::AudioProcessorValueTreeState& state,
                     const juce::File& reviewedRoot, const juce::File& newStagingRoot)
{
    const auto manifestFile=containedFile(reviewedRoot,"catalog.json");
    const auto manifest=readJson(manifestFile);
    FactoryLibrary library;
    require(validateFactoryLibrary(state,juce::JSON::toString(manifest),[&](const auto& relative)
        { return containedFile(reviewedRoot,relative).loadFileAsString(); },true,library),library.error);
    createRunRoot(newStagingRoot);
    const auto copy=[&](const juce::String& relative)
    {
        const auto source=containedFile(reviewedRoot,relative), target=containedFile(newStagingRoot,relative);
        require(!target.exists() && target.getParentDirectory().createDirectory().wasOk(),"Staging target collision");
        require(source.copyFileTo(target) && hashFile(source)==hashFile(target),"Promotion must be byte-identical");
    };
    for(const auto& entry:library.entries) copy(entry.relativePath);
    copy("catalog.json");
    // Deliberately does not install into Presets/Factory. The approved staging
    // tree is reviewed and committed separately in Phase B.
}
}
