package eu.mhsl.minecraft.pixelblocks;

import eu.mhsl.minecraft.pixelblocks.pixelblock.PixelBlock;
import eu.mhsl.minecraft.pixelblocks.utils.Direction;
import org.bukkit.Bukkit;
import org.bukkit.Location;

import java.sql.*;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;

public class PixelBlockDatabase {
    private final Connection db;

    // Alle Statement-Zugriffe nach dem Startup laufen ausschließlich auf diesem Thread,
    // da die geteilten PreparedStatements nicht threadsicher sind.
    private final ExecutorService executor =
        Executors.newSingleThreadExecutor(runnable -> new Thread(runnable, "PixelBlocks-Database"));

    private final PreparedStatement getAllPixelBlocks;
    private final PreparedStatement deletePixelBlock;
    private final PreparedStatement insertOrReplacePixelBlock;

    private record PixelBlockRow(
        String uuid,
        String owner,
        String worldName,
        double x,
        double y,
        double z,
        String entryWorldName,
        double entryX,
        double entryY,
        double entryZ,
        String direction
    ) {
    }

    public PixelBlockDatabase(String url) {
        try {
            Class.forName("org.sqlite.JDBC");
            this.db = DriverManager.getConnection(url);

            this.db.createStatement().execute(
                "CREATE TABLE IF NOT EXISTS pixelblocks (" +
                    "uuid CHAR(36) PRIMARY KEY, " +
                    "owner CHAR(36), " +
                    "locationWorldName CHAR(36), " +
                    "locationX DOUBLE, " +
                    "locationY DOUBLE, " +
                    "locationZ DOUBLE, " +
                    "entryLocationWorldName CHAR(36), " +
                    "entryLocationX DOUBLE, " +
                    "entryLocationY DOUBLE, " +
                    "entryLocationZ DOUBLE, " +
                    "direction CHAR(36)" +
                    ")"
            );

            this.deletePixelBlock = this.db.prepareStatement("DELETE FROM pixelblocks WHERE uuid = ?");
            this.getAllPixelBlocks = this.db.prepareStatement("SELECT * FROM pixelblocks");
            this.insertOrReplacePixelBlock = this.db.prepareStatement(
                "INSERT OR REPLACE INTO pixelblocks(uuid, owner, " +
                    "locationWorldName, locationX, locationY, locationZ, " +
                    "entryLocationWorldName, entryLocationX, entryLocationY, entryLocationZ, direction) " +
                    "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"
            );

        } catch(SQLException | RuntimeException | ClassNotFoundException e) {
            throw new RuntimeException("Error while initializing database", e);
        }
    }

    public void close() {
        this.executor.shutdown();
        try {
            if(!this.executor.awaitTermination(10, TimeUnit.SECONDS)) {
                Main.logger().warning("Database executor did not terminate in time, forcing shutdown");
                this.executor.shutdownNow();
            }
        } catch(InterruptedException e) {
            Thread.currentThread().interrupt();
            this.executor.shutdownNow();
        }

        try {
            this.deletePixelBlock.close();
            this.getAllPixelBlocks.close();
            this.insertOrReplacePixelBlock.close();
            this.db.close();
        } catch(SQLException e) {
            Main.logger().log(Level.SEVERE, "Failed closing the database", e);
        }
    }

    public void deletePixelBlock(PixelBlock pixelBlock) {
        String uuid = pixelBlock.getBlockUUID().toString();
        this.executor.execute(() -> {
            try {
                this.deletePixelBlock.setString(1, uuid);
                this.deletePixelBlock.executeUpdate();
            } catch(SQLException e) {
                Main.logger().log(Level.SEVERE, String.format("Failed to delete PixelBlock '%s' from the database", uuid), e);
            }
        });
    }

    public void savePixelBlock(PixelBlock pixelBlock) {
        Location blockLocation = pixelBlock.getPixelBlockLocation();
        Location entryLocation = pixelBlock.hasLastEntryLocation() ? pixelBlock.getLastEntryLocation() : blockLocation;

        PixelBlockRow row = new PixelBlockRow(
            pixelBlock.getBlockUUID().toString(),
            pixelBlock.getOwnerUUID().toString(),
            blockLocation.getWorld().getName(),
            blockLocation.getX(),
            blockLocation.getY(),
            blockLocation.getZ(),
            entryLocation.getWorld().getName(),
            entryLocation.getX(),
            entryLocation.getY(),
            entryLocation.getZ(),
            pixelBlock.getFacingDirection().toString()
        );

        this.executor.execute(() -> {
            try {
                this.insertOrReplacePixelBlock.setString(1, row.uuid());
                this.insertOrReplacePixelBlock.setString(2, row.owner());

                this.insertOrReplacePixelBlock.setString(3, row.worldName());
                this.insertOrReplacePixelBlock.setDouble(4, row.x());
                this.insertOrReplacePixelBlock.setDouble(5, row.y());
                this.insertOrReplacePixelBlock.setDouble(6, row.z());

                this.insertOrReplacePixelBlock.setString(7, row.entryWorldName());
                this.insertOrReplacePixelBlock.setDouble(8, row.entryX());
                this.insertOrReplacePixelBlock.setDouble(9, row.entryY());
                this.insertOrReplacePixelBlock.setDouble(10, row.entryZ());

                this.insertOrReplacePixelBlock.setString(11, row.direction());

                this.insertOrReplacePixelBlock.executeUpdate();
            } catch(SQLException e) {
                Main.logger().log(Level.SEVERE, String.format("Failed to create or update PixelBlock '%s' in the database", row.uuid()), e);
            }
        });
    }

    public void loadPixelBlocks() {
        try {
            ResultSet allPixelBlocks = this.getAllPixelBlocks.executeQuery();

            while(allPixelBlocks.next()) {
                String uuid = allPixelBlocks.getString("uuid");
                try {
                    Location blockLocation = new Location(
                        Bukkit.getWorld(allPixelBlocks.getString("locationWorldName")),
                        allPixelBlocks.getDouble("locationX"),
                        allPixelBlocks.getDouble("locationY"),
                        allPixelBlocks.getDouble("locationZ")
                    );

                    Location entryLocation = new Location(
                        Bukkit.getWorld(allPixelBlocks.getString("entryLocationWorldName")),
                        allPixelBlocks.getDouble("entryLocationX"),
                        allPixelBlocks.getDouble("entryLocationY"),
                        allPixelBlocks.getDouble("entryLocationZ")
                    );

                    Main.pixelBlocks.add(PixelBlock.fromExisting(
                        UUID.fromString(uuid),
                        UUID.fromString(allPixelBlocks.getString("owner")),
                        blockLocation,
                        Direction.valueOf(allPixelBlocks.getString("direction")),
                        entryLocation
                    ));
                } catch(Exception e) {
                    Main.logger().log(Level.SEVERE, String.format("Failed initializing existing pixelblock '%s'", uuid), e);
                }
            }
        } catch(SQLException e) {
            throw new RuntimeException("Failed loading PixelBlocks from the database", e);
        }
    }
}
