package p.studio.workspaces.assets.wizards;

import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.OverrunStyle;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.Window;
import p.packer.dtos.PackerEmittedArtifactDTO;
import p.packer.dtos.PackerPackSummaryAssetDTO;
import p.packer.dtos.PackerPackValidationAssetDTO;
import p.packer.messages.ListAssetsRequest;
import p.packer.messages.ListAssetsResult;
import p.packer.messages.PackWorkspaceRequest;
import p.packer.messages.PackWorkspaceResult;
import p.packer.messages.PackWorkspaceSummaryRequest;
import p.packer.messages.PackWorkspaceSummaryResult;
import p.packer.messages.PackerOperationStatus;
import p.packer.messages.ValidatePackWorkspaceRequest;
import p.packer.messages.ValidatePackWorkspaceResult;
import p.studio.Container;
import p.studio.projects.ProjectReference;
import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.AssetDetailsUiSupport;
import p.studio.workspaces.assets.details.bank.AssetBankCapacityService;
import p.studio.workspaces.assets.details.bank.AssetDetailsBankCompositionCapacityState;

import java.util.List;
import java.util.Objects;

public final class PackWorkspaceWizard {
    private enum Phase {
        DEEP_SYNC_INTRO,
        DEEP_SYNC_WAITING,
        SUMMARY,
        VALIDATION,
        PACKING,
        RESULT
    }

    private final ProjectReference projectReference;
    private final Stage stage;
    private final Label stepTitle = new Label();
    private final Label stepDescription = new Label();
    private final VBox stepBody = new VBox(12);
    private final Label feedbackLabel = new Label();
    private final Button backButton = new Button();
    private final Button nextButton = new Button();
    private final Button packButton = new Button();
    private final Button closeButton = new Button();
    private final Button copyFailuresButton = new Button();
    private final AssetBankCapacityService bankCapacityService = new AssetBankCapacityService();

    private Phase phase = Phase.DEEP_SYNC_INTRO;
    private boolean deepSyncRunning;
    private boolean deepSyncCompleted;
    private boolean loadingSummary;
    private boolean loadingValidation;
    private boolean packing;

    private PackWorkspaceSummaryResult summaryResult;
    private ValidatePackWorkspaceResult validationResult;
    private PackWorkspaceResult packResult;

    private PackWorkspaceWizard(Window owner, ProjectReference projectReference) {
        this.projectReference = Objects.requireNonNull(projectReference, "projectReference");
        this.stage = new Stage();
        stage.initOwner(owner);
        stage.initModality(Modality.WINDOW_MODAL);
        stage.setTitle(Container.i18n().text(I18n.ASSETS_PACK_WIZARD_TITLE));
        stage.setScene(new Scene(buildRoot(), 760, 560));
        stage.getScene().getStylesheets().add(Container.theme().getDefaultTheme());
        stage.setOnCloseRequest(event -> {
            if (packing || deepSyncRunning) {
                event.consume();
            }
        });
        render();
    }

    public static void showAndWait(Window owner, ProjectReference projectReference) {
        new PackWorkspaceWizard(owner, projectReference).stage.showAndWait();
    }

    private VBox buildRoot() {
        stepTitle.getStyleClass().add("studio-launcher-section-title");
        stepDescription.getStyleClass().add("studio-launcher-subtitle");
        feedbackLabel.getStyleClass().add("studio-launcher-feedback");
        feedbackLabel.setWrapText(true);

        backButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_BACK));
        backButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        backButton.setOnAction(ignored -> goBack());

        nextButton.textProperty().bind(Container.i18n().bind(I18n.WIZARD_NEXT));
        nextButton.getStyleClass().addAll("studio-button", "studio-button-primary");
        nextButton.setOnAction(ignored -> advancePrimaryFlow());

        packButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_BUTTON_PACK));
        packButton.getStyleClass().addAll("studio-button", "studio-button-primary");
        packButton.setOnAction(ignored -> startPack());

        copyFailuresButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_BUTTON_COPY_FAILURES));
        copyFailuresButton.getStyleClass().addAll("studio-button", "studio-button-secondary");
        copyFailuresButton.setDisable(true);

        closeButton.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_BUTTON_CLOSE));
        closeButton.getStyleClass().addAll("studio-button", "studio-button-cancel");
        closeButton.setOnAction(ignored -> {
            if (!packing) {
                stage.close();
            }
        });

        final HBox actions = new HBox(12, backButton, nextButton, packButton, copyFailuresButton, closeButton);
        actions.setAlignment(Pos.CENTER_RIGHT);

        final VBox root = new VBox(16, stepTitle, stepDescription, stepBody, feedbackLabel, actions);
        root.setPadding(new Insets(24));
        VBox.setVgrow(stepBody, Priority.ALWAYS);
        return root;
    }

    private void render() {
        final boolean busy = deepSyncRunning || loadingSummary || loadingValidation || packing;
        backButton.setDisable(busy || phase == Phase.DEEP_SYNC_INTRO || phase == Phase.DEEP_SYNC_WAITING);

        nextButton.setVisible(phase == Phase.DEEP_SYNC_INTRO
                || phase == Phase.DEEP_SYNC_WAITING
                || phase == Phase.SUMMARY);
        nextButton.setManaged(nextButton.isVisible());
        nextButton.setDisable(switch (phase) {
            case DEEP_SYNC_INTRO -> busy;
            case DEEP_SYNC_WAITING -> deepSyncRunning;
            case SUMMARY -> busy || summaryResult == null;
            default -> true;
        });

        packButton.setVisible(phase == Phase.VALIDATION);
        packButton.setManaged(phase == Phase.VALIDATION);
        packButton.setDisable(busy || validationResult == null || !validationResult.canPack());

        copyFailuresButton.setVisible(phase == Phase.RESULT);
        copyFailuresButton.setManaged(phase == Phase.RESULT);

        switch (phase) {
            case DEEP_SYNC_INTRO -> renderDeepSyncIntroPhase();
            case DEEP_SYNC_WAITING -> renderDeepSyncWaitingPhase();
            case SUMMARY -> renderSummaryPhase();
            case VALIDATION -> renderValidationPhase();
            case PACKING -> renderPackingPhase();
            case RESULT -> renderResultPhase();
        }
    }

    private void renderDeepSyncIntroPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_DEEP_SYNC_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_DEEP_SYNC_DESCRIPTION));
        stepBody.getChildren().setAll(
                AssetDetailsUiSupport.createSectionMessage(Container.i18n().text(I18n.ASSETS_PACK_WIZARD_DEEP_SYNC_NOTE)));
    }

    private void renderDeepSyncWaitingPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_DEEP_SYNC_WAITING_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_DEEP_SYNC_WAITING_DESCRIPTION));
        if (deepSyncRunning) {
            stepBody.getChildren().setAll(createBusyBox(I18n.ASSETS_PACK_WIZARD_LOADING_DEEP_SYNC));
            return;
        }
        final String message = deepSyncCompleted
                ? Container.i18n().text(I18n.ASSETS_PACK_WIZARD_DEEP_SYNC_COMPLETED)
                : Container.i18n().text(I18n.ASSETS_PACK_WIZARD_ERROR_DEEP_SYNC);
        stepBody.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(message));
    }

    private void renderSummaryPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_SUMMARY_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_SUMMARY_DESCRIPTION));
        if (loadingSummary) {
            stepBody.getChildren().setAll(createBusyBox(I18n.ASSETS_PACK_WIZARD_LOADING_SUMMARY));
            return;
        }
        if (summaryResult == null) {
            stepBody.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_ERROR_SUMMARY)));
            return;
        }

        stepBody.getChildren().setAll(
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_INCLUDED_ASSETS),
                        Integer.toString(summaryResult.packSummary().totalIncludedAssetCount())),
                createSummaryAssetsSection(),
                AssetDetailsUiSupport.createSectionMessage(summaryResult.summary()));
    }

    private Node createSummaryAssetsSection() {
        if (summaryResult.packSummary().assets().isEmpty()) {
            return AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_VALIDATION_EMPTY));
        }
        final VBox assets = new VBox(10);
        for (PackerPackSummaryAssetDTO asset : summaryResult.packSummary().assets()) {
            assets.getChildren().add(createSummaryRow(asset));
        }
        final ScrollPane scrollPane = new ScrollPane(assets);
        scrollPane.setFitToWidth(true);
        scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
        VBox.setVgrow(scrollPane, Priority.ALWAYS);
        return scrollPane;
    }

    private Node createSummaryRow(PackerPackSummaryAssetDTO asset) {
        final Label assetName = new Label(truncate(asset.assetName(), 50));
        assetName.getStyleClass().add("assets-details-value");
        assetName.setTextOverrun(OverrunStyle.ELLIPSIS);
        assetName.setMaxWidth(Double.MAX_VALUE);

        final Label assetIdChip = AssetDetailsUiSupport.createChip(
                "assets-details-chip-generic",
                "#" + asset.assetId());
        final Label familyChip = AssetDetailsUiSupport.createChip(
                AssetDetailsUiSupport.typeChipTone(asset.assetFamily()),
                AssetDetailsUiSupport.typeLabel(asset.assetFamily()));
        final AssetDetailsBankCompositionCapacityState capacityState = summaryCapacityState(asset);
        final ProgressBar capacityBar = new ProgressBar(capacityState.progress());
        capacityBar.setMaxWidth(Double.MAX_VALUE);
        applyCapacityTone(capacityBar, capacityState);

        final GridPane row = new GridPane();
        row.setHgap(12);
        row.setVgap(0);
        row.setAlignment(Pos.CENTER_LEFT);
        row.getStyleClass().add("studio-card");
        row.setMaxWidth(Double.MAX_VALUE);

        final ColumnConstraints idColumn = new ColumnConstraints();
        idColumn.setPercentWidth(12);
        final ColumnConstraints nameColumn = new ColumnConstraints();
        nameColumn.setPercentWidth(38);
        final ColumnConstraints familyColumn = new ColumnConstraints();
        familyColumn.setPercentWidth(15);
        final ColumnConstraints barColumn = new ColumnConstraints();
        barColumn.setPercentWidth(35);
        barColumn.setHgrow(Priority.ALWAYS);
        row.getColumnConstraints().setAll(idColumn, nameColumn, familyColumn, barColumn);

        row.add(assetIdChip, 0, 0);
        row.add(assetName, 1, 0);
        row.add(familyChip, 2, 0);
        row.add(capacityBar, 3, 0);
        GridPane.setHgrow(assetName, Priority.ALWAYS);
        GridPane.setHgrow(capacityBar, Priority.ALWAYS);
        return row;
    }

    private AssetDetailsBankCompositionCapacityState summaryCapacityState(PackerPackSummaryAssetDTO asset) {
        return bankCapacityService.evaluate(
                asset.assetFamily(),
                asset.artifactCount(),
                asset.outputMetadata(),
                asset.outputPipeline(),
                0L);
    }

    private void applyCapacityTone(ProgressBar capacityBar, AssetDetailsBankCompositionCapacityState state) {
        capacityBar.setStyle(switch (state.severity()) {
            case GREEN -> "-fx-accent: #2f9e44;";
            case ORANGE -> "-fx-accent: #f08c00;";
            case RED -> "-fx-accent: #e03131;";
        });
    }

    private String truncate(String value, int maxChars) {
        if (value == null || value.length() <= maxChars) {
            return value;
        }
        if (maxChars <= 1) {
            return value.substring(0, Math.max(0, maxChars));
        }
        return value.substring(0, Math.max(0, maxChars - 3)) + "...";
    }

    private void renderValidationPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_VALIDATION_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_VALIDATION_DESCRIPTION));
        if (loadingValidation) {
            stepBody.getChildren().setAll(createBusyBox(I18n.ASSETS_PACK_WIZARD_LOADING_VALIDATION));
            return;
        }
        if (validationResult == null) {
            stepBody.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_ERROR_VALIDATION)));
            return;
        }

        final VBox content = new VBox(12);
        content.getChildren().addAll(
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_BLOCKED_ASSETS),
                        Integer.toString(validationResult.assets().size())),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_STATUS),
                        validationResult.canPack()
                                ? Container.i18n().text(I18n.ASSETS_PACK_WIZARD_STATUS_READY)
                                : Container.i18n().text(I18n.ASSETS_PACK_WIZARD_STATUS_BLOCKED)),
                AssetDetailsUiSupport.createSectionMessage(validationResult.summary()));

        if (validationResult.assets().isEmpty()) {
            content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_VALIDATION_EMPTY)));
        } else {
            final Accordion accordion = new Accordion();
            for (PackerPackValidationAssetDTO asset : validationResult.assets()) {
                accordion.getPanes().add(createValidationPane(asset));
            }
            VBox.setVgrow(accordion, Priority.ALWAYS);
            content.getChildren().add(accordion);
        }
        stepBody.getChildren().setAll(content);
    }

    private TitledPane createValidationPane(PackerPackValidationAssetDTO asset) {
        final VBox content = new VBox(8);
        content.getChildren().addAll(
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_LABEL_NAME),
                        asset.assetName()),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_LABEL_LOCATION),
                        asset.assetPath().toString()),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_LAST_MODIFIED),
                        Long.toString(asset.lastModified())),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_STATUS),
                        asset.blocked()
                                ? Container.i18n().text(I18n.ASSETS_PACK_WIZARD_STATUS_BLOCKED)
                                : Container.i18n().text(I18n.ASSETS_PACK_WIZARD_STATUS_READY)));

        final List<p.packer.dtos.PackerDiagnosticDTO> blockingDiagnostics = asset.diagnostics().stream()
                .filter(p.packer.dtos.PackerDiagnosticDTO::blocking)
                .toList();
        if (blockingDiagnostics.isEmpty()) {
            content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_DIAGNOSTICS_EMPTY)));
        } else {
            blockingDiagnostics.forEach(diagnostic -> content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                    diagnostic.severity().name() + ": " + diagnostic.message())));
        }

        final String title = asset.assetName() + (asset.blocked() ? " (blocked)" : " (ready)");
        final TitledPane pane = new TitledPane(title, content);
        pane.setExpanded(false);
        return pane;
    }

    private void renderPackingPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_PACKING_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_PACKING_DESCRIPTION));
        stepBody.getChildren().setAll(createBusyBox(I18n.ASSETS_PACK_WIZARD_LOADING_PACKING));
    }

    private void renderResultPhase() {
        stepTitle.textProperty().unbind();
        stepDescription.textProperty().unbind();
        stepTitle.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_RESULT_TITLE));
        stepDescription.textProperty().bind(Container.i18n().bind(I18n.ASSETS_PACK_WIZARD_STEP_RESULT_DESCRIPTION));
        if (packResult == null) {
            stepBody.getChildren().setAll(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_ERROR_PACK)));
            return;
        }

        final VBox content = new VBox(12);
        content.getChildren().addAll(
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_STATUS),
                        switch (packResult.status()) {
                            case SUCCESS, PARTIAL -> Container.i18n().text(I18n.ASSETS_PACK_WIZARD_STATUS_FINISHED);
                            case FAILED -> packResult.status().name();
                        }),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_CANONICAL_ARTIFACT),
                        packResult.result().canonicalArtifactName()),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_PACKED_ASSETS),
                        Integer.toString(packResult.result().packedAssetCount())),
                AssetDetailsUiSupport.createKeyValueRow(
                        Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_ELAPSED),
                        formatElapsed(packResult.result().elapsedMillis())),
                AssetDetailsUiSupport.createSectionMessage(packResult.summary()));

        if (packResult.result().emittedArtifacts().isEmpty()) {
            content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                    Container.i18n().text(I18n.ASSETS_PACK_WIZARD_RESULT_NO_ARTIFACTS)));
        } else {
            final VBox artifacts = new VBox(8);
            artifacts.getChildren().add(new Label(Container.i18n().text(I18n.ASSETS_PACK_WIZARD_LABEL_EMITTED_ARTIFACTS)));
            for (PackerEmittedArtifactDTO artifact : packResult.result().emittedArtifacts()) {
                artifacts.getChildren().add(AssetDetailsUiSupport.createKeyValueRow(
                        artifact.label(),
                        artifact.path().toString()));
            }
            content.getChildren().add(artifacts);
        }
        content.getChildren().add(AssetDetailsUiSupport.createSectionMessage(
                Container.i18n().text(I18n.ASSETS_PACK_WIZARD_RESULT_COPY_FAILURES_HINT)));
        stepBody.getChildren().setAll(content);
    }

    private VBox createBusyBox(I18n messageKey) {
        final ProgressIndicator indicator = new ProgressIndicator();
        indicator.setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
        final VBox box = new VBox(16, indicator, AssetDetailsUiSupport.createSectionMessage(
                Container.i18n().text(messageKey)));
        box.setAlignment(Pos.CENTER_LEFT);
        return box;
    }

    private void goBack() {
        if (phase == Phase.VALIDATION && !loadingValidation) {
            phase = Phase.SUMMARY;
            feedbackLabel.setText("");
            render();
        }
    }

    private void advancePrimaryFlow() {
        switch (phase) {
            case DEEP_SYNC_INTRO -> startDeepSync();
            case DEEP_SYNC_WAITING -> {
                if (deepSyncCompleted) {
                    phase = Phase.SUMMARY;
                    feedbackLabel.setText("");
                    if (summaryResult == null) {
                        loadSummary();
                    } else {
                        render();
                    }
                } else if (!deepSyncRunning) {
                    startDeepSync();
                }
            }
            case SUMMARY -> loadValidation();
            default -> {
            }
        }
    }

    private void startDeepSync() {
        if (deepSyncRunning) {
            return;
        }
        phase = Phase.DEEP_SYNC_WAITING;
        deepSyncRunning = true;
        deepSyncCompleted = false;
        summaryResult = null;
        validationResult = null;
        packResult = null;
        feedbackLabel.setText("");
        render();
        Container.backgroundTasks().submit(() -> {
            try {
                final ListAssetsResult result = Container.packer().workspaceService().listAssets(
                        new ListAssetsRequest(projectReference.toPackerProjectContext(), true));
                Platform.runLater(() -> applyDeepSyncResult(result));
            } catch (RuntimeException exception) {
                Platform.runLater(() -> applyDeepSyncFailure(exception));
            }
        });
    }

    private void applyDeepSyncResult(ListAssetsResult result) {
        deepSyncRunning = false;
        deepSyncCompleted = result.status() != PackerOperationStatus.FAILED;
        feedbackLabel.setText(deepSyncCompleted ? "" : result.summary());
        render();
    }

    private void applyDeepSyncFailure(RuntimeException exception) {
        deepSyncRunning = false;
        deepSyncCompleted = false;
        feedbackLabel.setText(extractMessage(exception, I18n.ASSETS_PACK_WIZARD_ERROR_DEEP_SYNC));
        render();
    }

    private void loadSummary() {
        loadingSummary = true;
        feedbackLabel.setText("");
        summaryResult = null;
        render();
        Container.backgroundTasks().submit(() -> {
            try {
                final PackWorkspaceSummaryResult result = Container.packer().workspaceService().getPackWorkspaceSummary(
                        new PackWorkspaceSummaryRequest(projectReference.toPackerProjectContext()));
                Platform.runLater(() -> applySummaryResult(result));
            } catch (RuntimeException exception) {
                Platform.runLater(() -> applySummaryFailure(exception));
            }
        });
    }

    private void applySummaryResult(PackWorkspaceSummaryResult result) {
        loadingSummary = false;
        summaryResult = result;
        feedbackLabel.setText(result.status() == PackerOperationStatus.FAILED ? result.summary() : "");
        render();
    }

    private void applySummaryFailure(RuntimeException exception) {
        loadingSummary = false;
        feedbackLabel.setText(extractMessage(exception, I18n.ASSETS_PACK_WIZARD_ERROR_SUMMARY));
        render();
    }

    private void loadValidation() {
        if (summaryResult == null || loadingValidation) {
            return;
        }
        phase = Phase.VALIDATION;
        loadingValidation = true;
        validationResult = null;
        feedbackLabel.setText("");
        render();
        Container.backgroundTasks().submit(() -> {
            try {
                final ValidatePackWorkspaceResult result = Container.packer().workspaceService().validatePackWorkspace(
                        new ValidatePackWorkspaceRequest(projectReference.toPackerProjectContext()));
                Platform.runLater(() -> applyValidationResult(result));
            } catch (RuntimeException exception) {
                Platform.runLater(() -> applyValidationFailure(exception));
            }
        });
    }

    private void applyValidationResult(ValidatePackWorkspaceResult result) {
        loadingValidation = false;
        validationResult = result;
        if (!result.canPack()) {
            feedbackLabel.setText(result.summary());
        } else {
            feedbackLabel.setText("");
        }
        render();
    }

    private void applyValidationFailure(RuntimeException exception) {
        loadingValidation = false;
        feedbackLabel.setText(extractMessage(exception, I18n.ASSETS_PACK_WIZARD_ERROR_VALIDATION));
        render();
    }

    private void startPack() {
        if (validationResult == null || !validationResult.canPack() || packing) {
            return;
        }
        phase = Phase.PACKING;
        packing = true;
        packResult = null;
        feedbackLabel.setText("");
        render();
        Container.backgroundTasks().submit(() -> {
            try {
                final PackWorkspaceResult result = Container.packer().workspaceService().packWorkspace(
                        new PackWorkspaceRequest(projectReference.toPackerProjectContext()));
                Platform.runLater(() -> applyPackResult(result));
            } catch (RuntimeException exception) {
                Platform.runLater(() -> applyPackFailure(exception));
            }
        });
    }

    private void applyPackResult(PackWorkspaceResult result) {
        packing = false;
        packResult = result;
        phase = Phase.RESULT;
        feedbackLabel.setText(result.status() == PackerOperationStatus.FAILED ? result.summary() : "");
        render();
    }

    private void applyPackFailure(RuntimeException exception) {
        packing = false;
        phase = Phase.RESULT;
        feedbackLabel.setText(extractMessage(exception, I18n.ASSETS_PACK_WIZARD_ERROR_PACK));
        render();
    }

    private String extractMessage(RuntimeException exception, I18n fallbackKey) {
        final String message = exception.getMessage();
        if (message == null || message.isBlank()) {
            return Container.i18n().text(fallbackKey);
        }
        return message;
    }

    private String formatElapsed(long elapsedMillis) {
        final long seconds = elapsedMillis / 1000L;
        final long milliseconds = elapsedMillis % 1000L;
        return seconds + "." + String.format("%03d", milliseconds) + "s";
    }
}
