package de.gecheckt.pdf.umbenenner.adapter.in.gui.modelprices;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Supplier;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceChangeSet;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceEntry;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceKey;
import de.gecheckt.pdf.umbenenner.application.dto.ModelPriceView;
import javafx.application.Platform;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Dialog;
import javafx.scene.control.Label;
import javafx.scene.control.Tab;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

/**
 * GUI-Tab fuer die Verwaltung der persistierten Modell-Preise.
 *
 * <p>Zeigt die Tabelle {@code model_price} aufbereitet als
 * {@code $/1M Tokens} an. Eintraege bekannter Provider sind editierbar;
 * Eintraege unbekannter Provider werden read-only mit Tooltip dargestellt
 * und koennen lediglich geloescht werden.
 *
 * <p>Der Tab arbeitet ausschlie&szlig;lich gegen den
 * {@link GuiModelPriceManagementPort}. Bootstrap verdrahtet den Port mit
 * einer Implementierung, die anhand der aktuell geladenen Konfigurationsdatei
 * eine SQLite-Verbindung aufbaut.
 *
 * <p>Threading: alle DB-Operationen laufen auf einem dedizierten
 * Hintergrund-Worker-Thread; UI-Updates erfolgen ueber
 * {@link Platform#runLater(Runnable)}.
 */
public final class GuiModelPricesTab {

    private static final Logger LOG = LogManager.getLogger(GuiModelPricesTab.class);

    private static final String TAB_TITLE = "Modell-Preise";

    /** V3.3-Whitelist der unterstuetzten Provider. */
    public static final List<String> SUPPORTED_PROVIDERS = List.of("openai-compatible", "claude");

    private static final DateTimeFormatter GERMAN_DATE_TIME =
            DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");

    private static final BigDecimal NANO_TO_USD_PER_MILLION = new BigDecimal("1000000000")
            .divide(new BigDecimal("1000000"));

    private final Tab tab = new Tab(TAB_TITLE);
    private final TableView<EditableEntry> tableView = new TableView<>();
    private final ObservableList<EditableEntry> rows = FXCollections.observableArrayList();
    private final Label statusLabel = new Label();
    private final Button addButton = new Button("Modell hinzufuegen");
    private final Button saveButton = new Button("Speichern");
    private final Button reloadButton = new Button("Neu laden");

    private final GuiModelPriceManagementPort port;
    private final Supplier<Optional<Path>> configPathSupplier;
    private final Set<ModelPriceKey> originalKeys = new HashSet<>();
    private final List<ModelPriceKey> pendingDeletions = new ArrayList<>();

    private final ExecutorService workerExecutor = Executors.newSingleThreadExecutor(r -> {
        Thread t = new Thread(r, "gui-model-prices");
        t.setDaemon(true);
        return t;
    });

    /**
     * Erstellt den Tab und verdrahtet die Bedienelemente.
     *
     * @param port               Bridge-Port fuer DB-Zugriff; darf {@code null} sein (Tab zeigt dann Hinweis)
     * @param configPathSupplier Liefert den aktuell geladenen Konfigurationspfad oder leer; nicht {@code null}
     */
    public GuiModelPricesTab(GuiModelPriceManagementPort port,
                             Supplier<Optional<Path>> configPathSupplier) {
        this.port = port;
        this.configPathSupplier = Objects.requireNonNull(configPathSupplier, "configPathSupplier");
        tab.setClosable(false);
        buildUi();
        updateButtonStates();
    }

    /**
     * Liefert den JavaFX-Tab-Knoten.
     *
     * @return der Tab; nie {@code null}
     */
    public Tab tab() {
        return tab;
    }

    /**
     * Benachrichtigt den Tab ueber eine geaenderte Konfiguration.
     *
     * <p>Aktualisiert den Aktivierungsstatus der Buttons anhand des aktuell vom
     * {@code configPathSupplier} gelieferten Pfads. Muss auf dem JavaFX Application Thread
     * aufgerufen werden.
     */
    public void notifyConfigurationChanged() {
        updateButtonStates();
    }

    /**
     * Triggert ein Neuladen der Tabelle aus der aktuell geladenen Konfiguration.
     */
    public void reloadFromCurrentConfig() {
        Optional<Path> currentPath = configPathSupplier.get();
        if (port == null || currentPath.isEmpty()) {
            Platform.runLater(() -> {
                rows.clear();
                pendingDeletions.clear();
                originalKeys.clear();
                statusLabel.setText("Bitte zuerst eine Konfigurationsdatei laden.");
                updateButtonStates();
            });
            return;
        }
        Path configPath = currentPath.get();
        statusLabel.setText("Lade Modell-Preise ...");
        workerExecutor.submit(() -> {
            try {
                List<ModelPriceView> views = port.findAll(configPath);
                Platform.runLater(() -> applyLoadedRows(views));
            } catch (RuntimeException ex) {
                LOG.error("Modell-Preise konnten nicht geladen werden: {}", ex.getMessage(), ex);
                Platform.runLater(() -> statusLabel.setText("Fehler beim Laden: " + ex.getMessage()));
            }
        });
    }

    private void applyLoadedRows(List<ModelPriceView> views) {
        rows.clear();
        pendingDeletions.clear();
        originalKeys.clear();
        for (ModelPriceView view : views) {
            rows.add(EditableEntry.fromView(view));
            originalKeys.add(new ModelPriceKey(view.provider(), view.modelName()));
        }
        statusLabel.setText("Geladen: " + views.size() + " Eintraege.");
        updateButtonStates();
    }

    private void buildUi() {
        tableView.setItems(rows);
        tableView.setEditable(true);
        tableView.setPlaceholder(new Label("Keine Modell-Preise vorhanden."));

        TableColumn<EditableEntry, String> providerCol = new TableColumn<>("Provider");
        providerCol.setCellValueFactory(c -> c.getValue().providerProperty);
        providerCol.setPrefWidth(150);

        TableColumn<EditableEntry, String> modelCol = new TableColumn<>("Modellname");
        modelCol.setCellValueFactory(c -> c.getValue().modelNameProperty);
        modelCol.setPrefWidth(220);

        TableColumn<EditableEntry, String> inCol = new TableColumn<>("In/1M USD");
        inCol.setCellValueFactory(c -> c.getValue().inputPriceTextProperty);
        inCol.setPrefWidth(120);
        inCol.setCellFactory(col -> new PriceEditCell(true));

        TableColumn<EditableEntry, String> outCol = new TableColumn<>("Out/1M USD");
        outCol.setCellValueFactory(c -> c.getValue().outputPriceTextProperty);
        outCol.setPrefWidth(120);
        outCol.setCellFactory(col -> new PriceEditCell(false));

        TableColumn<EditableEntry, String> currencyCol = new TableColumn<>("Währung");
        currencyCol.setCellValueFactory(c -> c.getValue().currencyProperty);
        currencyCol.setPrefWidth(80);

        TableColumn<EditableEntry, String> updatedCol = new TableColumn<>("Letzte Änderung");
        updatedCol.setCellValueFactory(c -> c.getValue().updatedAtTextProperty);
        updatedCol.setPrefWidth(180);

        TableColumn<EditableEntry, Void> deleteCol = new TableColumn<>("Aktion");
        deleteCol.setCellFactory(col -> new DeleteButtonCell());
        deleteCol.setPrefWidth(80);

        tableView.getColumns().setAll(List.of(providerCol, modelCol, inCol, outCol,
                currencyCol, updatedCol, deleteCol));
        tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_LAST_COLUMN);

        addButton.setOnAction(e -> openAddDialog());
        saveButton.setOnAction(e -> handleSave());
        reloadButton.setOnAction(e -> reloadFromCurrentConfig());

        HBox buttonBar = new HBox(8, addButton, saveButton, reloadButton);
        buttonBar.setAlignment(Pos.CENTER_LEFT);

        VBox.setVgrow(tableView, Priority.ALWAYS);
        VBox content = new VBox(8, tableView, buttonBar, statusLabel);
        content.setPadding(new Insets(12));
        tab.setContent(content);

        statusLabel.setText("Klicken Sie auf \"Neu laden\", um die aktuellen Modell-Preise anzuzeigen.");
    }

    private void updateButtonStates() {
        Optional<Path> path = configPathSupplier.get();
        boolean active = port != null && path.isPresent();
        addButton.setDisable(!active);
        saveButton.setDisable(!active);
        reloadButton.setDisable(!active);
    }

    private void openAddDialog() {
        Dialog<EditableEntry> dialog = new Dialog<>();
        dialog.setTitle("Modell hinzufuegen");
        dialog.setHeaderText("Neuen Modell-Preis erfassen");

        ChoiceBox<String> providerBox = new ChoiceBox<>(FXCollections.observableArrayList(SUPPORTED_PROVIDERS));
        providerBox.getSelectionModel().selectFirst();
        TextField modelField = new TextField();
        TextField inputField = new TextField();
        TextField outputField = new TextField();

        GridPane grid = new GridPane();
        grid.setHgap(8);
        grid.setVgap(8);
        grid.add(new Label("Provider"), 0, 0);
        grid.add(providerBox, 1, 0);
        grid.add(new Label("Modellname"), 0, 1);
        grid.add(modelField, 1, 1);
        grid.add(new Label("In/1M USD"), 0, 2);
        grid.add(inputField, 1, 2);
        grid.add(new Label("Out/1M USD"), 0, 3);
        grid.add(outputField, 1, 3);

        dialog.getDialogPane().setContent(grid);
        dialog.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL);

        dialog.setResultConverter(button -> {
            if (button != ButtonType.OK) {
                return null;
            }
            String provider = providerBox.getValue();
            String modelName = modelField.getText() != null ? modelField.getText().trim() : "";
            if (provider == null || modelName.isEmpty()) {
                showError("Provider und Modellname sind Pflichtfelder.");
                return null;
            }
            try {
                long inputNano = parseUsdPerMillionToNano(inputField.getText());
                long outputNano = parseUsdPerMillionToNano(outputField.getText());
                EditableEntry entry = new EditableEntry(provider, modelName,
                        inputNano, outputNano, "USD", null, false, true);
                return entry;
            } catch (IllegalArgumentException ex) {
                showError(ex.getMessage());
                return null;
            }
        });

        Optional<EditableEntry> result = dialog.showAndWait();
        result.ifPresent(entry -> {
            for (EditableEntry existing : rows) {
                if (existing.providerProperty.get().equals(entry.providerProperty.get())
                        && existing.modelNameProperty.get().equals(entry.modelNameProperty.get())) {
                    showError("Eintrag fuer Provider \"" + entry.providerProperty.get()
                            + "\" und Modell \"" + entry.modelNameProperty.get()
                            + "\" existiert bereits.");
                    return;
                }
            }
            rows.add(entry);
            statusLabel.setText("Neuer Eintrag vorgemerkt; bitte speichern.");
        });
    }

    private void handleSave() {
        Optional<Path> currentPath = configPathSupplier.get();
        if (port == null || currentPath.isEmpty()) {
            return;
        }
        ModelPriceChangeSet changeSet;
        try {
            changeSet = buildChangeSet();
        } catch (IllegalArgumentException ex) {
            showError(ex.getMessage());
            return;
        }
        if (changeSet.isEmpty()) {
            statusLabel.setText("Keine Aenderungen zu speichern.");
            return;
        }
        Path configPath = currentPath.get();
        statusLabel.setText("Speichere ...");
        saveButton.setDisable(true);
        workerExecutor.submit(() -> {
            try {
                port.saveAllChanges(configPath, changeSet);
                Platform.runLater(() -> {
                    statusLabel.setText("Aenderungen gespeichert.");
                    reloadFromCurrentConfig();
                });
            } catch (RuntimeException ex) {
                LOG.error("Modell-Preis-Speichern fehlgeschlagen: {}", ex.getMessage(), ex);
                Platform.runLater(() -> {
                    statusLabel.setText("Fehler beim Speichern: " + ex.getMessage());
                    saveButton.setDisable(false);
                });
            }
        });
    }

    private ModelPriceChangeSet buildChangeSet() {
        List<ModelPriceEntry> upserts = new ArrayList<>();
        Instant placeholder = Instant.now();
        for (EditableEntry row : rows) {
            if (!row.editable) {
                continue;
            }
            if (!row.dirty && originalKeys.contains(
                    new ModelPriceKey(row.providerProperty.get(), row.modelNameProperty.get()))) {
                continue;
            }
            try {
                upserts.add(new ModelPriceEntry(
                        row.providerProperty.get(),
                        row.modelNameProperty.get(),
                        row.inputPriceNanoUsd,
                        row.outputPriceNanoUsd,
                        "USD",
                        placeholder));
            } catch (IllegalArgumentException ex) {
                throw new IllegalArgumentException("Eintrag (" + row.providerProperty.get()
                        + ", " + row.modelNameProperty.get() + ") ungueltig: " + ex.getMessage());
            }
        }
        return new ModelPriceChangeSet(upserts, List.copyOf(pendingDeletions));
    }

    private void showError(String message) {
        Alert alert = new Alert(Alert.AlertType.ERROR, message, ButtonType.OK);
        alert.setHeaderText("Eingabefehler");
        alert.showAndWait();
    }

    /**
     * Konvertiert eine $/1M-Tokens-Eingabe in Nano-USD/Token.
     *
     * <p>Akzeptiert Komma oder Punkt als Dezimaltrenner. Maximal sechs
     * Nachkommastellen sind erlaubt; mehr fuehrt zur
     * {@link IllegalArgumentException}. Negative Werte und Nicht-Numerisches
     * werden ebenfalls abgewiesen.
     *
     * @param raw Eingabetext
     * @return umgerechneter Nano-USD-Wert
     * @throws IllegalArgumentException bei ungueltiger Eingabe
     */
    static long parseUsdPerMillionToNano(String raw) {
        if (raw == null || raw.isBlank()) {
            throw new IllegalArgumentException("Preis darf nicht leer sein.");
        }
        String normalized = raw.trim().replace(',', '.');
        BigDecimal value;
        try {
            value = new BigDecimal(normalized);
        } catch (NumberFormatException nfe) {
            throw new IllegalArgumentException("Preis ist nicht numerisch: " + raw);
        }
        if (value.signum() < 0) {
            throw new IllegalArgumentException("Preis darf nicht negativ sein.");
        }
        if (value.scale() > 6) {
            throw new IllegalArgumentException("Maximal 6 Nachkommastellen erlaubt.");
        }
        BigDecimal nanoPerToken = value.multiply(BigDecimal.valueOf(1000L))
                .setScale(0, RoundingMode.HALF_UP);
        long nanoLong = nanoPerToken.longValueExact();
        if (nanoLong > ModelPriceEntry.MAX_PRICE_PER_TOKEN_NANO_USD) {
            throw new IllegalArgumentException("Preis ueberschreitet Maximum.");
        }
        return nanoLong;
    }

    /**
     * Formatiert einen Nano-USD-Wert als $/1M-Tokens-Text.
     *
     * @param nanoPerToken Nano-USD pro Token
     * @return Formatierter Text mit bis zu sechs Nachkommastellen
     */
    static String formatNanoAsUsdPerMillion(long nanoPerToken) {
        BigDecimal usdPerMillion = BigDecimal.valueOf(nanoPerToken)
                .multiply(NANO_TO_USD_PER_MILLION)
                .divide(new BigDecimal("1000000000"), 6, RoundingMode.HALF_UP)
                .stripTrailingZeros();
        return usdPerMillion.toPlainString();
    }

    /**
     * Mutable Tabellenzeile. Kapselt View-Felder als Properties und einen
     * Dirty-Flag fuer den ChangeSet-Bau.
     */
    private static final class EditableEntry {
        final SimpleStringProperty providerProperty;
        final SimpleStringProperty modelNameProperty;
        final SimpleStringProperty inputPriceTextProperty;
        final SimpleStringProperty outputPriceTextProperty;
        final SimpleStringProperty currencyProperty;
        final SimpleStringProperty updatedAtTextProperty;
        final SimpleObjectProperty<Boolean> invalidUpdatedAtProperty;
        long inputPriceNanoUsd;
        long outputPriceNanoUsd;
        boolean editable;
        boolean dirty;

        EditableEntry(String provider, String modelName,
                      long inputNano, long outputNano, String currency,
                      Instant updatedAt, boolean invalidUpdatedAt, boolean editable) {
            this.providerProperty = new SimpleStringProperty(provider);
            this.modelNameProperty = new SimpleStringProperty(modelName);
            this.inputPriceNanoUsd = inputNano;
            this.outputPriceNanoUsd = outputNano;
            this.inputPriceTextProperty = new SimpleStringProperty(formatNanoAsUsdPerMillion(inputNano));
            this.outputPriceTextProperty = new SimpleStringProperty(formatNanoAsUsdPerMillion(outputNano));
            this.currencyProperty = new SimpleStringProperty(currency);
            this.updatedAtTextProperty = new SimpleStringProperty(
                    invalidUpdatedAt ? "ungueltig"
                            : updatedAt == null ? "" : GERMAN_DATE_TIME.format(updatedAt.atZone(ZoneId.systemDefault())));
            this.invalidUpdatedAtProperty = new SimpleObjectProperty<>(invalidUpdatedAt);
            this.editable = editable;
            this.dirty = false;
        }

        static EditableEntry fromView(ModelPriceView view) {
            boolean editable = SUPPORTED_PROVIDERS.contains(view.provider());
            EditableEntry entry = new EditableEntry(
                    view.provider(), view.modelName(),
                    view.priceInputPerTokenNanoUsd(), view.priceOutputPerTokenNanoUsd(),
                    view.currency(), view.updatedAt(), view.invalidUpdatedAt(), editable);
            return entry;
        }
    }

    /**
     * Editierbare Zelle fuer Input-/Output-Preisfelder.
     */
    private final class PriceEditCell extends TableCell<EditableEntry, String> {
        private final TextField textField = new TextField();
        private final boolean isInputColumn;

        PriceEditCell(boolean isInputColumn) {
            this.isInputColumn = isInputColumn;
            textField.focusedProperty().addListener((obs, was, focused) -> {
                if (!focused) {
                    commit();
                }
            });
            textField.setOnAction(e -> commit());
        }

        private void commit() {
            EditableEntry row = getTableRow() != null ? getTableRow().getItem() : null;
            if (row == null || !row.editable) {
                return;
            }
            String text = textField.getText();
            try {
                long nano = parseUsdPerMillionToNano(text);
                if (isInputColumn) {
                    if (row.inputPriceNanoUsd != nano) {
                        row.inputPriceNanoUsd = nano;
                        row.dirty = true;
                    }
                    row.inputPriceTextProperty.set(formatNanoAsUsdPerMillion(nano));
                } else {
                    if (row.outputPriceNanoUsd != nano) {
                        row.outputPriceNanoUsd = nano;
                        row.dirty = true;
                    }
                    row.outputPriceTextProperty.set(formatNanoAsUsdPerMillion(nano));
                }
            } catch (IllegalArgumentException ex) {
                showError(ex.getMessage());
                String revert = isInputColumn
                        ? formatNanoAsUsdPerMillion(row.inputPriceNanoUsd)
                        : formatNanoAsUsdPerMillion(row.outputPriceNanoUsd);
                textField.setText(revert);
            }
        }

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (empty || item == null) {
                setText(null);
                setGraphic(null);
                return;
            }
            EditableEntry row = getTableRow() != null ? getTableRow().getItem() : null;
            if (row != null && !row.editable) {
                setText(item);
                setGraphic(null);
                setTooltip(new Tooltip("Unbekannter Provider – Bearbeitung in V3.3 nicht unterstuetzt."));
                return;
            }
            textField.setText(item);
            setText(null);
            setGraphic(textField);
        }
    }

    /**
     * Loesch-Button-Spalte mit Bestaetigungsdialog.
     */
    private final class DeleteButtonCell extends TableCell<EditableEntry, Void> {
        private final Button button = new Button("Loeschen");

        DeleteButtonCell() {
            button.setOnAction(e -> {
                EditableEntry row = getTableRow() != null ? getTableRow().getItem() : null;
                if (row == null) {
                    return;
                }
                String message = String.format(Locale.GERMAN,
                        "Eintrag fuer Provider \"%s\" und Modell \"%s\" wirklich loeschen?",
                        row.providerProperty.get(), row.modelNameProperty.get());
                Alert alert = new Alert(Alert.AlertType.CONFIRMATION, message, ButtonType.OK, ButtonType.CANCEL);
                alert.setHeaderText("Loeschen bestaetigen");
                alert.showAndWait().ifPresent(button -> {
                    if (button == ButtonType.OK) {
                        ModelPriceKey key = new ModelPriceKey(
                                row.providerProperty.get(), row.modelNameProperty.get());
                        if (originalKeys.contains(key)) {
                            pendingDeletions.add(key);
                        }
                        rows.remove(row);
                    }
                });
            });
        }

        @Override
        protected void updateItem(Void item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setGraphic(null);
            } else {
                setGraphic(button);
            }
        }
    }
}
