package ru.micord.ervu_dashboard.dao;

import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.jooq.Condition;
import org.jooq.Field;
import org.jooq.Record;
import org.jooq.SelectConnectByStep;
import org.jooq.SelectForUpdateStep;
import org.jooq.SelectJoinStep;
import org.jooq.SelectLimitStep;
import org.jooq.SelectOnConditionStep;
import org.jooq.SelectSelectStep;
import org.jooq.Table;
import org.springframework.util.CollectionUtils;

import ru.cg.webbpm.modules.database.api.bean.TableRow;
import ru.cg.webbpm.modules.database.api.dao.option.LoadOptions;
import ru.cg.webbpm.modules.database.api.utils.QueryUtils;
import ru.cg.webbpm.modules.database.bean.entity_graph.DefaultGraphBinding;
import ru.cg.webbpm.modules.database.bean.entity_graph.DefaultGraphNode;
import ru.cg.webbpm.modules.database.bean.entity_graph.EntityColumn;
import ru.cg.webbpm.modules.database.bean.entity_graph.condition.ConditionGroup;
import ru.cg.webbpm.modules.database.bean.filter.EntityFilter;

/**
 * @author r.latypov
 */
public class LessJoinsDao extends CustomCoalesceDao {
  @Override
  public List<TableRow> load(Set<EntityColumn> columns, LoadOptions loadOptions,
      boolean withGraphConditions) {
    DefaultGraphNode mainNode = graph.getMainNode();
    EntityColumn[] mainNodePkColumns = getGraphNodeTablePkFields(mainNode);

    if (mainNodePkColumns == null || mainNodePkColumns.length == 0) {
      throw new RuntimeException("Primary key does not exist in " + mainNode.getAlias() + " : "
                                 + mainNode.getSchemaName() + "." + mainNode.getTableName());
    }

    if (loadOptions.isLoadPK()) {
      Collections.addAll(columns, mainNodePkColumns);
    }

    Set<String> fieldsEntityNames = columns.stream()
        .map(EntityColumn::getEntity)
        .collect(Collectors.toUnmodifiableSet());
    // filters without groups
    List<EntityFilter> allEntityFilters = loadOptions.getEntityFilterGroup().getEntityFilters();
    Set<String> conditionsEntityNames = allEntityFilters.stream()
        .map(entityFilter -> entityFilter.getEntityColumn().getEntity())
        .collect(Collectors.toUnmodifiableSet());
    // push entity filters into map
    Map<String, List<Condition>> entitiesConditionsMap = conditionsEntityNames.stream()
        .collect(Collectors.toMap(
            entityName -> entityName,
            entityName -> getFilterConditions(
                allEntityFilters.stream()
                    .filter(entityFilter -> entityFilter.getEntityColumn()
                        .getEntity()
                        .equals(entityName))
                    .toList()
            )
        ));

    List<Field<?>> fieldList = convertEntityColumnsToJooqFields(columns);
    SelectJoinStep<Record> selectStep = selectByJooqColumns(fieldList, fieldsEntityNames,
        entitiesConditionsMap
    );

    SelectConnectByStep<Record> joinStep = buildWhereStep(selectStep,
        entitiesConditionsMap.get(mainNode.getAlias()), withGraphConditions
    );
    SelectLimitStep<Record> limitStep = getOrderByStep(joinStep, loadOptions.getSortFields(), true);
    SelectForUpdateStep<Record> selectForUpdateStep = getSelectForUpdateStep(limitStep,
        loadOptions.getOffset(), loadOptions.getLimit()
    );

    return recordListToTableFieldDataList(selectForUpdateStep.fetch(), columns);
  }

  private SelectJoinStep<Record> selectByJooqColumns(List<Field<?>> fieldList,
      Set<String> fieldsEntityNames, Map<String, List<Condition>> entitiesConditionsMap) {
    SelectSelectStep<Record> select = uniqueResult
                                      ? getDsl().selectDistinct(fieldList)
                                      : getDsl().select(fieldList);
    DefaultGraphNode mainNode = graph.getMainNode();
    SelectJoinStep<Record> selectJoinStep = select.from(getTableWithAlias(mainNode));
    addJoins(
        selectJoinStep, graph.getMainNodeIndex(), -1, getMatrix(),
        fieldsEntityNames, entitiesConditionsMap
    );
    return selectJoinStep;
  }

  private void addJoins(SelectJoinStep<Record> selectJoinStep, int currentIndex, int parentIndex,
      DefaultGraphBinding[][] matrix, Set<String> fieldsEntityNames,
      Map<String, List<Condition>> entitiesConditionsMap) {

    for (int i = 0; i < matrix.length; i++) {

      if (matrix[currentIndex][i] == null || matrix[currentIndex][i].isCyclic()
          || (parentIndex >= 0 && i == parentIndex)) {
        continue;
      }
      DefaultGraphNode endNode = graph.getNodeByIndex(i);
      Table<?> endNodeTable = getTableWithAlias(endNode);

      String tableAlias = endNode.getAlias();
      if (!fieldsEntityNames.contains(tableAlias)
          && !entitiesConditionsMap.containsKey(tableAlias)) {
        continue;
      }

      DefaultGraphBinding binding = matrix[currentIndex][i];
      if (binding.getRefToColumns() != null && binding.getRefOnColumns() != null) {
        EntityColumn refOnColumnsEntityColumn = binding.getRefOnColumns()[0];
        refOnColumnsEntityColumn.setEntity(binding.getRefOnEntityName());
        EntityColumn refToColumnsEntityColumn = binding.getRefToColumns()[0];
        refToColumnsEntityColumn.setEntity(binding.getRefToEntityName());
        Field startNodeCol = QueryUtils.toJooqField(refOnColumnsEntityColumn);
        Field endNodeCol = QueryUtils.toJooqField(refToColumnsEntityColumn);
        @SuppressWarnings("unchecked")
        Condition bindingCondition = startNodeCol.equal(endNodeCol);

        for (int j = 1; j < binding.getRefOnColumns().length; j++) {
          startNodeCol = QueryUtils.toJooqField(binding.getRefOnColumns()[j]);
          endNodeCol = QueryUtils.toJooqField(binding.getRefToColumns()[j]);
          @SuppressWarnings("unchecked")
          Condition condition = startNodeCol.equal(endNodeCol);
          bindingCondition = bindingCondition.and(condition);
        }
        ConditionGroup conditionGroup = binding.getConditionGroup();

        if (conditionGroup != null) {
          Condition condition = conditionBuilder.build(conditionGroup);

          if (condition != null) {
            bindingCondition = bindingCondition.and(condition);
          }
        }

        List<Condition> filterConditions = entitiesConditionsMap.get(tableAlias);
        boolean hasFilters = !CollectionUtils.isEmpty(filterConditions);

        if (binding.isRequired() || hasFilters) {
          SelectOnConditionStep<?> selectOnConditionStep = selectJoinStep
              .join(endNodeTable).on(bindingCondition);
          if (hasFilters) {
            filterConditions.forEach(selectOnConditionStep::and);
          }
        }
        else {
          selectJoinStep.leftOuterJoin(endNodeTable).on(bindingCondition);
        }
      }
      addJoins(selectJoinStep, i, currentIndex, matrix, fieldsEntityNames, entitiesConditionsMap);
    }
  }
}
