package io.xpipe.app.hub.comp;

import io.xpipe.app.ext.DataStore;
import io.xpipe.app.ext.DataStoreProvider;
import io.xpipe.app.ext.ShellStore;
import io.xpipe.app.ext.ValidatableStore;
import io.xpipe.app.ext.ValidationException;
import io.xpipe.app.hub.action.impl.OpenHubMenuLeafProvider;
import io.xpipe.app.issue.ErrorEventFactory;
import io.xpipe.app.platform.SimpleValidator;
import io.xpipe.app.platform.Validator;
import io.xpipe.app.storage.DataStorage;
import io.xpipe.app.storage.DataStoreCategory;
import io.xpipe.app.storage.DataStoreEntry;
import io.xpipe.app.util.*;

import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.beans.value.ObservableBooleanValue;
import javafx.beans.value.ObservableValue;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.experimental.FieldDefaults;

import java.util.List;
import java.util.UUID;
import java.util.function.Predicate;

@FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE)
@Getter
public class StoreCreationModel {

    ObjectProperty<DataStore> initialStore = new SimpleObjectProperty<>();
    Property<DataStoreProvider> provider;
    ObjectProperty<DataStore> store;
    Predicate<DataStoreProvider> filter;
    BooleanProperty busy = new SimpleBooleanProperty();
    Property<Validator> validator = new SimpleObjectProperty<>(new SimpleValidator());
    BooleanProperty finished = new SimpleBooleanProperty();
    BooleanProperty showing = new SimpleBooleanProperty();
    ObservableValue<DataStoreEntry> entry;
    BooleanProperty skippable = new SimpleBooleanProperty();
    BooleanProperty connectable = new SimpleBooleanProperty();
    StringProperty name;
    DataStoreEntry existingEntry;
    List<DataStore> existingDependencies;
    boolean staticDisplay;
    StoreCreationConsumer consumer;
    ObservableBooleanValue syncable;
    UUID uuid;

    public StoreCreationModel(
            Property<DataStoreProvider> provider,
            ObjectProperty<DataStore> store,
            Predicate<DataStoreProvider> filter,
            String initialName,
            DataStoreEntry existingEntry,
            boolean staticDisplay,
            StoreCreationConsumer consumer) {
        this.provider = provider;
        this.store = store;
        this.filter = filter;
        this.name = new SimpleStringProperty(initialName != null && !initialName.isEmpty() ? initialName : null);
        this.existingEntry = existingEntry;
        this.existingDependencies = existingEntry != null
                ? DataStorage.get().getDependencies(existingEntry).stream()
                        .<DataStore>map(ref -> ref.getStore())
                        .toList()
                : null;
        this.staticDisplay = staticDisplay;
        this.consumer = consumer;
        this.uuid = existingEntry != null ? existingEntry.getUuid() : UUID.randomUUID();

        this.provider.addListener((c, o, n) -> {
            store.unbind();
            store.setValue(null);
            if (n != null) {
                store.setValue(n.defaultStore(
                        getTargetCategory(existingEntry != null ? existingEntry.getCategoryUuid() : null)));
            }
        });

        this.provider.subscribe((n) -> {
            if (n != null) {
                connectable.setValue(n.canConnectDuringCreation());
            }
        });

        this.validator.addListener((observable, oldValue, newValue) -> {
            Platform.runLater(() -> {
                newValue.validate();
            });
        });

        this.entry = Bindings.createObjectBinding(
                () -> {
                    if (name.getValue() == null
                            || store.getValue() == null
                            || name.getValue().isBlank()) {
                        return null;
                    }

                    var initial = DataStoreEntry.createNew(
                            uuid, DataStorage.get().getSelectedCategory().getUuid(), name.getValue(), store.getValue());
                    var entryRef = existingEntry != null
                            ? existingEntry
                            : DataStorage.get().getDefaultDisplayParent(initial).orElse(initial);
                    var targetCategory = getTargetCategory(entryRef.getCategoryUuid());
                    return DataStoreEntry.createNew(uuid, targetCategory.getUuid(), name.getValue(), store.getValue());
                },
                name,
                store);

        this.syncable = Bindings.createBooleanBinding(
                () -> {
                    var targetCategory = getTargetCategory(
                            existingEntry != null
                                    ? existingEntry.getCategoryUuid()
                                    : StoreViewState.get()
                                            .getActiveCategory()
                                            .getValue()
                                            .getCategory()
                                            .getUuid());
                    var entry = DataStoreEntry.createNew(
                            UUID.randomUUID(), targetCategory.getUuid(), "Temp", store.getValue());
                    return DataStorage.get().shouldSync(entry);
                },
                store,
                StoreViewState.get().getActiveCategory());

        skippable.bind(Bindings.createBooleanBinding(
                () -> {
                    if (entry.getValue() != null
                            && store.get().isComplete()
                            && store.get() instanceof ValidatableStore) {
                        if (existingEntry != null) {
                            return !existingEntry.isFreeze()
                                    || !existingEntry.getName().equals(name.getValue());
                        } else {
                            return true;
                        }
                    } else {
                        return false;
                    }
                },
                store,
                name));
    }

    private DataStoreCategory getTargetCategory(UUID baseCategory) {
        var targetCategory = baseCategory != null
                ? baseCategory
                : DataStorage.get().getSelectedCategory().getUuid();
        var rootCategory = DataStorage.get()
                .getRootCategory(DataStorage.get()
                        .getStoreCategoryIfPresent(targetCategory)
                        .orElse(DataStorage.get().getAllConnectionsCategory()));

        // Don't put it in the wrong root category
        if ((provider.getValue().getCreationCategory() != null
                && !provider.getValue().getCreationCategory().getCategory().equals(rootCategory.getUuid()))) {
            targetCategory = provider.getValue().getCreationCategory().getCategory();
        }

        // Don't use the all connections category
        if (targetCategory.equals(DataStorage.get().getAllConnectionsCategory().getUuid())) {
            targetCategory = DataStorage.get().getDefaultConnectionsCategory().getUuid();
        }

        // Don't use the all scripts category
        if (targetCategory.equals(DataStorage.get().getAllScriptsCategory().getUuid())) {
            targetCategory = DataStorage.CUSTOM_SCRIPTS_CATEGORY_UUID;
        }

        // Don't use the all identities category
        if (targetCategory.equals(DataStorage.get().getAllIdentitiesCategory().getUuid())) {
            targetCategory = DataStorage.LOCAL_IDENTITIES_CATEGORY_UUID;
        }

        // Custom category stuff
        targetCategory = provider.getValue().getTargetCategory(store.getValue(), targetCategory);
        return DataStorage.get().getStoreCategoryIfPresent(targetCategory).orElseThrow();
    }

    ObservableBooleanValue canConnect() {
        return connectable
                .not()
                .or(Bindings.createBooleanBinding(
                        () -> {
                            return store.getValue() == null || !store.getValue().isComplete();
                        },
                        store));
    }

    boolean isQuickConnect() {
        return existingEntry != null && existingEntry.getUuid().equals(StoreQuickConnect.STORE_ID);
    }

    void connect() {
        var temp = entry.getValue() != null ? entry.getValue() : DataStoreEntry.createTempWrapper(store.getValue());
        var action = OpenHubMenuLeafProvider.Action.builder().ref(temp.ref()).build();
        action.executeAsync();
    }

    boolean hasBeenModified() {
        if (initialStore.getValue() == null) {
            return true;
        }

        var eq = initialStore.getValue().equals(store.getValue());
        return !eq;
    }

    boolean wasChanged() {
        if (existingEntry == null) {
            return true;
        }

        if (!existingEntry.getStore().equals(store.getValue())) {
            return true;
        }

        var newDependencies = DataStorage.get().getDependencies(entry.getValue()).stream()
                .map(ref -> ref.getStore())
                .toList();
        if (!existingDependencies.equals(newDependencies)) {
            return true;
        }

        return false;
    }

    void finish() {
        if (finished.get()) {
            return;
        }

        if (store.getValue() == null) {
            return;
        }

        if (!validator.getValue().validate()) {
            var msg = validator
                    .getValue()
                    .getValidationResult()
                    .getMessages()
                    .getFirst()
                    .getText();
            ErrorEventFactory.fromMessage(msg).expected().handle();
            return;
        }

        // We didn't change anything
        if (store.getValue().isComplete() && !wasChanged() && !isQuickConnect()) {
            commit(false);
            return;
        }

        var currentEntry = entry.getValue();
        if (currentEntry == null) {
            return;
        }

        ThreadHelper.runAsync(() -> {
            if (busy.get()) {
                return;
            }

            try (var ignored = new BooleanScope(busy).exclusive().start()) {
                DataStorage.get().addStoreEntryInProgress(currentEntry);
                validate();

                // If the validation happens while the dialog is minimized, remove queue entry
                var queueEntry = StoreCreationQueueEntry.findExisting(existingEntry);
                if (queueEntry.isPresent()) {
                    queueEntry.get().hide();
                }

                // The dialog might be completely closed, then discard changes
                if (showing.get() || queueEntry.isPresent()) {
                    commit(true);
                }
            } catch (Throwable ex) {
                if (ex instanceof ValidationException) {
                    ErrorEventFactory.expected(ex);
                } else if (ex instanceof StackOverflowError) {
                    // Cycles in connection graphs can fail hard but are expected
                    ErrorEventFactory.expected(ex);
                }

                var event = ErrorEventFactory.fromThrowable(ex);
                var queueEntry = StoreCreationQueueEntry.findExisting(existingEntry);
                if (queueEntry.isEmpty() && !showing.get()) {
                    event.omit();
                }
                event.handle();
            } finally {
                if (DataStorage.get() != null) {
                    DataStorage.get().removeStoreEntryInProgress(currentEntry);
                }
            }
        });
    }

    private void validate() throws Throwable {
        var s = entry.getValue().getStore();
        if (s == null) {
            return;
        }

        s.checkComplete();

        if (s instanceof ShellStore ss) {
            // Start session for later
            ss.getOrStartSession();
        } else if (s instanceof ValidatableStore vs) {
            vs.validate();
        }
    }

    void showDocs() {
        Hyperlinks.open(provider.getValue().getHelpLink().getLink());
    }

    ObservableBooleanValue canShowDocs() {
        var disable = Bindings.createBooleanBinding(
                () -> {
                    return provider.getValue() == null || provider.getValue().getHelpLink() == null;
                },
                provider);
        return disable;
    }

    void commit(boolean validated) {
        if (finished.get()) {
            return;
        }

        finished.setValue(true);
        consumer.consume(entry.getValue(), validated);
    }

    public String storeTypeNameKey() {
        var p = provider.getValue();
        var nameKey = p == null
                        || p.getCreationCategory() == null
                        || p.getCreationCategory().getCategory().equals(DataStorage.ALL_CONNECTIONS_CATEGORY_UUID)
                ? "connection"
                : p.getCreationCategory().getCategory().equals(DataStorage.ALL_SCRIPTS_CATEGORY_UUID)
                        ? "script"
                        : "identity";
        return nameKey;
    }

    public String getTitleKey() {
        var typeKey = storeTypeNameKey();
        var actionKey = getExistingEntry() != null ? "Edit" : "Add";
        return typeKey + actionKey;
    }
}
