package cn.iocoder.yudao.module.infra.service.codegen;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenCreateListReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.CodegenUpdateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTablePageReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.DatabaseTableRespVO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
import cn.iocoder.yudao.module.infra.service.db.DatabaseTableService;
import cn.iocoder.yudao.module.infra.util.DistributedLock;
import cn.iocoder.yudao.module.infra.util.InputStreamConverter;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.alibaba.excel.util.DateUtils;
import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import javax.sql.DataSource;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.zip.ZipInputStream;

import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment;

/**
 * 代码生成 Service 实现类
 *
 * @author 芋道源码
 */
@Slf4j
@Service
public class CodegenServiceImpl implements CodegenService {

    @Resource
    private DatabaseTableService databaseTableService;

    @Resource
    private CodegenTableMapper codegenTableMapper;
    @Resource
    private CodegenColumnMapper codegenColumnMapper;

    @Resource
    private AdminUserApi userApi;

    @Resource
    private CodegenBuilder codegenBuilder;
    @Resource
    private CodegenEngine codegenEngine;

    @Resource
    private CodegenProperties codegenProperties;

    @Autowired
    private DataSource dataSource;

    @Value("${runsystem.project.basedir.unzip}")
    private String unzipPath;


    @Value("${runsystem.project.basedir.menusql}")
    private String menuSqlPath;

    @Value("${runsystem.project.basedir.shell}")
    private String shellPath;

    @Value("${runsystem.project.basedir.config}")
    private String configPath;

    @Value("${runsystem.project.basedir.nginx}")
    private String nginxPath;

    @Autowired
    private DistributedLock distributedLock;

    @Value("${runsystem.project.host}")
    private String host;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public List<Long> createCodegenList(Long userId, CodegenCreateListReqVO reqVO) {
        List<Long> ids = new ArrayList<>(reqVO.getTableNames().size());
        // 遍历添加。虽然效率会低一点，但是没必要做成完全批量，因为不会这么大量
        reqVO.getTableNames().forEach(tableName -> ids.add(createCodegen(userId, reqVO.getDataSourceConfigId(), tableName)));
        return ids;
    }

    private Long createCodegen(Long userId, Long dataSourceConfigId, String tableName) {
        // 从数据库中，获得数据库表结构
        TableInfo tableInfo = databaseTableService.getTable(dataSourceConfigId, tableName);
        // 导入
        return createCodegen0(userId, dataSourceConfigId, tableInfo);
    }

    private Long createCodegen0(Long userId, Long dataSourceConfigId, TableInfo tableInfo) {
        // 校验导入的表和字段非空
        validateTableInfo(tableInfo);
        // 校验是否已经存在
        if (codegenTableMapper.selectByTableNameAndDataSourceConfigId(tableInfo.getName(),
                dataSourceConfigId) != null) {
            throw exception(CODEGEN_TABLE_EXISTS);
        }

        // 构建 CodegenTableDO 对象，插入到 DB 中
        CodegenTableDO table = codegenBuilder.buildTable(tableInfo);
        table.setDataSourceConfigId(dataSourceConfigId);
        table.setScene(CodegenSceneEnum.ADMIN.getScene()); // 默认配置下，使用管理后台的模板
        table.setFrontType(codegenProperties.getFrontType());
        table.setAuthor(userApi.getUser(userId).getCheckedData().getNickname());
        codegenTableMapper.insert(table);

        // 构建 CodegenColumnDO 数组，插入到 DB 中
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(table.getId(), tableInfo.getFields());
        // 如果没有主键，则使用第一个字段作为主键
        if (!tableInfo.isHavePrimaryKey()) {
            columns.get(0).setPrimaryKey(true);
        }
        codegenColumnMapper.insertBatch(columns);
        return table.getId();
    }

    @VisibleForTesting
    void validateTableInfo(TableInfo tableInfo) {
        if (tableInfo == null) {
            throw exception(CODEGEN_IMPORT_TABLE_NULL);
        }
        if (StrUtil.isEmpty(tableInfo.getComment())) {
            throw exception(CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL);
        }
        if (CollUtil.isEmpty(tableInfo.getFields())) {
            throw exception(CODEGEN_IMPORT_COLUMNS_NULL);
        }
        tableInfo.getFields().forEach(field -> {
            if (StrUtil.isEmpty(field.getComment())) {
                throw exception(CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL, field.getName());
            }
        });
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateCodegen(CodegenUpdateReqVO updateReqVO) {
        // 校验是否已经存在
        if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) {
            throw exception(CODEGEN_TABLE_NOT_EXISTS);
        }
        // 校验主表字段存在
        if (Objects.equals(updateReqVO.getTable().getTemplateType(), CodegenTemplateTypeEnum.SUB.getType())) {
            if (codegenTableMapper.selectById(updateReqVO.getTable().getMasterTableId()) == null) {
                throw exception(CODEGEN_MASTER_TABLE_NOT_EXISTS, updateReqVO.getTable().getMasterTableId());
            }
            if (CollUtil.findOne(updateReqVO.getColumns(),  // 关联主表的字段不存在
                    column -> column.getId().equals(updateReqVO.getTable().getSubJoinColumnId())) == null) {
                throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, updateReqVO.getTable().getSubJoinColumnId());
            }
        }

        // 更新 table 表定义
        CodegenTableDO updateTableObj = BeanUtils.toBean(updateReqVO.getTable(), CodegenTableDO.class);
        codegenTableMapper.updateById(updateTableObj);
        // 更新 column 字段定义
        List<CodegenColumnDO> updateColumnObjs = BeanUtils.toBean(updateReqVO.getColumns(), CodegenColumnDO.class);
        updateColumnObjs.forEach(updateColumnObj -> codegenColumnMapper.updateById(updateColumnObj));
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void syncCodegenFromDB(Long tableId) {
        // 校验是否已经存在
        CodegenTableDO table = codegenTableMapper.selectById(tableId);
        if (table == null) {
            throw exception(CODEGEN_TABLE_NOT_EXISTS);
        }
        // 从数据库中，获得数据库表结构
        TableInfo tableInfo = databaseTableService.getTable(table.getDataSourceConfigId(), table.getTableName());
        // 执行同步
        syncCodegen0(tableId, tableInfo);
    }

    private void syncCodegen0(Long tableId, TableInfo tableInfo) {
        // 1. 校验导入的表和字段非空
        validateTableInfo(tableInfo);
        List<TableField> tableFields = tableInfo.getFields();

        // 2. 构建 CodegenColumnDO 数组，只同步新增的字段
        List<CodegenColumnDO> codegenColumns = codegenColumnMapper.selectListByTableId(tableId);
        Set<String> codegenColumnNames = convertSet(codegenColumns, CodegenColumnDO::getColumnName);

        // 3.1 计算需要【修改】的字段，插入时重新插入，删除时将原来的删除
        Map<String, CodegenColumnDO> codegenColumnDOMap = convertMap(codegenColumns, CodegenColumnDO::getColumnName);
        BiPredicate<TableField, CodegenColumnDO> primaryKeyPredicate =
                (tableField, codegenColumn) -> tableField.getMetaInfo().getJdbcType().name().equals(codegenColumn.getDataType())
                        && tableField.getMetaInfo().isNullable() == codegenColumn.getNullable()
                        && tableField.isKeyFlag() == codegenColumn.getPrimaryKey()
                        && tableField.getComment().equals(codegenColumn.getColumnComment());
        Set<String> modifyFieldNames = IntStream.range(0, tableFields.size()).mapToObj(index -> {
            TableField tableField = tableFields.get(index);
            String columnName = tableField.getColumnName();
            CodegenColumnDO codegenColumn = codegenColumnDOMap.get(columnName);
            if (codegenColumn == null) {
                return null;
            }
            if (!primaryKeyPredicate.test(tableField, codegenColumn) || codegenColumn.getOrdinalPosition() != index) {
                return columnName;
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toSet());
        // 3.2 计算需要【删除】的字段
        Set<String> tableFieldNames = convertSet(tableFields, TableField::getName);
        Set<Long> deleteColumnIds = codegenColumns.stream()
                .filter(column -> (!tableFieldNames.contains(column.getColumnName())) || modifyFieldNames.contains(column.getColumnName()))
                .map(CodegenColumnDO::getId).collect(Collectors.toSet());
        // 移除已经存在的字段
        tableFields.removeIf(column -> codegenColumnNames.contains(column.getColumnName()) && (!modifyFieldNames.contains(column.getColumnName())));
        if (CollUtil.isEmpty(tableFields) && CollUtil.isEmpty(deleteColumnIds)) {
            throw exception(CODEGEN_SYNC_NONE_CHANGE);
        }

        // 4.1 插入新增的字段
        List<CodegenColumnDO> columns = codegenBuilder.buildColumns(tableId, tableFields);
        codegenColumnMapper.insertBatch(columns);
        // 4.2 删除不存在的字段
        if (CollUtil.isNotEmpty(deleteColumnIds)) {
            codegenColumnMapper.deleteBatchIds(deleteColumnIds);
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void deleteCodegen(Long tableId) {
        // 校验是否已经存在
        if (codegenTableMapper.selectById(tableId) == null) {
            throw exception(CODEGEN_TABLE_NOT_EXISTS);
        }

        // 删除 table 表定义
        codegenTableMapper.deleteById(tableId);
        // 删除 column 字段定义
        codegenColumnMapper.deleteListByTableId(tableId);
    }

    @Override
    public List<CodegenTableDO> getCodegenTableList(Long dataSourceConfigId) {
        return codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId);
    }

    @Override
    public PageResult<CodegenTableDO> getCodegenTablePage(CodegenTablePageReqVO pageReqVO) {
        return codegenTableMapper.selectPage(pageReqVO);
    }

    @Override
    public CodegenTableDO getCodegenTable(Long id) {
        return codegenTableMapper.selectById(id);
    }

    @Override
    public List<CodegenColumnDO> getCodegenColumnListByTableId(Long tableId) {
        return codegenColumnMapper.selectListByTableId(tableId);
    }

    @Override
    public Map<String, String> generationCodes(Long tableId) {
        // 校验是否已经存在
        CodegenTableDO table = codegenTableMapper.selectById(tableId);
        if (table == null) {
            throw exception(CODEGEN_TABLE_NOT_EXISTS);
        }
        List<CodegenColumnDO> columns = codegenColumnMapper.selectListByTableId(tableId);
        if (CollUtil.isEmpty(columns)) {
            throw exception(CODEGEN_COLUMN_NOT_EXISTS);
        }

        // 如果是主子表，则加载对应的子表信息
        List<CodegenTableDO> subTables = null;
        List<List<CodegenColumnDO>> subColumnsList = null;
        if (CodegenTemplateTypeEnum.isMaster(table.getTemplateType())) {
            // 校验子表存在
            subTables = codegenTableMapper.selectListByTemplateTypeAndMasterTableId(
                    CodegenTemplateTypeEnum.SUB.getType(), tableId);
            if (CollUtil.isEmpty(subTables)) {
                throw exception(CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE);
            }
            // 校验子表的关联字段存在
            subColumnsList = new ArrayList<>();
            for (CodegenTableDO subTable : subTables) {
                List<CodegenColumnDO> subColumns = codegenColumnMapper.selectListByTableId(subTable.getId());
                if (CollUtil.findOne(subColumns, column -> column.getId().equals(subTable.getSubJoinColumnId())) == null) {
                    throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId());
                }
                subColumnsList.add(subColumns);
            }
        }

        // 执行生成
        return codegenEngine.execute(table, columns, subTables, subColumnsList);
    }

    @Override
    public List<DatabaseTableRespVO> getDatabaseTableList(Long dataSourceConfigId, String name, String comment) {
        List<TableInfo> tables = databaseTableService.getTableList(dataSourceConfigId, name, comment);
        // 移除在 Codegen 中，已经存在的
        Set<String> existsTables = convertSet(
                codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName);
        tables.removeIf(table -> existsTables.contains(table.getName()));
        return BeanUtils.toBean(tables, DatabaseTableRespVO.class);
    }

    /**
     * 应用程序前后端代码生成，并解压到配置的路径
     */
    @Override
    public String genAppCode(String tableIdStr, String appName) {
        appName = appName.toLowerCase().replaceAll("[^a-zA-Z0-9]", "");
        log.info("应用程序前后端代码生成开始，tableIdStr:{},appName:{}", tableIdStr, appName);

        String lockKey = "genAppCode:" + ":" + appName;
        if (!distributedLock.lock(lockKey, 10, TimeUnit.MINUTES)) {
            log.error("Failed to acquire lock for appName: {}", appName);
            throw new ServiceException(500,appName + " Failed to acquire lock");
        }
        Long[] tableIds = Convert.toLongArray(tableIdStr);
        try {

            String appDir = "/" + appName + "-" + DateUtils.DATE_FORMAT_10;
            String savePath = unzipPath + appDir;
            String url = "";

            Map<String,String> codeResult=new HashMap<String, String>();
            // 生成代码
            for (Long tableId : tableIds) {
                Map<String, String> codes = this.generationCodes(tableId);
                codeResult.putAll(codes);
            }

            // 构建 zip 包
            String[] paths = codeResult.keySet().toArray(new String[0]);
            ByteArrayInputStream[] ins = codeResult.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);

            // 解压ZIP文件内容到目标文件夹
            File targetFolder = new File(savePath);
            if (!targetFolder.exists()) {
                targetFolder.mkdirs();
            } else {
                FileUtils.deleteDirectory(targetFolder);
            }

            InputStream bais = InputStreamConverter.convertByteArrayInputStreamArrayToInputStream(ins);
            ZipInputStream zis = new ZipInputStream(bais);
            ZipUtil.unzip(zis, targetFolder);

            //运行系统-执行菜单文件夹中的SQL脚本
            Connection connection = dataSource.getConnection();
            String menuScriptsDir = savePath + menuSqlPath;
            if (null != connection) {
                executeMenuScripts(menuScriptsDir, savePath);
            }

            //运行系统
            executeRunSystemShell(appName, savePath);

            //构建nginx配置文件
            url = buildNginxConf(appName, savePath);

            // 返回成功信息
            log.info("代码生成成功，已保存到: " + savePath, "运行系统url地址：" + url);
            return StringUtils.isNotBlank(url) ? url : "应用容器启动失败";
        } catch (Exception e) {
            log.error("Failed to generate app code: {}", e.getMessage());
            return "应用容器启动失败:" + e.getMessage();
        } finally {
            distributedLock.unlock(lockKey);
            log.info("Lock released for tableIdStr: {}, appName: {}", tableIdStr, appName);
            //删除gen_table和gen_table_column中table_id相关的数据
            codegenTableMapper.deleteByIds(Arrays.asList(tableIds));
        }
    }

    /**
     * 执行菜单脚本并合并为一个文件
     */
    private static void executeMenuScripts(String menuScriptsDir, String targetDir) throws IOException {
        File dir = new File(menuScriptsDir);
        if (!dir.exists() || !dir.isDirectory()) {
            throw new IllegalArgumentException("指定的目录不存在或不是一个文件夹: " + menuScriptsDir);
        }

        File[] sqlFiles = dir.listFiles((dir1, name) -> name.toLowerCase().endsWith(".sql"));
        if (sqlFiles == null || sqlFiles.length == 0) {
            System.out.println("没有找到 SQL 脚本文件");
            return;
        }

        Arrays.sort(sqlFiles); // 按文件名排序

        StringBuilder combinedSql = new StringBuilder();

        for (File sqlFile : sqlFiles) {
            String sqlContent = readFile(sqlFile);
            combinedSql.append(sqlContent).append(";\n");
        }

        // 将合并后的 SQL 内容写入目标文件
        String targetFilePath = targetDir + File.separator + "menu.sql";
        Path targetPath = Paths.get(targetFilePath);
        Files.createDirectories(targetPath.getParent());
        Files.write(targetPath, combinedSql.toString().getBytes());

        System.out.println("合并后的menu SQL脚本已保存到: " + targetFilePath);
    }

    private static String readFile(File file) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }

    /**
     * 执行运行系统shell脚本
     *
     * @param appName
     * @return
     */
    private String executeRunSystemShell(String appName, String savePath) {
        String result = "";
        try {
            // 组合命令和参数
//            String[] command = {"/bin/sh", "bash "+savePath + shellPath + "/runSystem.sh", appName};


            // 执行Shell脚本
            Process process = Runtime.getRuntime().exec("sh " + savePath + shellPath + "/runSystem.sh " + appName);

            // 读取脚本的输出

            BufferedReader reader = new BufferedReader(new BufferedReader(new InputStreamReader(Runtime.getRuntime().exec("ls").getInputStream())));
            String line;
            while ((line = reader.readLine()) != null) {
                result += line;
            }

            // 等待脚本执行完成
//            process.waitFor();
            int exitCode = process.waitFor();
            log.info("Script runSystem executed with exit code: " + exitCode);

//            Thread.sleep(15000);

        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }

    @SneakyThrows
    private String buildNginxConf(String appName, String savePath) {
        String url = "";
        // 读取nginx.conf文件内容并替换{port}和{host_ip}
        // 读取文件内容
        StringBuilder contentBuilder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new FileReader(savePath + configPath + "/nginx.conf"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                contentBuilder.append(line).append("\n");
            }
        }


        String hostIp = executeSelectIpShell(appName, savePath);
        String port = getPortSequence().toString();

        if (ObjectUtil.isAllNotEmpty(hostIp, port)) {
            String content = contentBuilder.toString();
            content = content.replace("{port}", port);
            content = content.replace("{host_ip}", hostIp);

            // 构建输出文件路径
            File outputDir = new File(nginxPath);
            if (!outputDir.exists()) {
                outputDir.mkdirs(); // 如果目录不存在，则创建目录
            }
            File outputFile = new File(outputDir, appName + ".conf");

            // 写入替换后的内容到新文件
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
                writer.write(content);
            }
            log.info("文件内容已成功替换并保存到: " + outputFile.getAbsolutePath());
            url = host + ":" + port;
        }
        // 替换 {port} 和 {host_ip}


        Runtime.getRuntime().exec("nginx -s reload");

        return url;
    }

    /**
     * 执行查找ip shell脚本
     *
     * @param appName
     * @return
     */
    private String executeSelectIpShell(String appName, String savePath) {
        String result = "";
        try {
            // 执行Shell脚本
            Process process = Runtime.getRuntime().exec("sh " + savePath + shellPath + "/selectIp.sh " + appName);

            // 读取脚本的输出
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = reader.readLine()) != null) {
                result += line;
            }

            // 等待脚本执行完成
            int exitCode = process.waitFor();
            log.info("Script selectIp executed with exit code: " + exitCode);
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 获取端口号序列值
     *
     * @return
     * @throws SQLException
     */
    private Long getPortSequence() throws SQLException {
        Long port = 10000L;
        Connection connection = dataSource.getConnection();
        CallableStatement callableStatement = connection.prepareCall("{call get_next_sequence()}");
        boolean hasResult = callableStatement.execute();
        if (hasResult) {
            ResultSet resultSet = callableStatement.getResultSet();
            if (resultSet.next()) {
                port = resultSet.getLong(1);
                log.info("Next sequence value: " + port);
            }
        }
        return port;
    }

}
