package com.salpa.subject.service.impl;

import com.salpa.subject.domain.*;
import com.salpa.subject.domain.data.*;
import com.salpa.subject.domain.diff.*;
import com.salpa.subject.domain.error.DomainErrors;
import com.salpa.subject.domain.error.MetaopsException;
import com.salpa.subject.mapper.DatabaseMetaConverter;
import com.salpa.subject.mapper.DocumentPojoConverter;
import com.salpa.subject.domain.converter.JsonConverter;
import com.salpa.subject.domain.diff.data.DiffType;
import com.salpa.subject.domain.diff.data.RootDiff;
import com.salpa.subject.mapper.*;
import com.salpa.subject.service.DocumentSyncService;
import com.salpa.subject.service.ProjectService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

@Slf4j
@Service
public class DocumentSyncServiceImpl implements DocumentSyncService {

    @Autowired
    private ProjectService projectService;

    @Autowired
    private SubjectMapper subjectMapper;

    @Autowired
    private DocumentMapper documentMapper;

    @Autowired
    private ProjectSyncRuleMapper projectSyncRuleMapper;

    @Autowired
    private DatabaseMapper databaseMapper;

    @Autowired
    private DataSourcePropertyMapper dataSourcePropertyMapper;

    @Autowired
    private TableColumnDocumentMapper tableColumnDocumentMapper;

    @Autowired
    private TableIndexDocumentMapper tableIndexDocumentMapper;

    @Autowired
    private TableForeignKeyDocumentMapper tableForeignKeyDocumentMapper;

    @Autowired
    private TableTriggerDocumentMapper tableTriggerDocumentMapper;

    @Autowired
    private JsonConverter jsonConverter;

    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private DocumentPojoConverter documentPojoConverter;

    @Autowired
    DatabaseMetaConverter databaseMetaConverter;

    @Autowired
    private WarningMapper warningMapper;

    /**
     * 同步项目
     * @param projectId
     */
    @Override
    public Integer syncByProjectId(Integer projectId, Integer taskId) {
        DatabaseMeta current = retrieveDatabaseMeta(projectId);
        Optional<MonitorDatabaseDocument> originalOption = documentMapper.selectNotArchivedByProjectId(projectId);
        Integer docId = null;
        if (originalOption.isPresent()) {
            if (!"CANCELED".equals(selectStatusById(taskId))) {
                MonitorDatabaseDocument original = originalOption.get();
                DatabaseMeta originalMeta = retrieveOriginalDatabaseMeta(original);
                RootDiff diff = Diffs.diff(originalMeta, current);
                if (diff.getDiffType() == DiffType.NONE) {
                    log.info("ignore project {} {} sync data, because without change",
                            projectId,
                            original.getDatabaseName());
                    return null;
                } else {
                    Integer previousDocumentId = original.getId();
                    // archive old version
                    documentMapper.updateIsArchiveById(previousDocumentId, true);
                    Long version = original.getDocumentVersion();
                    docId = saveNewDocument(current, version + 1, original.getProjectId(), taskId);
                    /*添加告警信息*/
                    if (version >= 1) {
                        MonitorWarning monitorWarning = new MonitorWarning();
                        monitorWarning.setWarningName(original.getDatabaseName()+"数据库告警");
                        monitorWarning.setWarningSubject(subjectMapper.getById(projectService.getById(projectId).getSubjectId()).getSubjectName() + "/" + projectService.getById(projectId).getProjectName());
                        monitorWarning.setWarningDataSource(original.getDatabaseName());
                        monitorWarning.setWarningTime(new Date());
                        Long newVersion = version + 1;
                        monitorWarning.setWarningContent("数据库发生变化,最新文档版本为【" + newVersion + "】");
                        warningMapper.addWarning(monitorWarning);
                    }
                    if (docId != null) {
                        eventPublisher.publishEvent(new DocumentUpdated(diff, version + 1, version, projectId, docId));
                    }
                }
            }
        } else {
            if (!"CANCELED".equals(selectStatusById(taskId))) {
                docId = saveNewDocument(current, 1L, projectId, taskId);
                RootDiff diff = null;
                try {
                    diff = Diffs.diff(null, current);
                } catch (Exception e) {
                    log.error("diff project " + projectId + " error, fallback diff type to NONE", e);
                }
                if (docId != null) {
                    eventPublisher.publishEvent(new DocumentUpdated(diff, 1L, null, projectId, docId));
                }
            }
        }
        return docId;
    }

    /**
     * 根据id查询任务状态
     * @param taskId
     * @return
     */
    @Override
    public String selectStatusById(Integer taskId) {
        return documentMapper.selectStatusById(taskId);
    }

    /**
     * 获取版本号
     * @return
     */
    @Override
    public Long getDocumentVersionById(Integer docId) {
        return documentMapper.getDocumentVersionById(docId);
    }

    /**
     * 删除文档
     * @param docId
     */
    @Override
    public void deleteDocumentById(Integer docId) {
        documentMapper.deleteDocumentById(docId);
    }

    /**
     * 恢复上个版本
     * @param projectId
     * @param version
     */
    @Override
    public void updateDocument(Integer projectId, Long version) {
        documentMapper.updateIsArchive(projectId,version);
    }

    /**
     * 连接数据源获取新数据
     * @param projectId
     * @return
     */
    private DatabaseMeta retrieveDatabaseMeta(Integer projectId) {
        MonitorProjectSyncRule rule = projectSyncRuleMapper.selectByProjectId(projectId);
        MonitorDataSource dataSource = databaseMapper.selectDataSourceById(projectId);
        List<Map<String,Object>> properties = dataSourcePropertyMapper.selectPropertyBySourceId(dataSource.getId());
        Connection jdbcConnection = projectService.create(dataSource, properties);
        DatabasirConfig databasirConfig = new DatabasirConfig();
        databasirConfig.setIgnoreTableNameRegex(jsonConverter.fromJson(rule.getIgnoreTableNameRegexArray().substring(1,rule.getIgnoreTableNameRegexArray().length()-1)));
        databasirConfig.setIgnoreTableColumnNameRegex(jsonConverter.fromJson(rule.getIgnoreColumnNameRegexArray().substring(1,rule.getIgnoreColumnNameRegexArray().length()-1)));
        try {
            if (jdbcConnection == null) {
                throw DomainErrors.CONNECT_DATABASE_FAILED.exception();
            }
            DatabaseMeta databaseMeta = Databasir.of(databasirConfig)
                    .get(jdbcConnection, dataSource.getDatabaseName(), dataSource.getSchemaName())
                    .orElseThrow(DomainErrors.DATABASE_META_NOT_FOUND::exception);
            return databaseMeta;
        } finally {
            try {
                if (jdbcConnection != null && !jdbcConnection.isClosed()) {
                    jdbcConnection.close();
                }
            } catch (SQLException e) {
                log.error("close jdbc connection error", e);
            }
        }
    }

    /**
     * 恢复历史数据
     * @param original
     * @return
     */
    private DatabaseMeta retrieveOriginalDatabaseMeta(MonitorDatabaseDocument original) {
        Integer docId = original.getId();
        List<MonitorTableDocument> tables = documentMapper.getTableDocument(docId,null);
        List<MonitorTableColumnDocument> columns = tableColumnDocumentMapper.getTableColumn(docId);
        List<MonitorTableIndexDocument> indexes = tableIndexDocumentMapper.getTableIndex(docId);
        List<MonitorTableTriggerDocument> triggers = tableTriggerDocumentMapper.getTableTrigger(docId);
        List<MonitorTableForeignKeyDocument> fks = tableForeignKeyDocumentMapper.getTableForeignKey(docId);
        return databaseMetaConverter.of(original, tables, columns, indexes, triggers, fks);
    }

    /**
     * 创建新文档
     * @param meta
     * @param version
     * @param projectId
     * @return
     */
    private Integer saveNewDocument(DatabaseMeta meta,
                                    Long version,
                                    Integer projectId,
                                    Integer taskId) {

        MonitorDatabaseDocument dbDocPojo = new MonitorDatabaseDocument();
        dbDocPojo.setProjectId(projectId);
        dbDocPojo.setDatabaseName(meta.getDatabaseName());
        dbDocPojo.setSchemaName(meta.getSchemaName());
        dbDocPojo.setProductName(meta.getProductName());
        dbDocPojo.setProductVersion(meta.getProductVersion());
        dbDocPojo.setDocumentVersion(version);
        Integer docId = null;
        try {
            if (!"CANCELED".equals(selectStatusById(taskId))) {
                documentMapper.addDatabaseDocument(dbDocPojo);
                docId = dbDocPojo.getId();
                dbDocPojo.setId(docId);
            }
        } catch (DuplicateKeyException e) {
            log.warn("ignore insert database document projectId={} version={}", projectId, version);
            throw new MetaopsException(DomainErrors.DATABASE_DOCUMENT_DUPLICATE_KEY);
        }
        if (docId != null) {
            Integer finalDocId = docId;
            List<TableMeta> list = meta.getTables();
            for (TableMeta table : list) {
                MonitorTableDocument tableMeta = new MonitorTableDocument();
                tableMeta.setName(table.getName());
                tableMeta.setDatabaseDocumentId(finalDocId);
                tableMeta.setType(table.getType());
                tableMeta.setComment(table.getComment());
                documentMapper.insertTableDocumentAndReturnId(tableMeta);
                Integer tableMetaId = tableMeta.getId();
                tableMeta.setId(tableMetaId);
                // column
                var columns = documentPojoConverter.toColumnPojo(finalDocId, tableMetaId, table.getColumns());
                for (MonitorTableColumnDocument column : columns) {
                    tableColumnDocumentMapper.insertTableColumn(column);
                }
                // index
                var indexes = documentPojoConverter.toIndexPojo(finalDocId, tableMetaId, table.getIndexes())
                        .stream()
                        .filter(index -> {
                            if (index.getName() != null) {
                                return true;
                            } else {
                                return false;
                            }
                        })
                        .collect(Collectors.toList());
                for (MonitorTableIndexDocument index : indexes) {
                    tableIndexDocumentMapper.insertTableIndex(index);
                }
                // foreign key
                var foreignKeys = documentPojoConverter.toForeignKeyPojo(finalDocId, tableMetaId, table.getForeignKeys());
                for (MonitorTableForeignKeyDocument foreignKey : foreignKeys) {
                    tableForeignKeyDocumentMapper.insertTableForeignKey(foreignKey);
                }

                // trigger
                var triggers = documentPojoConverter.toTriggerPojo(finalDocId, tableMetaId, table.getTriggers());
                for (MonitorTableTriggerDocument trigger : triggers) {
                    tableTriggerDocumentMapper.insertTableTrigger(trigger);
                }
            }
        }
        log.info("save new version document success: projectId = {}, name = {}, version =  {}",
                projectId, meta.getDatabaseName(), version);
        return docId;
    }
}
