package leigh.gnusto;

import leigh.mecha.log.MechaLogger;
import leigh.mecha.log.MechaLoggerFactory;
import org.apache.commons.io.FilenameUtils;
import org.apache.ibatis.jdbc.ScriptRunner;

import java.io.*;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

/**
 * Gnusto provides a simple framework for performing SQL schema migrations on a MySQL database.
 *
 * @author Alex Leigh
 */
public class Gnusto {
    private static final MechaLogger logger = MechaLoggerFactory.getLogger(Gnusto.class);

    /**
     * Prepare a database for use by Gnusto. This installs the Gnusto dictionary table(s) as required. If
     * a Gnusto catalog is already installed in the database, it will be overwritten by this method.
     */
    public void prepare(Connection con) throws SQLException, IOException {
        SqlMigration migration = new SqlMigration("gnusto", 1) {
            @Override
            InputStream getInputStream() {
                return Gnusto.class.getResourceAsStream("/leigh/gnusto/mysql/schema-1.sql");
            }
        };
        migrate(con, migration);
    }

    /**
     * Returns the stepping for the given schema, or <code>-1</code> if this schema is not already installed
     * in the database. This method requires that the <code>gnu_schema_t</code> table already be installed
     * in the database, or a SQL exception is likely to be thrown.
     */
    public int getSchemaStep(Connection con, String schemaName) throws SQLException {
        try (PreparedStatement ps = con.prepareStatement("SELECT MAX(step) FROM gnu_schema_t WHERE nm=?")) {
            ps.setString(1, schemaName);
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return rs.getInt(1);
                }
            }
        }
        return -1;
    }

    /**
     * Attempt to run the given migration against the given connection. If the script successfully runs,
     * the gnusto schema table will be updated to reflect that the new schema was installed.
     */
    public void migrate(Connection con, SqlMigration migration) throws SQLException, FileNotFoundException {
        logger.info("Running migration: {}", migration);
        ScriptRunner sr = new ScriptRunner(con);
        Reader reader = new InputStreamReader(new BufferedInputStream(migration.getInputStream()));
        sr.runScript(reader);
        try (PreparedStatement ps = con.prepareStatement("INSERT INTO gnu_schema_t (nm,step) VALUES (?,?)")) {
            ps.setString(1, migration.getName());
            ps.setInt(2, migration.getStep());
            logger.info("Updating catalog. [nm: {}] [step: {}]", migration.getName(), migration.getStep());
            ps.execute();
        }
    }

    /**
     * Attempt to run the given migrations against the given connection. Loading multiple schemas at the same
     * time is supported, but for a given step, the order in which the different schemas will be migrated is
     * undefined.
     */
    public void migrate(Connection con, List<SqlMigration> migrations) throws SQLException, FileNotFoundException {
        ArrayList<SqlMigration> sorted = new ArrayList<>(migrations);
        sorted.sort((o1, o2) -> ((Integer) o1.getStep()).compareTo(o2.getStep()));

        for (SqlMigration migration : migrations) {
            int dbStep = getSchemaStep(con, migration.getName());
            if (dbStep < migration.getStep()) {
                migrate(con, migration);
            }
        }
    }

    public void migrate(Connection con, Path base) throws IOException, SQLException {
        ArrayList<SqlMigration> migrations = new ArrayList<>();

        Files.walkFileTree(base, new SimpleFileVisitor<>() {
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
                if (file.toString().endsWith(".sql")) {
                    String basename = FilenameUtils.removeExtension(file.getFileName().toString());
                    logger.info("Found migration. [basename: {}]", basename);
                    String[] fn = basename.split("-");
                    migrations.add(new SqlMigration(fn[0], Integer.parseInt(fn[1])) {
                        @Override
                        InputStream getInputStream() throws FileNotFoundException {
                            return new FileInputStream(file.toFile());
                        }
                    });
                }
                return FileVisitResult.CONTINUE;
            }
        });

        migrate(con, migrations);
    }
}
