#include "CandidateFiles.h"
#include "ParameterContext.h"
#include "PluginProcessor.h"
#include "PluginEditor.h"
#include <juce_audio_utils/juce_audio_utils.h>

namespace deeppluck::presets
{
namespace
{
class AuditionContent final : public juce::Component, private juce::MidiKeyboardState::Listener,
                              private juce::Timer
{
public:
    explicit AuditionContent (const juce::File& root, bool silent)
        : auditionRoot (root), processor (root, true), keyboard (keyboardState, juce::MidiKeyboardComponent::horizontalKeyboard)
    {
        require (readJson (root.getChildFile ("audition-root.json"))["format"].toString() == "DeepPluckDeveloperAuditionRoot",
                 "Use a prepared developer audition root");
        manifest = validateRun (processor.getValueTreeState(), root);
        editor=std::make_unique<DeepPluckAudioProcessorEditor> (processor,
            gui::DeepPluckGuiPreferences::createForTests (root.getChildFile ("GUI.properties")));
        viewport.setViewedComponent (editor.get(), false);
        addAndMakeVisible (viewport); addAndMakeVisible (keyboard);
        for (auto* button : {&devicesButton,&keepButton,&maybeButton,&rejectButton,&fineTuneButton}) addAndMakeVisible (*button);
        addAndMakeVisible (note); note.setTextToShowWhenEmpty ("Review note (Keep is not Factory approval)",juce::Colours::grey);
        addAndMakeVisible (status); status.setText ("Developer audition — isolated files; no Factory approval",juce::dontSendNotification);
        keepButton.onClick=[this]{rate("Keep");}; maybeButton.onClick=[this]{rate("Maybe");}; rejectButton.onClick=[this]{rate("Reject");};
        fineTuneButton.onClick=[this]{recordFineTune();};
        fineTuneButton.setTooltip ("After editing a candidate and using Save As into User, record its origin here.");
        devicesButton.onClick=[this]
        {
            juce::DialogWindow::LaunchOptions options;
            auto* selector=new juce::AudioDeviceSelectorComponent(deviceManager,0,0,2,2,true,false,true,false);
            selector->setSize(600,440); options.content.setOwned(selector);
            options.dialogTitle="Developer audio / MIDI"; options.dialogBackgroundColour=juce::Colours::darkgrey;
            options.escapeKeyTriggersCloseButton=true; options.useNativeTitleBar=true; options.resizable=false;
            options.launchAsync();
        };
        keyboardState.addListener(this);
        if(!silent)
        {
            const auto error=deviceManager.initialise(0,2,nullptr,true);
            if(error.isNotEmpty()) status.setText("Audio device: "+error,juce::dontSendNotification);
            player.setProcessor(&processor); deviceManager.addAudioCallback(&player);
            deviceManager.addMidiInputDeviceCallback({},&player);
        }
        setSize(1450,850); startTimerHz(5);
    }
    ~AuditionContent() override
    {
        stopTimer(); keyboardState.removeListener(this);
        deviceManager.removeMidiInputDeviceCallback({},&player);
        deviceManager.removeAudioCallback(&player); player.setProcessor(nullptr);
        viewport.setViewedComponent(nullptr,false); editor.reset();
    }
    void resized() override
    {
        auto area=getLocalBounds().reduced(6);
        auto row=area.removeFromTop(30);
        devicesButton.setBounds(row.removeFromLeft(110).reduced(2));
        keepButton.setBounds(row.removeFromLeft(70).reduced(2));
        maybeButton.setBounds(row.removeFromLeft(70).reduced(2));
        rejectButton.setBounds(row.removeFromLeft(70).reduced(2));
        fineTuneButton.setBounds(row.removeFromLeft(160).reduced(2)); note.setBounds(row.reduced(2));
        status.setBounds(area.removeFromTop(25)); keyboard.setBounds(area.removeFromBottom(60));
        viewport.setBounds(area);
    }
private:
    void send (juce::MidiMessage message)
    {
        message.setTimeStamp(juce::Time::getMillisecondCounterHiRes()*0.001);
        player.getMidiMessageCollector().addMessageToQueue(message);
    }
    void handleNoteOn (juce::MidiKeyboardState*,int channel,int number,float velocity) override
    { send(juce::MidiMessage::noteOn(channel,number,velocity)); }
    void handleNoteOff (juce::MidiKeyboardState*,int channel,int number,float) override
    { send(juce::MidiMessage::noteOff(channel,number)); }
    juce::var candidateForFile (const juce::File& file) const
    {
        for(const auto& entry:*manifest["entries"].getArray())
            if(containedFile(auditionRoot,entry["path"].toString())==file) return entry;
        return {};
    }
    void timerCallback() override
    {
        const auto display=processor.getPresetDisplayState();
        if(display.stableId!=lastStableId)
        {
            const auto candidate=candidateForFile(display.file);
            if(candidate.isObject()) { origin=candidate; note.clear(); }
            // A Custom sound can be an edited version of the last candidate.
            // Only an explicit candidate load establishes an origin. User-file
            // loads cannot silently become new candidate origins.
            lastStableId=display.stableId;
        }
        const bool candidate=candidateForFile(display.file).isObject();
        keepButton.setEnabled(candidate); maybeButton.setEnabled(candidate); rejectButton.setEnabled(candidate);
        fineTuneButton.setEnabled(display.editable && origin.isObject());
    }
    void rate (const juce::String& rating)
    {
        try
        {
            const auto display=processor.getPresetDisplayState(); const auto candidate=candidateForFile(display.file);
            require(candidate.isObject() && hashFile(display.file)==candidate["sha256"].toString(),"Select an unchanged candidate first");
            auto entry=object(); set(entry,"id",candidate["id"]); set(entry,"sha256",candidate["sha256"]);
            set(entry,"rating",rating); set(entry,"note",note.getText());
            set(entry,"recordedUtc",juce::Time::getCurrentTime().toISO8601(true));
            // Append-only review events preserve history and never overwrite a
            // manually edited ratings.json or another audition session.
            writeJson(auditionRoot.getChildFile("reviews/"+juce::Uuid().toString()+".json"),entry);
            status.setText(candidate["name"].toString()+": "+rating+" recorded — not final approval",juce::dontSendNotification);
        }
        catch(const std::exception& error){status.setText(error.what(),juce::dontSendNotification);}
    }
    void recordFineTune()
    {
        try
        {
            const auto display=processor.getPresetDisplayState();
            require(origin.isObject() && display.editable,"Load origin candidate, edit, Save As in User, then record origin");
            require(hashFile(containedFile(auditionRoot,origin["path"].toString()))==origin["sha256"].toString(),"Origin changed");
            const auto relative=display.file.getRelativePathFrom(auditionRoot).replaceCharacter('\\','/');
            require(relative.startsWith("User/") && containedFile(auditionRoot,relative)==display.file,"Fine tune outside isolated User");
            auto entry=object(); set(entry,"format","DeepPluckFineTuneOrigin"); set(entry,"schemaVersion",1);
            set(entry,"originId",origin["id"]); set(entry,"originSha256",origin["sha256"]);
            set(entry,"name",display.name); set(entry,"path",relative); set(entry,"sha256",hashFile(display.file));
            set(entry,"note",note.getText()); set(entry,"approval","unreviewed");
            set(entry,"recordedUtc",juce::Time::getCurrentTime().toISO8601(true));
            juce::AlertWindow::showOkCancelBox(juce::MessageBoxIconType::QuestionIcon,"Confirm fine-tune origin",
                "Record '"+display.name+"' as a fine tune of '"+origin["name"].toString()+"'?\nCancel if this is not its actual origin.",
                "Record origin","Cancel",this,juce::ModalCallbackFunction::create(
                [safe=juce::Component::SafePointer<AuditionContent>(this),entry](int answer)
                {
                    if(answer!=1 || safe==nullptr) return;
                    try
                    {
                        require(hashFile(containedFile(safe->auditionRoot,entry["path"].toString()))==entry["sha256"].toString(),"Fine tune changed while confirming");
                        writeJson(safe->auditionRoot.getChildFile("fine-tunes/"+juce::Uuid().toString()+".json"),entry);
                        safe->status.setText("Fine-tune origin recorded: "+entry["name"].toString(),juce::dontSendNotification);
                    }
                    catch(const std::exception& error){safe->status.setText(error.what(),juce::dontSendNotification);}
                }));
        }
        catch(const std::exception& error){status.setText(error.what(),juce::dontSendNotification);}
    }
    juce::File auditionRoot;
    DeepPluckAudioProcessor processor;
    juce::AudioDeviceManager deviceManager;
    juce::AudioProcessorPlayer player;
    juce::MidiKeyboardState keyboardState;
    juce::MidiKeyboardComponent keyboard;
    std::unique_ptr<juce::AudioProcessorEditor> editor;
    juce::Viewport viewport;
    juce::TextButton devicesButton{"Audio / MIDI"},keepButton{"Keep"},maybeButton{"Maybe"},rejectButton{"Reject"},fineTuneButton{"Record Fine Tune Origin"};
    juce::TextEditor note;
    juce::Label status;
    juce::var manifest,origin;
    juce::String lastStableId;
};

class AuditionWindow final : public juce::DocumentWindow
{
public:
    AuditionWindow(const juce::File& root,bool silent)
        : DocumentWindow("DeepPluck Developer Audition",juce::Colours::black,allButtons)
    {
        setUsingNativeTitleBar(true); setContentOwned(new AuditionContent(root,silent),true);
        setResizable(true,true); centreWithSize(getWidth(),getHeight()); setVisible(true);
    }
    void closeButtonPressed() override { juce::JUCEApplication::getInstance()->systemRequestedQuit(); }
};
}

class AuditionApplication final : public juce::JUCEApplication
{
public:
    const juce::String getApplicationName() override { return "DeepPluck Developer Audition"; }
    const juce::String getApplicationVersion() override { return DEEPPLUCK_PRESET_TOOL_VERSION; }
    bool moreThanOneInstanceAllowed() override { return true; }
    void initialise(const juce::String& commandLine) override
    {
        try
        {
            const auto args=juce::StringArray::fromTokens(commandLine,true);
            require((args.size()==2 || (args.size()==3 && args[2]=="--silent-smoke")) && args[0]=="--root",
                     "Usage: DeepPluckPresetAudition --root ABSOLUTE_PREPARED_AUDITION_ROOT");
            const auto path=args[1].unquoted(); require(juce::File::isAbsolutePath(path),"Use absolute root");
            const juce::File root(path);
            // Validation checks hashes and forbids reparse paths before opening
            // the processor or audio devices. No default Documents fallback.
            ParameterContext context; juce::ignoreUnused(validateRun(context.state,root));
            const auto allowed=juce::File(DEEPPLUCK_CANDIDATE_OUTPUT_ROOT);
            require(root.isAChildOf(allowed) || root.isAChildOf(juce::File(DEEPPLUCK_CANDIDATE_TEST_ROOT)),"Not a developer root");
            window=std::make_unique<AuditionWindow>(root,args.size()==3);
        }
        catch(const std::exception& error)
        {
            setApplicationReturnValue(1);
            juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::WarningIcon,"Developer audition",error.what(),"Close",nullptr,
                juce::ModalCallbackFunction::create([](int){juce::JUCEApplication::getInstance()->quit();}));
        }
    }
    void shutdown() override { window.reset(); }
    void systemRequestedQuit() override { quit(); }
    void anotherInstanceStarted(const juce::String&) override {}
private:
    std::unique_ptr<AuditionWindow> window;
};
}
START_JUCE_APPLICATION(deeppluck::presets::AuditionApplication)
