package p.studio.workspaces.assets.details.contract;

import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import p.packer.dtos.PackerCodecConfigurationFieldDTO;
import p.packer.messages.UpdateAssetContractRequest;
import p.packer.messages.assets.OutputCodecCatalog;
import p.studio.Container;
import p.studio.controls.forms.StudioFormEditScopeChangedEvent;
import p.studio.controls.forms.StudioFormMode;
import p.studio.controls.forms.StudioFormSession;
import p.studio.controls.forms.StudioFormSection;
import p.studio.controls.lifecycle.StudioControlLifecycle;
import p.studio.controls.lifecycle.StudioControlLifecycleSupport;
import p.studio.lsp.events.StudioWorkspaceEventBus;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
import p.studio.workspaces.assets.messages.events.StudioAssetLogEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsDetailsViewStateChangedEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsRefreshRequestedEvent;
import p.studio.workspaces.assets.messages.events.StudioAssetsWorkspaceSelectionRequestedEvent;
import p.studio.workspaces.framework.StudioSubscriptionBag;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

public final class AssetDetailsContractControl extends HBox implements StudioControlLifecycle {
    private final ProjectReference projectReference;
    private final StudioWorkspaceEventBus workspaceBus;
    private final StudioSubscriptionBag subscriptions = new StudioSubscriptionBag();

    private final RuntimeContractSection runtimeSection;
    private final CodecSection codecSection;

    private AssetWorkspaceDetailsViewState viewState;

    public AssetDetailsContractControl(ProjectReference projectReference, StudioWorkspaceEventBus workspaceBus) {
        StudioControlLifecycleSupport.install(this, this);
        this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
        this.workspaceBus = Objects.requireNonNull(workspaceBus, "workspaceBus");
        this.runtimeSection = new RuntimeContractSection();
        this.codecSection = new CodecSection();
        getStyleClass().add("assets-details-contract-body");
        setSpacing(16);
        HBox.setHgrow(runtimeSection, Priority.ALWAYS);
        HBox.setHgrow(codecSection, Priority.ALWAYS);
        runtimeSection.setMaxWidth(Double.MAX_VALUE);
        codecSection.setMaxWidth(Double.MAX_VALUE);
        runtimeSection.prefWidthProperty().bind(widthProperty().subtract(16).multiply(0.5d));
        codecSection.prefWidthProperty().bind(widthProperty().subtract(16).multiply(0.5d));
        getChildren().setAll(runtimeSection, codecSection);
    }

    @Override
    public void subscribe() {
        subscriptions.add(workspaceBus.subscribe(StudioAssetsDetailsViewStateChangedEvent.class, event -> {
            viewState = event.viewState();
            runtimeSection.syncSession();
            codecSection.syncSession();
            runtimeSection.renderSection();
            codecSection.renderSection();
        }));
        subscriptions.add(workspaceBus.subscribe(StudioFormEditScopeChangedEvent.class, event -> {
            runtimeSection.handleExternalEditScopeChanged(event);
            codecSection.handleExternalEditScopeChanged(event);
        }));
    }

    @Override
    public void unsubscribe() {
        subscriptions.clear();
    }

    private String currentScopeKey() {
        return viewState == null || viewState.selectedAssetReference() == null
                ? null
                : "asset-details:" + viewState.selectedAssetReference();
    }

    private AssetWorkspaceAssetDetails selectedDetails() {
        return viewState == null ? null : viewState.selectedAssetDetails();
    }

    private void requestRefresh() {
        if (viewState == null || viewState.selectedAssetReference() == null) {
            return;
        }
        workspaceBus.publish(new StudioAssetsWorkspaceSelectionRequestedEvent(viewState.selectedAssetReference(), true));
    }

    private void logContractEvent(String message) {
        workspaceBus.publish(new StudioAssetLogEvent("ASSET DETAILS CONTRACT", message));
    }

    private abstract class BaseContractSection<D> extends StudioFormSection {
        private final String sectionId;
        private final VBox content = new VBox(12);
        private StudioFormSession<D> formSession;

        protected BaseContractSection(String sectionId) {
            this.sectionId = sectionId;
            getStyleClass().add("assets-details-contract-section-host");
            content.getStyleClass().add("assets-details-contract-column");
        }

        protected abstract D sourceFromDetails(AssetWorkspaceAssetDetails details);

        protected abstract Node buildContent(AssetWorkspaceAssetDetails details, D draft, boolean editing);

        protected abstract void applyDraft(D draft);

        protected final StudioFormSession<D> formSession() {
            return formSession;
        }

        protected final void syncSession() {
            final AssetWorkspaceAssetDetails details = selectedDetails();
            if (details == null) {
                publishEditScope(workspaceBus, currentScopeKey(), null);
                formSession = null;
                return;
            }
            final D source = sourceFromDetails(details);
            if (formSession == null) {
                formSession = new StudioFormSession<>(source);
                return;
            }
            if (!Objects.equals(formSession.source(), source)) {
                publishEditScope(workspaceBus, currentScopeKey(), null);
                formSession.replaceSource(source);
            }
        }

        protected final void handleExternalEditScopeChanged(StudioFormEditScopeChangedEvent event) {
            handleEditScopeChanged(event, currentScopeKey());
        }

        @Override
        protected final void renderSection() {
            final AssetWorkspaceAssetDetails details = selectedDetails();
            if (details == null) {
                clearRenderedSection();
                return;
            }
            if (formSession == null) {
                syncSession();
            }
            if (formSession == null) {
                clearRenderedSection();
                return;
            }
            content.getChildren().setAll(buildContent(details, formSession.draft(), formSession.mode() == StudioFormMode.EDITING));
            renderFormSection(content);
        }

        @Override
        protected final StudioFormMode formMode() {
            return formSession == null ? StudioFormMode.READ_ONLY : formSession.mode();
        }

        @Override
        protected final boolean isDirty() {
            return formSession != null && formSession.isDirty();
        }

        @Override
        protected final void beginEdit() {
            if (formSession == null) {
                return;
            }
            formSession.beginEdit();
            publishEditScope(workspaceBus, currentScopeKey(), sectionId);
        }

        @Override
        protected final void apply() {
            if (formSession == null) {
                return;
            }
            applyDraft(formSession.draft());
        }

        @Override
        protected final void reset() {
            if (formSession == null) {
                return;
            }
            formSession.resetDraft();
        }

        @Override
        protected final void cancel() {
            if (formSession == null) {
                return;
            }
            formSession.cancelEdit();
            publishEditScope(workspaceBus, currentScopeKey(), null);
        }

        @Override
        protected final String formSectionId() {
            return sectionId;
        }

        @Override
        protected final String sectionStyleClass() {
            return "assets-details-contract-section";
        }
    }

    private final class RuntimeContractSection extends BaseContractSection<RuntimeContractDraft> {
        private static final String SECTION_ID = "asset-details.runtime-contract";

        private RuntimeContractSection() {
            super(SECTION_ID);
        }

        @Override
        protected RuntimeContractDraft sourceFromDetails(AssetWorkspaceAssetDetails details) {
            return RuntimeContractDraft.fromDetails(details);
        }

        @Override
        protected Node buildContent(AssetWorkspaceAssetDetails details, RuntimeContractDraft draft, boolean editing) {
            final VBox column = new VBox(8);
            column.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
                    Container.i18n().text(I18n.ASSETS_LABEL_PRELOAD),
                    createPreloadEditor(draft, editing)));
            column.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
                    Container.i18n().text(I18n.ASSETS_LABEL_BANK),
                    draft.bank()));
            column.getChildren().add(createRuntimeMetadataSection(details, draft, editing));
            return column;
        }

        @Override
        protected String sectionTitle() {
            return Container.i18n().text(I18n.ASSETS_SECTION_RUNTIME_CONTRACT);
        }

        @Override
        protected void applyDraft(RuntimeContractDraft draft) {
            final AssetWorkspaceAssetDetails details = selectedDetails();
            if (details == null) {
                return;
            }
            Container.backgroundTasks().submit(() -> {
                try {
                    final var response = Container.packer().workspaceService().updateAssetContract(new UpdateAssetContractRequest(
                            projectReference.toPackerProjectContext(),
                            draft.assetReference(),
                            draft.preload(),
                            details.outputCodec(),
                            currentCodecFieldValues(details),
                            draft.metadataFieldValues()));
                    Platform.runLater(() -> handleApplyResponse(response.success(), response.errorMessage()));
                } catch (Exception exception) {
                    Platform.runLater(() -> handleApplyResponse(false, exception.getMessage()));
                }
            });
        }

        private void handleApplyResponse(boolean success, String errorMessage) {
            if (success) {
                formSession().apply();
                publishEditScope(workspaceBus, currentScopeKey(), null);
                logContractEvent("Runtime contract updated");
                requestRefresh();
                renderSection();
                return;
            }
            logContractEvent(errorMessage);
            cancel();
            renderSection();
        }

        private Node createPreloadEditor(RuntimeContractDraft draft, boolean editing) {
            final CheckBox checkBox = new CheckBox();
            checkBox.getStyleClass().add("assets-details-readonly-check");
            checkBox.setSelected(draft.preload());
            if (editing) {
                checkBox.selectedProperty().addListener((ignored, oldValue, newValue) -> {
                    if (!Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withPreload(newValue));
                        updateActionBarState();
                    }
                });
            } else {
                configureReadOnlyControl(checkBox);
            }
            return checkBox;
        }

        private Node createMetadataFieldControl(
                PackerCodecConfigurationFieldDTO field,
                RuntimeContractDraft draft,
                boolean editing) {
            return switch (field.fieldType()) {
                case BOOLEAN -> createMetadataBooleanField(field, draft, editing);
                case ENUM -> createMetadataEnumField(field, draft, editing);
                case INTEGER, TEXT -> createMetadataTextField(field, draft, editing);
            };
        }

        private Node createRuntimeMetadataSection(
                AssetWorkspaceAssetDetails details,
                RuntimeContractDraft draft,
                boolean editing) {
            final VBox content = new VBox(8);
            content.getStyleClass().add("assets-details-contract-metadata-content");
            if (details.metadataFields().isEmpty()) {
                content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                        Container.i18n().text(I18n.ASSETS_DETAILS_METADATA_EMPTY)));
                return createCodecMetadataPane(content);
            }
            for (PackerCodecConfigurationFieldDTO field : details.metadataFields()) {
                content.getChildren().add(createLabeledFieldRow(
                        field.label(),
                        createMetadataFieldControl(field, draft, editing)));
            }
            return createCodecMetadataPane(content);
        }

        private Node createMetadataBooleanField(PackerCodecConfigurationFieldDTO field, RuntimeContractDraft draft, boolean editing) {
            final CheckBox checkBox = new CheckBox();
            checkBox.getStyleClass().add("assets-details-readonly-check");
            checkBox.setSelected(Boolean.parseBoolean(draft.metadataFieldValue(field.key(), field.value())));
            if (editing) {
                checkBox.selectedProperty().addListener((ignored, oldValue, newValue) -> {
                    if (!Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withMetadataFieldValue(field.key(), Boolean.toString(newValue)));
                        updateActionBarState();
                    }
                });
            } else {
                configureReadOnlyControl(checkBox);
            }
            return checkBox;
        }

        private Node createMetadataEnumField(PackerCodecConfigurationFieldDTO field, RuntimeContractDraft draft, boolean editing) {
            final ComboBox<String> comboBox = new ComboBox<>(FXCollections.observableArrayList(field.options()));
            comboBox.getStyleClass().add("assets-details-combo");
            comboBox.setMaxWidth(Double.MAX_VALUE);
            comboBox.getSelectionModel().select(draft.metadataFieldValue(field.key(), field.value()));
            if (editing) {
                comboBox.valueProperty().addListener((ignored, oldValue, newValue) -> {
                    if (newValue != null && !Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withMetadataFieldValue(field.key(), newValue));
                        updateActionBarState();
                    }
                });
            } else {
                configureReadOnlyControl(comboBox);
            }
            return comboBox;
        }

        private Node createMetadataTextField(PackerCodecConfigurationFieldDTO field, RuntimeContractDraft draft, boolean editing) {
            final TextField textField = new TextField(draft.metadataFieldValue(field.key(), field.value()));
            textField.getStyleClass().add("assets-workspace-search");
            textField.setMaxWidth(Double.MAX_VALUE);
            if (editing) {
                textField.textProperty().addListener((ignored, oldValue, newValue) -> {
                    if (!Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withMetadataFieldValue(field.key(), Objects.requireNonNullElse(newValue, "")));
                        updateActionBarState();
                    }
                });
            } else {
                textField.setEditable(false);
                configureReadOnlyControl(textField);
            }
            return textField;
        }
    }

    private final class CodecSection extends BaseContractSection<CodecDraft> {
        private static final String SECTION_ID = "asset-details.codec";

        private CodecSection() {
            super(SECTION_ID);
        }

        @Override
        protected CodecDraft sourceFromDetails(AssetWorkspaceAssetDetails details) {
            return CodecDraft.fromDetails(details);
        }

        @Override
        protected Node buildContent(AssetWorkspaceAssetDetails details, CodecDraft draft, boolean editing) {
            final VBox column = new VBox(8);
            column.getStyleClass().add("assets-details-contract-codec-column");
            column.getChildren().add(createCodecEditor(details, draft, editing));
            column.getChildren().add(createCodecConfigurationSection(details, draft, editing));
            return column;
        }

        @Override
        protected String sectionTitle() {
            return Container.i18n().text(I18n.ASSETS_LABEL_CODEC);
        }

        @Override
        protected void applyDraft(CodecDraft draft) {
            final AssetWorkspaceAssetDetails details = selectedDetails();
            if (details == null) {
                return;
            }
            Container.backgroundTasks().submit(() -> {
                try {
                    final var response = Container.packer().workspaceService().updateAssetContract(new UpdateAssetContractRequest(
                            projectReference.toPackerProjectContext(),
                            draft.assetReference(),
                            details.summary().preload(),
                            draft.selectedCodec(),
                            draft.codecFieldValues(),
                            currentMetadataFieldValues(details)));
                    Platform.runLater(() -> handleApplyResponse(response.success(), response.errorMessage()));
                } catch (Exception exception) {
                    Platform.runLater(() -> handleApplyResponse(false, exception.getMessage()));
                }
            });
        }

        private void handleApplyResponse(boolean success, String errorMessage) {
            if (success) {
                formSession().apply();
                publishEditScope(workspaceBus, currentScopeKey(), null);
                logContractEvent("Codec updated");
                requestRefresh();
                renderSection();
                return;
            }
            logContractEvent(errorMessage);
            cancel();
            renderSection();
        }

        private Node createCodecEditor(AssetWorkspaceAssetDetails details, CodecDraft draft, boolean editing) {
            final ComboBox<OutputCodecCatalog> codecCombo = new ComboBox<>(FXCollections.observableArrayList(details.availableOutputCodecs()));
            codecCombo.getStyleClass().add("assets-details-combo");
            codecCombo.setMaxWidth(Double.MAX_VALUE);
            codecCombo.setCellFactory(ignored -> new javafx.scene.control.ListCell<>() {
                @Override
                protected void updateItem(OutputCodecCatalog item, boolean empty) {
                    super.updateItem(item, empty);
                    setText(empty || item == null ? null : item.displayName());
                }
            });
            codecCombo.setButtonCell(new javafx.scene.control.ListCell<>() {
                @Override
                protected void updateItem(OutputCodecCatalog item, boolean empty) {
                    super.updateItem(item, empty);
                    setText(empty || item == null ? null : item.displayName());
                }
            });
            codecCombo.getSelectionModel().select(draft.selectedCodec());
            if (editing) {
                codecCombo.valueProperty().addListener((ignored, oldValue, newValue) -> {
                    if (newValue != null && !Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withSelectedCodec(newValue));
                        renderSection();
                    }
                });
            } else {
                configureReadOnlyControl(codecCombo);
            }
            return codecCombo;
        }

        private Node createCodecConfigurationSection(AssetWorkspaceAssetDetails details, CodecDraft draft, boolean editing) {
            final VBox content = new VBox(8);
            content.getStyleClass().add("assets-details-contract-metadata-content");
            if (draft.selectedCodec() == OutputCodecCatalog.NONE || draft.selectedCodec() == OutputCodecCatalog.UNKNOWN) {
                content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                        Container.i18n().text(I18n.ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY)));
                return createCodecMetadataPane(content);
            }

            final var fields = details.codecConfigurationFieldsByCodec().getOrDefault(draft.selectedCodec(), java.util.List.of());
            if (fields.isEmpty()) {
                content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                        Container.i18n().text(I18n.ASSETS_DETAILS_CODEC_CONFIGURATION_EMPTY)));
                return createCodecMetadataPane(content);
            }

            for (PackerCodecConfigurationFieldDTO field : fields) {
                content.getChildren().add(createCodecFieldControl(field, draft, editing));
            }
            return createCodecMetadataPane(content);
        }

        private Node createCodecFieldControl(PackerCodecConfigurationFieldDTO field, CodecDraft draft, boolean editing) {
            final Node valueNode = switch (field.fieldType()) {
                case BOOLEAN -> createBooleanField(field, draft, editing);
                case ENUM -> createEnumField(field, draft, editing);
                case INTEGER, TEXT -> createTextField(field, draft, editing);
            };
            if (valueNode instanceof TextField textField) {
                textField.setPromptText(field.label());
            }
            if (valueNode instanceof ComboBox<?> comboBox) {
                comboBox.setPromptText(field.label());
            }
            return valueNode;
        }

        private Node createBooleanField(PackerCodecConfigurationFieldDTO field, CodecDraft draft, boolean editing) {
            final CheckBox checkBox = new CheckBox(field.label());
            checkBox.getStyleClass().add("assets-details-readonly-check");
            checkBox.setSelected(draft.codecFieldValue(draft.selectedCodec(), field.key(), field.value()).equalsIgnoreCase("true"));
            if (editing) {
                checkBox.selectedProperty().addListener((ignored, oldValue, newValue) -> {
                    if (!Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withCodecFieldValue(current.selectedCodec(), field.key(), Boolean.toString(newValue)));
                        updateActionBarState();
                    }
                });
            } else {
                configureReadOnlyControl(checkBox);
            }
            return checkBox;
        }

        private Node createEnumField(PackerCodecConfigurationFieldDTO field, CodecDraft draft, boolean editing) {
            final ComboBox<String> comboBox = new ComboBox<>(FXCollections.observableArrayList(field.options()));
            comboBox.getStyleClass().add("assets-details-combo");
            comboBox.setMaxWidth(Double.MAX_VALUE);
            comboBox.getSelectionModel().select(draft.codecFieldValue(draft.selectedCodec(), field.key(), field.value()));
            if (editing) {
                comboBox.valueProperty().addListener((ignored, oldValue, newValue) -> {
                    if (newValue != null && !Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withCodecFieldValue(current.selectedCodec(), field.key(), newValue));
                        updateActionBarState();
                    }
                });
            } else {
                configureReadOnlyControl(comboBox);
            }
            return comboBox;
        }

        private Node createTextField(PackerCodecConfigurationFieldDTO field, CodecDraft draft, boolean editing) {
            final TextField textField = new TextField(draft.codecFieldValue(draft.selectedCodec(), field.key(), field.value()));
            textField.getStyleClass().add("assets-workspace-search");
            textField.setMaxWidth(Double.MAX_VALUE);
            if (editing) {
                textField.textProperty().addListener((ignored, oldValue, newValue) -> {
                    if (!Objects.equals(oldValue, newValue)) {
                        formSession().updateDraft(current -> current.withCodecFieldValue(current.selectedCodec(), field.key(), Objects.requireNonNullElse(newValue, "")));
                        updateActionBarState();
                    }
                });
            } else {
                textField.setEditable(false);
                configureReadOnlyControl(textField);
            }
            return textField;
        }
    }

    private Node createLabeledFieldRow(String label, Node valueNode) {
        return AssetDetailsUiSupport.createKeyValueRow(label, valueNode);
    }

    private Node createCodecMetadataPane(VBox content) {
        final ScrollPane scrollPane = new ScrollPane(content);
        scrollPane.setFitToWidth(true);
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        scrollPane.getStyleClass().add("assets-details-contract-metadata-scroll");
        final VBox container = new VBox(scrollPane);
        VBox.setVgrow(scrollPane, Priority.ALWAYS);
        return container;
    }

    private void configureReadOnlyControl(Control control) {
        control.setMouseTransparent(true);
        control.setFocusTraversable(false);
    }

    private Map<String, String> currentMetadataFieldValues(AssetWorkspaceAssetDetails details) {
        final Map<String, String> values = new LinkedHashMap<>();
        details.metadataFields().forEach(field -> values.put(field.key(), field.value()));
        return Map.copyOf(values);
    }

    private Map<String, String> currentCodecFieldValues(AssetWorkspaceAssetDetails details) {
        final Map<String, String> values = new LinkedHashMap<>();
        details.codecConfigurationFieldsByCodec().forEach((codec, fields) ->
                fields.forEach(field -> values.put(codec.name() + ":" + field.key(), field.value())));
        return Map.copyOf(values);
    }
}
