package com.njcn.gather.systemops.database.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.njcn.common.pojo.enums.response.CommonResponseEnum;
import com.njcn.common.pojo.exception.BusinessException;
import com.njcn.gather.systemops.database.component.DataPumpCommandExecutor;
import com.njcn.gather.systemops.database.component.JdbcExportComponent;
import com.njcn.gather.systemops.database.config.DbmsProperties;
import com.njcn.gather.systemops.database.constant.DatabaseOpsConst;
import com.njcn.gather.systemops.database.mapper.DatabaseOperationTaskMapper;
import com.njcn.gather.systemops.database.pojo.enums.BackupModeEnum;
import com.njcn.gather.systemops.database.pojo.enums.BackupStrategyEnum;
import com.njcn.gather.systemops.database.pojo.enums.FileFormatEnum;
import com.njcn.gather.systemops.database.pojo.enums.OperationTypeEnum;
import com.njcn.gather.systemops.database.pojo.enums.TaskStatusEnum;
import com.njcn.gather.systemops.database.pojo.param.DatabaseBackupParam;
import com.njcn.gather.systemops.database.pojo.po.DatabaseBackupFile;
import com.njcn.gather.systemops.database.pojo.po.DatabaseConnection;
import com.njcn.gather.systemops.database.pojo.po.DatabaseOperationTask;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskCreateVO;
import com.njcn.gather.systemops.database.pojo.vo.DatabaseTaskVO;
import com.njcn.gather.systemops.database.service.DatabaseBackupFileService;
import com.njcn.gather.systemops.database.service.DatabaseConnectionService;
import com.njcn.gather.systemops.database.service.DatabaseOperationTaskService;
import com.njcn.gather.systemops.database.util.DatabaseChecksumUtil;
import com.njcn.gather.systemops.database.util.DatabaseFileNameUtil;
import com.njcn.gather.systemops.database.util.DatabaseOpsIdUtil;
import com.njcn.gather.systemops.database.util.DatabasePathUtil;
import com.njcn.web.factory.PageFactory;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;

/**
 * 数据库运维任务服务实现。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class DatabaseOperationTaskServiceImpl extends ServiceImpl<DatabaseOperationTaskMapper, DatabaseOperationTask> implements DatabaseOperationTaskService {

    private final DatabaseConnectionService databaseConnectionService;
    private final DatabaseBackupFileService databaseBackupFileService;
    private final DataPumpCommandExecutor dataPumpCommandExecutor;
    private final JdbcExportComponent jdbcExportComponent;
    private final DbmsProperties dbmsProperties;
    private final ObjectMapper objectMapper;
    @Resource(name = "dbmsTaskExecutorService")
    private ExecutorService dbmsTaskExecutorService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public DatabaseTaskCreateVO createBackupTask(DatabaseBackupParam.CreateParam param) {
        DatabaseConnection connection = databaseConnectionService.requireEnabled(param.getConnectionId());
        validateBackupParam(param, connection);
        if (existsRunningTask(connection.getId())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "当前连接存在运行中的任务");
        }
        DatabaseOperationTask task = buildBackupTask(param, connection);
        this.save(task);
        dbmsTaskExecutorService.submit(() -> executeBackupTask(task.getId(), param));
        return toCreateVO(task);
    }

    @Override
    public Page<DatabaseTaskVO> listBackupTasks(DatabaseBackupParam.TaskQueryParam param) {
        DatabaseBackupParam.TaskQueryParam query = param == null ? new DatabaseBackupParam.TaskQueryParam() : param;
        LambdaQueryWrapper<DatabaseOperationTask> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED)
                .eq(DatabaseOperationTask::getOperationType, OperationTypeEnum.BACKUP.name())
                .eq(StrUtil.isNotBlank(query.getConnectionId()), DatabaseOperationTask::getConnectionId, query.getConnectionId())
                .eq(StrUtil.isNotBlank(query.getTaskStatus()), DatabaseOperationTask::getTaskStatus, query.getTaskStatus())
                .orderByDesc(DatabaseOperationTask::getCreateTime);
        Page<DatabaseOperationTask> page = this.page(new Page<>(PageFactory.getPageNum(query), PageFactory.getPageSize(query)), wrapper);
        Page<DatabaseTaskVO> result = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        result.setRecords(page.getRecords().stream().map(this::toVO).collect(Collectors.toList()));
        return result;
    }

    @Override
    public DatabaseTaskVO getStatus(String taskId) {
        DatabaseOperationTask task = this.getById(taskId);
        if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
        }
        return toVO(task);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean deleteTask(String taskId, String confirmText) {
        if (!DatabaseOpsConst.CONFIRM_DELETE.equals(confirmText)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "确认文案不正确");
        }
        DatabaseOperationTask task = this.getById(taskId);
        if (task == null || !Integer.valueOf(DatabaseOpsConst.STATE_ENABLED).equals(task.getState())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "任务不存在或已删除");
        }
        if (TaskStatusEnum.RUNNING.name().equals(task.getTaskStatus()) || TaskStatusEnum.WAITING.name().equals(task.getTaskStatus())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "运行中的任务不能删除");
        }
        task.setState(DatabaseOpsConst.STATE_DELETED);
        task.setUpdateTime(LocalDateTime.now());
        return this.updateById(task);
    }

    @Override
    public boolean existsRunningTask(String connectionId) {
        return this.lambdaQuery()
                .eq(DatabaseOperationTask::getConnectionId, connectionId)
                .eq(DatabaseOperationTask::getState, DatabaseOpsConst.STATE_ENABLED)
                .in(DatabaseOperationTask::getTaskStatus, Arrays.asList(TaskStatusEnum.WAITING.name(), TaskStatusEnum.RUNNING.name()))
                .count() > 0;
    }

    private void executeBackupTask(String taskId, DatabaseBackupParam.CreateParam param) {
        DatabaseOperationTask task = this.getById(taskId);
        try {
            markRunning(task);
            DatabaseConnection connection = databaseConnectionService.requireEnabled(task.getConnectionId());
            connection.setSchemaName(task.getSchemaName());
            String password = databaseConnectionService.resolvePassword(connection, param.getTemporaryPassword());
            DatabaseBackupFile backupFile;
            if (BackupStrategyEnum.DATA_PUMP.name().equals(task.getBackupStrategy())) {
                backupFile = executeDataPumpBackup(task, connection, password, param);
            } else {
                backupFile = executeJdbcExportBackup(task, connection, password, param);
            }
            databaseBackupFileService.save(backupFile);
            markSuccess(task, "备份任务执行成功");
        } catch (Exception exception) {
            log.error("数据库备份任务失败，taskId={}", taskId, exception);
            markFail(task, exception.getMessage());
        }
    }

    private DatabaseBackupFile executeDataPumpBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
                                                     DatabaseBackupParam.CreateParam param) {
        String directoryName = StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName());
        if (StrUtil.isBlank(directoryName)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "DATA_PUMP 备份需要 Oracle Directory 名称");
        }
        String baseName = buildBaseFileName(connection, task);
        String dumpFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".dmp", task.getTaskNo());
        String logFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".log", task.getTaskNo());
        DataPumpCommandExecutor.CommandResult commandResult = dataPumpCommandExecutor.expdp(connection, password, directoryName, dumpFileName, logFileName, param.getTargetNames());
        if (!Boolean.TRUE.equals(commandResult.getSuccess())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 执行失败：" + commandResult.getOutput());
        }
        if (StrUtil.isBlank(connection.getDirectoryPath())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "Data Pump 备份需要配置可管理的 directoryPath");
        }
        Path dumpPath = buildManagedPath(connection.getDirectoryPath(), dumpFileName);
        Path logPath = buildManagedPath(connection.getDirectoryPath(), logFileName);
        return buildBackupFile(task, connection, param, FileFormatEnum.DMP.name(), dumpFileName, dumpPath, logFileName, logPath, null);
    }

    private DatabaseBackupFile executeJdbcExportBackup(DatabaseOperationTask task, DatabaseConnection connection, String password,
                                                       DatabaseBackupParam.CreateParam param) throws Exception {
        String baseName = buildBaseFileName(connection, task);
        String fileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + ".csv", task.getTaskNo());
        String metadataFileName = DatabaseFileNameUtil.appendTodayWithTask(baseName + "_metadata.json", task.getTaskNo());
        Path dataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), fileName);
        Path metadataFilePath = buildManagedPath(dbmsProperties.getBackup().getStoragePath(), metadataFileName);
        jdbcExportComponent.exportCsv(connection, password, param, dataFilePath, metadataFilePath);
        return buildBackupFile(task, connection, param, FileFormatEnum.CSV.name(), fileName, dataFilePath, null, null, metadataFilePath);
    }

    private DatabaseBackupFile buildBackupFile(DatabaseOperationTask task, DatabaseConnection connection, DatabaseBackupParam.CreateParam param,
                                               String fileFormat, String fileName, Path filePath, String logFileName, Path logFilePath,
                                               Path metadataFilePath) {
        if (filePath == null || !Files.exists(filePath)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份文件未生成");
        }
        DatabaseBackupFile file = new DatabaseBackupFile();
        file.setId(DatabaseOpsIdUtil.uuid());
        file.setTaskId(task.getId());
        file.setConnectionId(connection.getId());
        file.setDbType(connection.getDbType());
        file.setBackupStrategy(task.getBackupStrategy());
        file.setFileFormat(fileFormat);
        file.setSchemaName(task.getSchemaName());
        file.setTargetNamesJson(task.getTargetNamesJson());
        file.setBackupMode(StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT));
        file.setBackupStartTime(param.getStartTime());
        file.setBackupEndTime(param.getEndTime());
        file.setTimeColumn(param.getTimeColumn());
        file.setDirectoryName(StrUtil.blankToDefault(param.getDirectoryName(), connection.getDirectoryName()));
        file.setDumpFileName(FileFormatEnum.DMP.name().equals(fileFormat) ? fileName : null);
        file.setLogFileName(logFileName);
        file.setFileName(fileName);
        file.setFilePath(filePath.toString());
        file.setLogFilePath(logFilePath == null ? null : logFilePath.toString());
        file.setMetadataFilePath(metadataFilePath == null ? null : metadataFilePath.toString());
        file.setFileSize(readFileSize(filePath));
        file.setChecksum(DatabaseChecksumUtil.sha256(filePath));
        file.setState(DatabaseOpsConst.STATE_ENABLED);
        file.setCreateTime(LocalDateTime.now());
        file.setUpdateTime(LocalDateTime.now());
        return file;
    }

    private Long readFileSize(Path filePath) {
        try {
            if (filePath != null && Files.exists(filePath) && !Files.isDirectory(filePath)) {
                return Files.size(filePath);
            }
        } catch (Exception ignored) {
            return null;
        }
        return null;
    }

    private Path buildManagedPath(String rootPath, String fileName) {
        Path root = DatabasePathUtil.normalize(rootPath);
        if (root == null) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份目录未配置");
        }
        return root.resolve(fileName).normalize();
    }

    private String buildBaseFileName(DatabaseConnection connection, DatabaseOperationTask task) {
        return connection.getSchemaName() + "_" + task.getBackupStrategy().toLowerCase(Locale.ROOT);
    }

    private DatabaseOperationTask buildBackupTask(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
        DatabaseOperationTask task = new DatabaseOperationTask();
        task.setId(DatabaseOpsIdUtil.uuid());
        task.setTaskNo(DatabaseOpsIdUtil.taskNo("DBMSB"));
        task.setConnectionId(connection.getId());
        task.setDbType(connection.getDbType());
        task.setOperationType(OperationTypeEnum.BACKUP.name());
        task.setBackupStrategy(resolveBackupStrategy(param.getBackupStrategy()));
        task.setTaskStatus(TaskStatusEnum.WAITING.name());
        task.setSchemaName(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()));
        task.setTargetNamesJson(writeJson(param.getTargetNames()));
        task.setRequestParamJson(writeJsonWithoutPassword(param));
        task.setProgressPercent(BigDecimal.ZERO);
        task.setState(DatabaseOpsConst.STATE_ENABLED);
        task.setCreateTime(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        return task;
    }

    private void validateBackupParam(DatabaseBackupParam.CreateParam param, DatabaseConnection connection) {
        if (!DatabaseOpsConst.DB_TYPE_ORACLE.equals(connection.getDbType())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "一期仅支持 ORACLE");
        }
        if (param.getTargetNames() == null || param.getTargetNames().isEmpty()) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份表不能为空");
        }
        if (StrUtil.isBlank(StrUtil.blankToDefault(param.getSchemaName(), connection.getSchemaName()))) {
            throw new BusinessException(CommonResponseEnum.FAIL, "备份 Schema 不能为空");
        }
        String backupMode = StrUtil.blankToDefault(param.getBackupMode(), BackupModeEnum.FULL_TABLE.name()).toUpperCase(Locale.ROOT);
        if (BackupModeEnum.TIME_RANGE.name().equals(backupMode)
                && (param.getStartTime() == null || param.getEndTime() == null)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "按时间备份必须传入开始时间和结束时间");
        }
        if (BackupModeEnum.TIME_RANGE.name().equals(backupMode)
                && param.getStartTime() != null && param.getEndTime() != null
                && param.getStartTime().isAfter(param.getEndTime())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "开始时间不能晚于结束时间");
        }
        if (BackupModeEnum.SIZE_SPLIT.name().equals(backupMode)
                && (param.getMaxFileSizeMb() == null || param.getMaxFileSizeMb() <= 0)) {
            throw new BusinessException(CommonResponseEnum.FAIL, "按大小分片必须传入大于 0 的文件大小");
        }
        if (BackupStrategyEnum.JDBC_EXPORT.name().equals(resolveBackupStrategy(param.getBackupStrategy()))
                && BackupModeEnum.TIME_RANGE.name().equals(backupMode)
                && StrUtil.isBlank(param.getTimeColumn())) {
            throw new BusinessException(CommonResponseEnum.FAIL, "JDBC 按时间备份必须传入时间字段");
        }
    }

    private String resolveBackupStrategy(String backupStrategy) {
        String value = StrUtil.blankToDefault(backupStrategy, BackupStrategyEnum.DATA_PUMP.name()).trim().toUpperCase(Locale.ROOT);
        try {
            return BackupStrategyEnum.valueOf(value).name();
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.FAIL, "不支持的备份策略：" + backupStrategy);
        }
    }

    private void markRunning(DatabaseOperationTask task) {
        task.setTaskStatus(TaskStatusEnum.RUNNING.name());
        task.setStartedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private void markSuccess(DatabaseOperationTask task, String message) {
        task.setTaskStatus(TaskStatusEnum.SUCCESS.name());
        task.setResultMessage(message);
        task.setProgressPercent(new BigDecimal("100.00"));
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private void markFail(DatabaseOperationTask task, String message) {
        task.setTaskStatus(TaskStatusEnum.FAIL.name());
        task.setResultMessage(message);
        task.setFinishedAt(LocalDateTime.now());
        task.setUpdateTime(LocalDateTime.now());
        this.updateById(task);
    }

    private String writeJson(Object value) {
        try {
            return objectMapper.writeValueAsString(value);
        } catch (Exception exception) {
            throw new BusinessException(CommonResponseEnum.JSON_CONVERT_EXCEPTION, exception.getMessage());
        }
    }

    private String writeJsonWithoutPassword(DatabaseBackupParam.CreateParam param) {
        DatabaseBackupParam.CreateParam copy = new DatabaseBackupParam.CreateParam();
        BeanUtil.copyProperties(param, copy);
        copy.setTemporaryPassword(null);
        return writeJson(copy);
    }

    private DatabaseTaskCreateVO toCreateVO(DatabaseOperationTask task) {
        DatabaseTaskCreateVO vo = new DatabaseTaskCreateVO();
        vo.setTaskId(task.getId());
        vo.setTaskNo(task.getTaskNo());
        vo.setTaskStatus(task.getTaskStatus());
        return vo;
    }

    private DatabaseTaskVO toVO(DatabaseOperationTask task) {
        DatabaseTaskVO vo = new DatabaseTaskVO();
        BeanUtil.copyProperties(task, vo);
        return vo;
    }
}
