package com.atlassian.stash.internal.backup.liquibase;

import com.atlassian.stash.internal.liquibase.LiquibaseUtils;
import liquibase.database.Database;
import liquibase.database.structure.Column;
import liquibase.database.structure.Table;
import liquibase.database.typeconversion.TypeConverter;
import liquibase.database.typeconversion.TypeConverterFactory;
import liquibase.exception.DatabaseException;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.DatabaseSnapshotGeneratorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.CleanupFailureDataAccessException;

import javax.annotation.Nonnull;
import javax.sql.DataSource;
import java.sql.Connection;
import java.util.function.Function;

public class DefaultLiquibaseSession implements LiquibaseSession {

    private static final Logger log = LoggerFactory.getLogger(DefaultLiquibaseSession.class);

    /**
     * A function that returns the lowercased name of a Liquibase table passed to it.
     */
    public static final Function<Table, String> TO_LOWERCASE_TABLE_NAME = table -> table.getName().toLowerCase();
    /**
     * A function that returns the lowercased name of a Liquibase table column passed to it.
     */
    public static final Function<Column, String> TO_LOWERCASE_COLUMN_NAME = column -> column.getName().toLowerCase();

    private final Database database;
    
    /**
     * A lazily-loaded snapshot of the underlying database structure, used to provide metadata for backup-related
     * operations.
     */
    private DatabaseSnapshot snapshot;
    
    /**
     * The number of changes executed in the context of the current database transaction.
     */
    private long changeCount;

    private TypeConverter cachedTypeConverter;


    public DefaultLiquibaseSession(DataSource dataSource) {
        this.database = LiquibaseUtils.findDatabase(dataSource);
    }

    public DefaultLiquibaseSession(Connection connection) {
        this.database = LiquibaseUtils.findDatabaseForConnection(connection);
    }

    @Override
    public Database getDatabase() {
        return database;
    }

    @Override
    public DatabaseSnapshot getSnapshot() {
        if (snapshot == null) {
            log.info("Examining structure of source database");
            DatabaseSnapshotGeneratorFactory snapShotFactory = DatabaseSnapshotGeneratorFactory.getInstance();
            try {
                snapshot = snapShotFactory.createSnapshot(database, null, null);
            } catch (DatabaseException e) {
                throw new LiquibaseDataAccessException("Failed to obtain snapshot", database, e);
            }
        }
        return snapshot;
    }

    @Override
    public void incrementChangeCount() {
        changeCount++;
    }

    @Override
    public long getChangeCount() {
        return changeCount;
    }

    @Nonnull
    @Override
    public TypeConverter getTypeConverter() {
        if (cachedTypeConverter == null) {
            cachedTypeConverter = TypeConverterFactory.getInstance().findTypeConverter(getDatabase());
        }
        return cachedTypeConverter;
    }

    @Override
    public void resetChangeCount() {
        changeCount = 0;
    }

    @Override
    public void close() {
        log.debug("Closing Liquibase");
        try {
            // release resources
            getDatabase().close();
            log.debug("Liquibase closed");
        } catch (DatabaseException e) {
            throw new CleanupFailureDataAccessException("Failed to close database", e);
        }
    }
}
