package com.andrewlalis.perfin.control;

import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
import com.andrewlalis.perfin.data.SavedQueryRepository;
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
import com.andrewlalis.perfin.data.util.FileUtil;
import com.andrewlalis.perfin.model.Profile;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Modality;
import javafx.stage.StageStyle;
import javafx.stage.Window;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Controller for the SQL Console View, in which the user can write and execute
 * arbitrary SQL queries on the database. This allows power users to create
 * custom analytics queries and get exactly the data they want, without fiddling
 * with user-friendly search fields.
 */
public class SqlConsoleViewController implements RouteSelectionListener {

    @FXML public TextArea sqlEditorTextArea;
    @FXML public TextArea outputTextArea;
    @FXML public VBox savedQueriesVBox;

    @Override
    public void onRouteSelected(Object context) {
        sqlEditorTextArea.clear();
        outputTextArea.clear();
        refreshSavedQueries();
    }

    @FXML public void executeQuery() {
        List<String> queries = getCurrentQueries();
        outputTextArea.clear();
        JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
        try (
                var conn = dataSource.getConnection();
                var stmt = conn.createStatement()
        ) {
            StringBuilder sb = new StringBuilder();
            for (int queryIdx = 0; queryIdx < queries.size(); queryIdx++) {
                sb.append("Query ").append(queryIdx + 1).append(" of ").append(queries.size()).append(":\n");
                String query = queries.get(queryIdx);
                ResultSet rs = stmt.executeQuery(query);
                int columnCount = rs.getMetaData().getColumnCount();

                for (int i = 1; i <= columnCount; i++) {
                    sb.append(rs.getMetaData().getColumnLabel(i));
                    if (i < columnCount) sb.append(", ");
                }
                sb.append('\n');
                while (rs.next()) {
                    for (int i = 1; i <= columnCount; i++) {
                        sb.append(rs.getString(i));
                        if (i < columnCount) sb.append(", ");
                    }
                    sb.append('\n');
                }
                if (queryIdx < queries.size() - 1) {
                    sb.append("-----\n\n");
                }
            }
            outputTextArea.setText(sb.toString());
        } catch (SQLException e) {
            outputTextArea.setText("Error: " + e.getMessage());
        }
    }

    @FXML public void saveQuery() {
        if (sqlEditorTextArea.getText().isBlank()) {
            Popups.message(sqlEditorTextArea, "Cannot save an empty query.");
            return;
        }
        TextInputDialog dialog = new TextInputDialog();
        dialog.setTitle("Save Query");
        dialog.setContentText("Enter a name for this query.");
        Optional<String> result = dialog.showAndWait();
        if (result.isPresent()) {
            SavedQueryRepository repo = Profile.getCurrent().dataSource().getSavedQueryRepository();
            String name = result.get().strip();
            if (
                repo.getSavedQueries().contains(name) &&
                !Popups.confirm(sqlEditorTextArea, "Are you sure you want to overwrite this saved query?")
            ) {
                return;
            }
            String content = sqlEditorTextArea.getText().strip();
            repo.createSavedQuery(name, content);
            refreshSavedQueries();
        }
    }

    @FXML public void exportToFile() {
        if (sqlEditorTextArea.getText().isBlank()) {
            Popups.message(sqlEditorTextArea, "Cannot export the results of an empty query.");
            return;
        }
        if (getCurrentQueries().size() > 1) {
            Popups.message(sqlEditorTextArea, "Note: Export to file will only export the results of your first query.");
        }

        FileChooser fileChooser = new FileChooser();
        fileChooser.setTitle("Export to File");
        fileChooser.setInitialFileName("export.csv");
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
                "CSV Files", ".csv"
        ));
        fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(
                "JSON Files", ".json"
        ));
        fileChooser.setInitialDirectory(Profile.getCurrent().dataSource().getContentDir().toFile());
        File chosenFile = fileChooser.showSaveDialog(sqlEditorTextArea.getScene().getWindow());
        if (chosenFile == null) return;

        String name = chosenFile.getName().strip().toLowerCase();
        if (!name.endsWith(".csv") && !name.endsWith(".json")) {
            Popups.error(sqlEditorTextArea, "Invalid file format. Only CSV and JSON are permitted.");
            return;
        }
        String query = getCurrentQueries().getFirst();
        JdbcDataSource dataSource = (JdbcDataSource) Profile.getCurrent().dataSource();
        try (
                var conn = dataSource.getConnection();
                var stmt = conn.createStatement()
        ) {
            ResultSet rs = stmt.executeQuery(query);
            if (name.endsWith(".csv")) {
                writeQueryResultsToCsv(rs, chosenFile.toPath());
            } else if (name.endsWith(".json")) {
//                writeQueryResultsToJson(rs, chosenFile.toPath());
                Popups.message(sqlEditorTextArea, "JSON not yet supported.");
            }
        } catch (Exception e) {
            Popups.error(sqlEditorTextArea, e);
        }
    }

    private void writeQueryResultsToCsv(ResultSet rs, Path file) throws SQLException, IOException {
        try (var out = Files.newOutputStream(file); var writer = new PrintWriter(out)) {
            final int columnCount = rs.getMetaData().getColumnCount();
            // First write the header.
            for (int i = 1; i <= columnCount; i++) {
                writer.append(FileUtil.escapeCSVText(rs.getMetaData().getColumnLabel(i)));
                if (i < columnCount) writer.append(',');
            }
            writer.println();
            // Then write the body rows.
            while (rs.next()) {
                for (int i = 1; i <= columnCount; i++) {
                    writer.append(FileUtil.escapeCSVText(rs.getString(i)));
                    if (i < columnCount) writer.append(',');
                }
                writer.println();
            }
        }
    }

    private void refreshSavedQueries() {
        savedQueriesVBox.getChildren().clear();
        List<String> savedQueries = Profile.getCurrent().dataSource()
                .getSavedQueryRepository().getSavedQueries();
        savedQueriesVBox.getChildren().addAll(savedQueries.stream().map(this::makeQueryTile).toList());
    }

    private Node makeQueryTile(String name) {
        AnchorPane pane = new AnchorPane();
        pane.getStyleClass().addAll("tile");
        Label nameLabel = new Label(name);
        AnchorPane.setLeftAnchor(nameLabel, 0.0);
        AnchorPane.setTopAnchor(nameLabel, 0.0);
        AnchorPane.setBottomAnchor(nameLabel, 0.0);
        pane.getChildren().add(nameLabel);

        HBox buttonsBox = new HBox();
        buttonsBox.getStyleClass().addAll("std-spacing", "small-font");
        Button loadButton = new Button("Load");
        loadButton.setOnAction(event -> sqlEditorTextArea.setText(
                Profile.getCurrent().dataSource().getSavedQueryRepository()
                        .getSavedQueryContent(name)
        ));
        buttonsBox.getChildren().add(loadButton);
        Button deleteButton = new Button("Delete");
        deleteButton.setOnAction(event -> {
            if (Popups.confirm(pane, "Are you sure you want to delete this query?")) {
                Profile.getCurrent().dataSource().getSavedQueryRepository().deleteSavedQuery(name);
                refreshSavedQueries();
            }
        });
        buttonsBox.getChildren().add(deleteButton);

        AnchorPane.setRightAnchor(buttonsBox, 0.0);
        AnchorPane.setTopAnchor(buttonsBox, 0.0);
        AnchorPane.setBottomAnchor(buttonsBox, 0.0);
        pane.getChildren().add(buttonsBox);
        return pane;
    }

    private List<String> getCurrentQueries() {
        String queryText = sqlEditorTextArea.getText().strip();
        String[] rawQueries = queryText.split("\\s*;\\s*");
        return Arrays.stream(rawQueries)
                .filter(s -> !s.isBlank())
                .filter(s -> !s.startsWith("#") && !s.startsWith("//"))
                .toList();
    }

    @FXML public void showSchema() {
        SchemaDialog dialog = new SchemaDialog(sqlEditorTextArea.getScene().getWindow());
        dialog.show();
    }

    private static class SchemaDialog extends Dialog<Void> {
        public SchemaDialog(Window owner) {
            DialogPane pane = new DialogPane();
            TextArea schemaTextArea = new TextArea();
            schemaTextArea.setEditable(false);
            schemaTextArea.getStyleClass().addAll("mono-font", "small-font");
            try (var in = SqlConsoleViewController.class.getResourceAsStream("/sql/schema.sql")) {
                if (in == null) throw new IOException("Could not load database schema from resource location.");
                String schemaStr = new String(in.readAllBytes(), StandardCharsets.UTF_8);
                schemaTextArea.setText(schemaStr);
            } catch (IOException e) {
                schemaTextArea.setText("Failed to load schema file!");
            }
            pane.setContent(schemaTextArea);
            pane.getButtonTypes().add(ButtonType.OK);

            initOwner(owner);
            initModality(Modality.NONE);
            initStyle(StageStyle.DECORATED);
            setResizable(true);
            setTitle("Perfin Database Schema");
            setDialogPane(pane);
        }
    }
}
