package ru.yandex.qe.dispenser.domain.dao.goal;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.collect.ImmutableMap;
import org.jetbrains.annotations.NotNull;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;

import ru.yandex.inside.goals.model.Goal.Importance;
import ru.yandex.inside.goals.model.Goal.Status;
import ru.yandex.qe.dispenser.domain.dao.SqlDaoBase;
import ru.yandex.qe.dispenser.domain.exception.SingleMessageException;
import ru.yandex.qe.dispenser.domain.index.LongIndexBase;

@ParametersAreNonnullByDefault
public class SqlGoalDao extends SqlDaoBase implements GoalDao {

    private static final String GET_ALL_QUERY = "SELECT * FROM goal";
    private static final String BASE_INSERT_QUERY = "INSERT INTO goal (id, name, status, importance) " +
            "VALUES (:id, :name, cast(:status as goal_status), cast(:importance as goal_importance))";
    private static final String INSERT_QUERY = "INSERT INTO goal (id, name, status, importance, " +
            "value_stream_goal_id, umbrella_goal_id, contour_goal_id) " +
            "VALUES (:id, :name, cast(:status as goal_status), cast(:importance as goal_importance), " +
            ":valueStreamGoalId, :umbrellaGoalId, :contourGoalId)";
    private static final String GET_BY_ID_QUERY = GET_ALL_QUERY + " WHERE id IN (:id)";
    private static final String UPDATE_SET = "SET name = :name,  status = cast(:status as goal_status), "
            + "importance = cast(:importance as goal_importance) WHERE goal.id = :id";
    private static final String UPDATE_QUERY = "UPDATE goal " + UPDATE_SET;
    private static final String UPSERT = BASE_INSERT_QUERY + " ON CONFLICT (id) DO UPDATE " + UPDATE_SET;
    private static final String DELETE_QUERY = "DELETE FROM goal WHERE id = :id";
    private static final String CLEAR_QUERY = "TRUNCATE goal CASCADE";
    private static final String SET_OKR_ANCESTOR_IDS_BY_GOAL_ID = "UPDATE goal SET value_stream_goal_id = :valueStreamGoalId, " +
            "umbrella_goal_id = :umbrellaGoalId, contour_goal_id = :contourGoalId WHERE id = :goalId";

    @Override
    @NotNull
    public Set<Goal> getAll() {
        return jdbcTemplate.queryForSet(GET_ALL_QUERY, SqlGoalDao::toGoal);
    }


    @NotNull
    @Override
    public Goal create(final Goal goal) {
        jdbcTemplate.update(INSERT_QUERY, toParams(goal));
        return goal;
    }

    @Override
    public boolean upsert(final Collection<BaseGoal> goals) {
        final MapSqlParameterSource[] mapSqlParameterSources = goals.stream()
                .map(SqlGoalDao::toBaseParams)
                .map(MapSqlParameterSource::new)
                .toArray(MapSqlParameterSource[]::new);

        jdbcTemplate.batchUpdate(UPSERT, mapSqlParameterSources);
        return true;
    }

    @Override
    public void updateOrkAncestors(final Long goalId, final OkrAncestors okrAncestors) {
        final HashMap<String, Object> params = toOkrParams(okrAncestors);
        params.put("goalId", goalId);
        jdbcTemplate.update(SET_OKR_ANCESTOR_IDS_BY_GOAL_ID, params);
    }

    @NotNull
    @Override
    public Goal read(final Long id) throws SingleMessageException {
        return jdbcTemplate.queryForOptional(GET_BY_ID_QUERY, Collections.singletonMap("id", id), SqlGoalDao::toGoal)
                .orElseThrow(() -> SingleMessageException.illegalArgument("goal.not.found.try.again.later", id));
    }

    @NotNull
    @Override
    public Map<Long, Goal> read(@NotNull final Collection<Long> ids) {
        if (ids.isEmpty()) {
            return Collections.emptyMap();
        }

        final Map<Long, Goal> goalsById = jdbcTemplate.queryForSet(GET_BY_ID_QUERY, Collections.singletonMap("id", ids), SqlGoalDao::toGoal)
                .stream()
                .collect(Collectors.toMap(LongIndexBase::getId, Function.identity()));

        for (final Long id : ids) {
            if (!goalsById.containsKey(id)) {
                throw SingleMessageException.illegalArgument("goal.not.found.try.again.later", id);
            }
        }

        return goalsById;
    }

    @Override
    public boolean update(final Goal goal) {
        return jdbcTemplate.update(UPDATE_QUERY, toBaseParams(goal)) > 0;
    }

    @Override
    public boolean delete(final Goal goal) {
        return jdbcTemplate.update(DELETE_QUERY, toBaseParams(goal)) > 0;
    }

    @Override
    public boolean clear() {
        return jdbcTemplate.update(CLEAR_QUERY) > 0;
    }

    private static Goal toGoal(final ResultSet rs, final int i) throws SQLException {
        final Importance goalImportance = Importance.valueOf(rs.getString("importance"));
        final Status goalStatus = Status.valueOf(rs.getString("status"));
        final EnumMap<OkrAncestors.OkrType, Long> goalIdByType = new EnumMap<>(OkrAncestors.OkrType.class);
        goalIdByType.put(OkrAncestors.OkrType.VALUE_STREAM, getLong(rs, "value_stream_goal_id"));
        goalIdByType.put(OkrAncestors.OkrType.UMBRELLA, getLong(rs, "umbrella_goal_id"));
        goalIdByType.put(OkrAncestors.OkrType.CONTOUR, getLong(rs, "contour_goal_id"));
        final OkrAncestors okrAncestors = new OkrAncestors(goalIdByType);

        return new Goal(rs.getLong("id"), rs.getString("name"), goalImportance, goalStatus, okrAncestors);
    }

    /**
     * For JOIN
     */
    public static Goal toGoal(final ResultSet rs) throws SQLException {
        final Long goalId = getLong(rs, "goal_id");

        if (goalId == null) {
            return null;
        }

        final Importance goalImportance = Importance.valueOf(rs.getString("goal_importance"));
        final Status goalStatus = Status.valueOf(rs.getString("goal_status"));

        final EnumMap<OkrAncestors.OkrType, Long> goalIdByType = new EnumMap<>(OkrAncestors.OkrType.class);
        goalIdByType.put(OkrAncestors.OkrType.VALUE_STREAM, getLong(rs, "goal_value_stream_goal_id"));
        goalIdByType.put(OkrAncestors.OkrType.UMBRELLA, getLong(rs, "goal_umbrella_goal_id"));
        goalIdByType.put(OkrAncestors.OkrType.CONTOUR, getLong(rs, "goal_contour_goal_id"));
        final OkrAncestors okrAncestors = new OkrAncestors(goalIdByType);

        return new Goal(goalId, rs.getString("goal_name"), goalImportance, goalStatus, okrAncestors);

    }

    private static Map<String, ?> toBaseParams(final BaseGoal goal) {
        return ImmutableMap.of(
                "id", goal.getId(),
                "name", goal.getName(),
                "status", goal.getStatus().name(),
                "importance", goal.getImportance().name()
        );
    }

    private static HashMap<String, Object> toOkrParams(final OkrAncestors okrAncestors) {
        final HashMap<String, Object> params = new HashMap<>();
        params.put("valueStreamGoalId", okrAncestors.getGoalId(OkrAncestors.OkrType.VALUE_STREAM));
        params.put("umbrellaGoalId", okrAncestors.getGoalId(OkrAncestors.OkrType.UMBRELLA));
        params.put("contourGoalId", okrAncestors.getGoalId(OkrAncestors.OkrType.CONTOUR));

        return params;
    }


    private static Map<String, ?> toParams(final Goal goal) {
        final HashMap<String, Object> params = new HashMap<>();
        params.put("id", goal.getId());
        params.put("name", goal.getName());
        params.put("status", goal.getStatus().name());
        params.put("importance", goal.getImportance().name());
        params.putAll(toOkrParams(goal.getOkrParents()));
        return params;
    }
}
