package com.lanyuanxiaoyao.service.hudi.service;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.lanyuanxiaoyao.service.common.entity.TableMeta;
import com.lanyuanxiaoyao.service.configuration.ExecutorProvider;
import com.lanyuanxiaoyao.service.configuration.entity.PageResponse;
import com.lanyuanxiaoyao.service.configuration.entity.hudi.HudiCleanerPlan;
import com.lanyuanxiaoyao.service.configuration.entity.hudi.HudiCompactionPlan;
import com.lanyuanxiaoyao.service.configuration.entity.hudi.HudiInstant;
import com.lanyuanxiaoyao.service.configuration.entity.hudi.HudiRollbackPlan;
import com.lanyuanxiaoyao.service.configuration.utils.ComparatorUtil;
import com.lanyuanxiaoyao.service.forest.service.InfoService;
import com.lanyuanxiaoyao.service.hudi.utils.HoodieUtils;
import java.io.IOException;
import java.util.function.Function;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileStatus;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hudi.avro.model.HoodieCleanerPlan;
import org.apache.hudi.avro.model.HoodieCompactionPlan;
import org.apache.hudi.avro.model.HoodieRollbackPlan;
import org.apache.hudi.common.table.HoodieTableMetaClient;
import org.apache.hudi.common.table.timeline.HoodieInstant;
import org.apache.hudi.common.table.timeline.HoodieTimeline;
import org.apache.hudi.common.util.CleanerUtils;
import org.apache.hudi.common.util.CompactionUtils;
import org.apache.hudi.table.action.rollback.RollbackUtils;
import org.eclipse.collections.api.factory.Lists;
import org.eclipse.collections.api.factory.Maps;
import org.eclipse.collections.api.list.ImmutableList;
import org.eclipse.collections.api.list.MutableList;
import org.eclipse.collections.api.map.ImmutableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

/**
 * 时间线查询
 *
 * @author lanayuanxiaoyao
 * @date 2023-05-01
 */
@Service
public class TimelineService {
    private static final Logger logger = LoggerFactory.getLogger(TimelineService.class);
    private static final String INSTANT_TYPE_ACTIVE = "active";
    private static final String INSTANT_TYPE_ARCHIVE = "archive";
    private static final ImmutableMap<String, Function<HudiInstant, String>> TIMELINE_SORT_MAP = Maps.immutable.of(
            "timestamp",
            HudiInstant::getTimestamp
    );
    private final InfoService infoService;

    @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
    public TimelineService(InfoService infoService) {
        this.infoService = infoService;
    }

    @Cacheable(value = "timeline", sync = true, key = "#flinkJobId.toString()+#alias")
    @Retryable(Throwable.class)
    public PageResponse<HudiInstant> timeline(
            Integer page,
            Integer count,
            String order,
            String direction,
            Long flinkJobId,
            String alias,
            ImmutableList<String> filterType,
            ImmutableList<String> filterAction,
            ImmutableList<String> filterState
    ) throws IOException {
        TableMeta meta = infoService.tableMetaDetail(flinkJobId, alias);
        return timeline(
                page,
                count,
                order,
                direction,
                meta.getHudi().getTargetHdfsPath(),
                filterType,
                filterAction,
                filterState
        );
    }

    @Cacheable(value = "timeline", sync = true, key = "#hdfs")
    @Retryable(Throwable.class)
    public PageResponse<HudiInstant> timeline(
            Integer page,
            Integer count,
            String order,
            String direction,
            String hdfs,
            ImmutableList<String> filterType,
            ImmutableList<String> filterAction,
            ImmutableList<String> filterState
    ) throws IOException {
        Configuration configuration = new Configuration();
        FileSystem fileSystem = FileSystem.get(configuration);
        if (!fileSystem.exists(new Path(hdfs))) {
            throw new IOException(StrUtil.format("Path {} is not exists", hdfs));
        }
        HoodieTableMetaClient client = HoodieTableMetaClient.builder()
                .setConf(configuration)
                .setBasePath(hdfs)
                .build();
        MutableList<HudiInstant> instants = Lists.mutable.empty();
        if (ObjectUtil.isEmpty(filterType)) {
            filterType = Lists.immutable.of(INSTANT_TYPE_ARCHIVE, INSTANT_TYPE_ACTIVE);
        }
        if (filterType.contains(INSTANT_TYPE_ARCHIVE)) {
            HoodieUtils.getAllInstants(client, HoodieTableMetaClient::getArchivedTimeline)
                    .collect(instant -> covert(INSTANT_TYPE_ARCHIVE, instant))
                    .forEach(instants::add);
        }
        if (filterType.contains(INSTANT_TYPE_ACTIVE)) {
            HoodieUtils.getAllInstants(client, HoodieTableMetaClient::getActiveTimeline)
                    .collect(instant -> covert(INSTANT_TYPE_ACTIVE, instant))
                    .forEach(instants::add);
        }
        ImmutableList<HudiInstant> hudiInstants = instants
                .toSortedList(HudiInstant::compareTo)
                .select(instant -> ObjectUtil.isEmpty(filterAction) || filterAction.contains(instant.getAction()))
                .select(instant -> ObjectUtil.isEmpty(filterState) || filterState.contains(instant.getState()))
                .toSortedList(ComparatorUtil.stringComparator(order, direction, TIMELINE_SORT_MAP))
                .toImmutable();
        ImmutableList<HudiInstant> result = hudiInstants
                .drop(Math.max(page - 1, 0) * count)
                .take(count)
                .asParallel(ExecutorProvider.EXECUTORS, 1)
                .collect(instant -> {
                    Path instantPath = new Path(StrUtil.format("{}/{}", client.getMetaPath(), instant.getFileName()));
                    try {
                        if (fileSystem.exists(instantPath)) {
                            FileStatus status = fileSystem.getFileStatus(instantPath);
                            instant.setFileTime(status.getModificationTime());
                        }
                    } catch (IOException e) {
                        logger.error("IOError", e);
                    }
                    return instant;
                })
                .toList()
                .toImmutable();
        return new PageResponse<>(result.toList(), hudiInstants.size());
    }

    @Cacheable(value = "read-compaction-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiCompactionPlan readCompactionPlan(Long flinkJobId, String alias, String instant) throws IOException {
        TableMeta meta = infoService.tableMetaDetail(flinkJobId, alias);
        return readCompactionPlan(meta.getHudi().getTargetHdfsPath(), instant);
    }

    @Cacheable(value = "read-compaction-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiCompactionPlan readCompactionPlan(String hdfs, String instant) throws IOException {
        HoodieTableMetaClient client = HoodieTableMetaClient.builder()
                .setConf(new Configuration())
                .setBasePath(hdfs)
                .build();
        HoodieCompactionPlan plan = CompactionUtils.getCompactionPlan(client, instant);
        return new HudiCompactionPlan(
                plan.getVersion(),
                ObjectUtil.isNotNull(plan.getOperations())
                        ? Lists.immutable.ofAll(plan.getOperations())
                        .collect(o -> new HudiCompactionPlan.Operation(
                                o.getBaseInstantTime(),
                                Lists.immutable.ofAll(o.getDeltaFilePaths()),
                                o.getDataFilePath(),
                                o.getFileId(),
                                o.getPartitionPath(),
                                Maps.immutable.ofAll(o.getMetrics()),
                                o.getBootstrapFilePath()
                        ))
                        : Lists.immutable.empty(),
                ObjectUtil.isNotNull(plan.getExtraMetadata()) ? Maps.immutable.ofAll(plan.getExtraMetadata()) : Maps.immutable.empty()
        );
    }

    @Cacheable(value = "read-rollback-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiRollbackPlan readRollbackPlan(Long flinkJobId, String alias, String instant) throws IOException {
        TableMeta meta = infoService.tableMetaDetail(flinkJobId, alias);
        return readRollbackPlan(meta.getHudi().getTargetHdfsPath(), instant);
    }

    @Cacheable(value = "read-rollback-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiRollbackPlan readRollbackPlan(String hdfs, String instant) throws IOException {
        HoodieTableMetaClient client = HoodieTableMetaClient.builder()
                .setConf(new Configuration())
                .setBasePath(hdfs)
                .build();
        HoodieRollbackPlan plan = RollbackUtils.getRollbackPlan(client, new HoodieInstant(HoodieInstant.State.INFLIGHT, HoodieTimeline.ROLLBACK_ACTION, instant));
        return new HudiRollbackPlan(
                plan.getVersion(),
                ObjectUtil.isNotNull(plan.getInstantToRollback())
                        ? new HudiRollbackPlan.Info(plan.getInstantToRollback().getAction(), plan.getInstantToRollback().getCommitTime())
                        : null,
                ObjectUtil.isNotNull(plan.getRollbackRequests())
                        ? Lists.immutable.ofAll(plan.getRollbackRequests())
                        .collect(r -> new HudiRollbackPlan.Request(
                                r.getFileId(),
                                r.getPartitionPath(),
                                r.getLatestBaseInstant(),
                                Lists.immutable.ofAll(r.getFilesToBeDeleted()),
                                Maps.immutable.ofAll(r.getLogBlocksToBeDeleted())
                        ))
                        : Lists.immutable.empty()
        );
    }

    @Cacheable(value = "read-cleaner-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiCleanerPlan readCleanerPlan(Long flinkJobId, String alias, String instant) throws IOException {
        TableMeta meta = infoService.tableMetaDetail(flinkJobId, alias);
        return readCleanerPlan(meta.getHudi().getTargetHdfsPath(), instant);
    }

    @Cacheable(value = "read-cleaner-plan", sync = true)
    @Retryable(Throwable.class)
    public HudiCleanerPlan readCleanerPlan(String hdfs, String instant) throws IOException {
        HoodieTableMetaClient client = HoodieTableMetaClient.builder()
                .setConf(new Configuration())
                .setBasePath(hdfs)
                .build();
        HoodieCleanerPlan plan = CleanerUtils.getCleanerPlan(client, HoodieTimeline.getCleanInflightInstant(instant));
        return new HudiCleanerPlan(
                plan.getVersion(),
                plan.getPolicy(),
                ObjectUtil.isNotNull(plan.getEarliestInstantToRetain())
                        ? new HudiCleanerPlan.Instant(plan.getEarliestInstantToRetain().getAction(), plan.getEarliestInstantToRetain().getState(), plan.getEarliestInstantToRetain().getTimestamp())
                        : null,
                Maps.immutable.ofAll(plan.getFilePathsToBeDeletedPerPartition())
                        .collectValues((key, value) -> Lists.immutable.ofAll(value)
                                .collect(i -> new HudiCleanerPlan.Info(
                                        i.getFilePath(),
                                        i.getIsBootstrapBaseFile()
                                ))),
                Maps.immutable.ofAll(plan.getFilesToBeDeletedPerPartition())
                        .collectValues((key, value) -> Lists.immutable.ofAll(value)),
                Lists.immutable.ofAll(plan.getPartitionsToBeDeleted())
        );
    }

    @Cacheable(value = "pending_compaction_timeline", sync = true, key = "#flinkJobId.toString()+#alias")
    @Retryable(Throwable.class)
    public ImmutableList<HudiInstant> pendingCompactionTimeline(Long flinkJobId, String alias) throws IOException {
        TableMeta meta = infoService.tableMetaDetail(flinkJobId, alias);
        HoodieTableMetaClient client = HoodieTableMetaClient.builder()
                .setConf(new Configuration())
                .setBasePath(meta.getHudi().getTargetHdfsPath())
                .build();
        return HoodieUtils.getAllInstants(client, HoodieTableMetaClient::getActiveTimeline)
                .select(instant -> StrUtil.equals(instant.getAction(), HoodieTimeline.COMPACTION_ACTION))
                .reject(instant -> ObjectUtil.equals(instant.getState(), HoodieInstant.State.COMPLETED))
                .collect(instant -> covert(INSTANT_TYPE_ACTIVE, instant));
    }

    private HudiInstant covert(String type, HoodieInstant instant) {
        return new HudiInstant(
                instant.getAction(),
                instant.getState().name(),
                instant.getTimestamp(),
                instant.getFileName(),
                type
        );
    }
}
