package ru.yandex.qe.dispenser.domain.dao.entity.meta;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collections;
import java.util.Map;
import java.util.Set;

import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.EmptyResultDataAccessException;

import ru.yandex.qe.dispenser.domain.EntityMetaType;
import ru.yandex.qe.dispenser.domain.EntitySpec;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.dao.SqlUtils;
import ru.yandex.qe.dispenser.domain.dao.entity.SqlEntityDaoUtils;
import ru.yandex.qe.dispenser.domain.hierarchy.Hierarchy;

import static ru.yandex.qe.dispenser.domain.dao.entity.SqlEntityDaoUtils.formatEntityUsageTable;
import static ru.yandex.qe.dispenser.domain.dao.entity.SqlEntityDaoUtils.toEntityUsageTableName;

public final class SqlEntityMetaTypeDao extends SqlDaoBase implements EntityMetaTypeDao {
    private static final String CREATE_META_TYPES_QUERY = "INSERT INTO entity_meta_type (key, entity_spec_id, data_type) VALUES (:key, :entitySpecId, :dataType)";

    private static final String GET_QUERY = "SELECT * FROM entity_meta_type";
    private static final String READ_BY_ID_QUERY = GET_QUERY + " WHERE id = :id";

    private static final String UPDATE_QUERY = "UPDATE entity_meta_type SET key = :key, entity_spec_id = :entitySpecId, data_type = :dataType WHERE id = :id";

    private static final String DELETE_QUERY = "DELETE FROM entity_meta_type WHERE id = (:id)";
    private static final String CLEAR_QUERY = "TRUNCATE entity_meta_type CASCADE";

    private static final String ADD_COLUMN_TO_USAGES_TABLE_QUERY = "ALTER TABLE %s ADD COLUMN %s %s";
    private static final String ADD_UNIQUE_INDEX_TO_USAGES_TABLE_QUERY = "CREATE UNIQUE INDEX %s ON %s (entity_id, project_id, %s)";
    private static final String ADD_CONSTRAINT_TO_USAGES_TABLE_QUERY = "ALTER TABLE %s ADD CONSTRAINT %s UNIQUE USING INDEX %s";

    private static final String CHANGE_COLUMN_NAME = "ALTER TABLE %s RENAME :prevKey TO :key";
    private static final String DROP_COLUMN = "ALTER TABLE %s DROP COLUMN %s";

    @NotNull
    @Override
    public Set<EntityMetaType<?>> getAll() {
        return jdbcTemplate.queryForSet(GET_QUERY, this::toEntitySpec);
    }

    @NotNull
    @Override
    public EntityMetaType<?> create(@NotNull final EntityMetaType<?> metaType) {
        final String newColumn = metaType.getKey().getPublicKey();
        final String dataType = SqlUtils.toSqlType(metaType.getDataType());
        final EntitySpec spec = metaType.getSpec();
        final String usagesTableName = toEntityUsageTableName(spec);
        final String indexName = SqlEntityDaoUtils.usagesIndexName(spec);
        final String constraintName = SqlEntityDaoUtils.usagesConstraintName(spec);

        metaType.setId(jdbcTemplate.insert(CREATE_META_TYPES_QUERY, toParams(metaType)));
        jdbcTemplate.update(String.format(ADD_COLUMN_TO_USAGES_TABLE_QUERY, usagesTableName, newColumn, dataType));
        jdbcTemplate.update(String.format(ADD_UNIQUE_INDEX_TO_USAGES_TABLE_QUERY, indexName, usagesTableName, newColumn));
        jdbcTemplate.update(String.format(ADD_CONSTRAINT_TO_USAGES_TABLE_QUERY, usagesTableName, constraintName, indexName));

        return metaType;
    }

    @NotNull
    @Override
    public EntityMetaType<?> read(@NotNull final Long id) throws EmptyResultDataAccessException {
        return jdbcTemplate.queryForOptional(READ_BY_ID_QUERY, Collections.singletonMap("id", id), this::toEntitySpec)
                .orElseThrow(() -> new EmptyResultDataAccessException("No entity meta type with id " + id, 1));
    }

    @Override
    public boolean update(@NotNull final EntityMetaType<?> metaType) {
        final EntityMetaType<?> prevType = read(metaType.getId());
        final Map<String, ?> params = ImmutableMap.of("prevKey", prevType.getKey().getPublicKey(), "key", metaType.getKey().getPublicKey());
        jdbcTemplate.update(formatEntityUsageTable(CHANGE_COLUMN_NAME, metaType.getSpec()), params);
        return jdbcTemplate.update(UPDATE_QUERY, toParams(metaType)) > 0;
    }

    @Override
    public boolean delete(@NotNull final EntityMetaType<?> metaType) {
        jdbcTemplate.update(String.format(DROP_COLUMN, toEntityUsageTableName(metaType.getSpec()), metaType.getKey().getPublicKey()));
        return jdbcTemplate.update(DELETE_QUERY, "id", metaType.getId()) > 0;
    }

    @NotNull
    private Map<String, ?> toParams(@NotNull final EntityMetaType<?> metaType) {
        return ImmutableMap.<String, Object>builder()
                .put("key", metaType.getKey().getPublicKey())
                .put("entitySpecId", metaType.getSpec().getId())
                .put("dataType", SqlUtils.toSqlType(metaType.getDataType()))
                .build();
    }

    @NotNull
    private EntityMetaType<?> toEntitySpec(@NotNull final ResultSet rs, final int i) throws SQLException {
        final EntitySpec spec = Hierarchy.get().getEntitySpecReader().read(rs.getLong("entity_spec_id"));
        final String key = rs.getString("key");
        final Class<?> dataType = SqlUtils.fromSqlType(rs.getString("data_type"));
        return new EntityMetaType<>(key, dataType, spec);
    }
}
