/**
 * 根据 Swagger json 自动生成接口服务文件
 */

const config = require('./config');

// 输出文件及目录定义
const ROOT_PATH = './src/services';
const ENUM_FILE = `${ROOT_PATH}/enums.ts`;
const TYPING_D = `${ROOT_PATH}/typing.d.ts`;
const API_PATH = `${ROOT_PATH}/apis`;

// JS关键字
const JS_KEY_WORDS = [
    'break', 'else', 'new', 'var', 'case', 'finally', 'return', 'void', 'catch', 'for', 'switch',
    'while', 'continue', 'function', 'this', 'with', 'default', 'if', 'throw', 'delete', 'in',
    'try', 'do', 'instanceof', 'typeof', 'abstract', 'enum', 'int', 'short', 'boolean', 'export',
    'interface', 'static', 'byte', 'extends', 'long', 'super', 'char', 'final', 'native', 'synchronized',
    'class', 'float', 'package', 'throws', 'const', 'goto', 'private ', 'transient', 'debugger',
    'implements', 'protected ', 'volatile', 'double', 'import', 'public',
];

/** 数值类型映射 */
const IntegerMapping = {
    decimal: 'number',
    int16: 'number',
    int32: 'number',
    int64: 'string',
};

/** 返回结果类型映射 */
const ResultMapping = {
    Decimal: 'number',
    Int16: 'number',
    Int32: 'number',
    Int64: 'string',
    Boolean: 'boolean',
    Object: 'any',
    String: 'string',
    DateTime: 'string',
    FileContentResult: 'FileContentResult',
};

/** 统一注释 */
const UnifyComment = `// @ts-ignore\n/* eslint-disable */\n\n// 该文件自动生成，请勿手动修改！`;

// ----------------------------------------

// 执行外部命令
const execSync = require('child_process').execSync;
// 文件操作
const fs = require('fs');

// ----------------------------------------

/**
 * 下划线中横线分隔字符串转换为驼峰格式
 * @param {string} value 待转换字符串
 * @param {boolean} [capitalize] 首字母大写
 * @returns
 */
const kebabToCamelCase = (value, capitalize = false) => {
    let n = value.replace(/[_-][a-zA-z]/g, (str) => str.substr(-1).toUpperCase()).trim() || '';
    if (capitalize && n.length != 0) {
        n = n.slice(0, 1).toUpperCase() + n.slice(1);
    }
    return n;
};

/**
 * 根据路径生成操作名称
 * @param {string} path 接口路径
 * @returns 取路径最后一级的名称
 */
const getActionName = (path) => {
    const pp = path.split('/');
    if (pp.length == 0) {
        return '';
    }
    const n = pp[pp.length - 1];
    let an = kebabToCamelCase(n);
    if (JS_KEY_WORDS.includes(an.toLocaleLowerCase())) {
        an = `${an}Action`;
    }
    return an;
};

/**
 * 生成控制器名称
 * @param {string} name Swagger的tag名称
 * @returns
 */
const getControllerName = (name) => {
    const n = kebabToCamelCase(name, true);
    return `${n}Controller`;
};

/**
 * 清除文件
 * @param {*} dir 需要清空的目录
 */
const clearFiles = (dir) => {
    const oldFiles = fs.readdirSync(dir);
    oldFiles.forEach((f) => {
        const filePath = `${dir}/${f}`;
        const s = fs.statSync(filePath);
        if (s.isDirectory()) {
            return;
        }
        fs.unlinkSync(filePath);
    });
};

/** 获取引用类型 */
const getRefEntity = ($ref) => {
    const rs = $ref.split('/');
    if (rs.length == 0) {
        return 'any';
    }
    return rs[rs.length - 1];
};

/** 获取简单类型 */
const getSimpleType = ({ type, format }) => {
    if (!type) {
        return 'any';
    }
    const ltype = type.toLowerCase();
    if (['string', 'boolean', 'number'].includes(type)) {
        return ltype;
    }
    if (ltype == 'integer') {
        return IntegerMapping[format] ?? 'number';
    }
    return 'any';
};

/** 获取响应结果类型 */
const getResultType = (type) => {
    const t = ResultMapping[type];
    if (t) {
        return t;
    }
    return `API.${type}`;
};

/** 获取实体类型 */
const getEntityType = (name, schema) => {
    const props = schema.properties;

    let entity = '';
    const desc = schema.description;
    if (desc) {
        entity = `/** ${desc} */\n`;
    }
    entity = `${entity}type ${name} = {\n`;

    const requiredList = schema.required ?? [];

    for (const pname in props) {
        const prop = props[pname];
        let member = '';
        const memberDesc = prop.description;
        if (memberDesc) {
            member = `/** ${memberDesc} */\n`;
        }

        let memberType = '';
        if (prop['$ref']) {
            memberType = `${getRefEntity(prop['$ref'])}`;
        } else if (prop['items']) {
            if (prop['items']['$ref']) {
                memberType = `${getRefEntity(prop['items']['$ref'])}[]`;
            } else {
                memberType = `${getSimpleType(prop['items'])}[]`;
            }
        } else {
            memberType = `${getSimpleType(prop)}`;
        }

        const required = requiredList.includes(pname) || (prop.nullable !== undefined && !prop.nullable);

        member = `${member}${pname}${required ? '' : '?'}: ${memberType};\n`;
        entity = `${entity}${member}`;
    }
    entity = `${entity}}\n`;
    return entity;
};

/** 获取请求类型 */
const getRequestBodyType = (reqBody) => {
    if (reqBody['content']['application/json']) {
        const schema = reqBody['content']['application/json']['schema'];
        let tps = '';
        if (schema['$ref']) {
            tps = getRefEntity(schema['$ref']);
        } else if (schema['items']) {
            if (schema['items']['$ref']) {
                tps = getRefEntity(schema['items']['$ref']);
            } else {
                tps = getSimpleType(schema['items']);
            }
        } else {
            tps = getSimpleType(schema);
        }
        if (schema['type'] == 'array') {
            return `${tps}[]`;
        }
        return tps;
    }
    if (reqBody['content']['multipart/form-data']) {
        return 'FormData';
    }
    return 'any';
};

/** 获取统一返回类型 */
const getXnRestfulResultType = (propRef) => {
    const rtype = getRefEntity(propRef).replaceAll('XnRestfulResult_', '');
    const ps = rtype.split('_');
    if (ps.length == 1) {
        return getResultType(ps);
    }
    if (ps[0] == 'List') {
        return `${getResultType(rtype.replaceAll('List_', ''))}[]`;
    }
    if (ps[0] == 'PageResult') {
        return `API.PageResponse<${getResultType(rtype.replaceAll('PageResult_', ''))}>`;
    }
    return getResultType(rtype);
};

/** 获取响应类型 */
const getResponseType = (res) => {
    const schema = res['200']?.['content']?.['application/json']?.['schema'];
    if (!schema) {
        return 'any';
    }
    if (schema['$ref']) {
        return getXnRestfulResultType(schema['$ref']);
    } else if (schema['items']) {
        if (schema['items']['$ref']) {
            return getXnRestfulResultType(schema['items']['$ref']);
        } else {
            return getSimpleType(schema['items']);
        }
    } else {
        return getSimpleType(schema);
    }
};

/** 获取查询参数类型 */
const getParamsType = (parameters) => {
    let pb = '';
    for (const p of parameters) {
        const pdesc = p.description ?? p.name;
        pb = `${pb}/** ${pdesc} */\n${p.name}${p.required ? '' : '?'}:`;

        const prop = p.schema;
        let ptype = '';
        if (prop['$ref']) {
            ptype = `${getRefEntity(prop['$ref'])}`;
        } else if (prop['items']) {
            if (prop['items']['$ref']) {
                ptype = `${getRefEntity(prop['items']['$ref'])}[]`;
            } else {
                ptype = `${getSimpleType(prop['items'])}[]`;
            }
        } else {
            ptype = `${getSimpleType(prop)}`;
        }

        pb = `${pb}${ptype};\n`;
    }
    return `{\n${pb}}`;
};

/** 判断是否简单类型 */
const isSimpleType = (type) => {
    return ['string', 'number', 'boolean', 'object', 'any', 'formdata'].includes(type.toLowerCase());
};

/** 获取 tag 描述字典 */
const getTagDescDict = (tags) => {
    let tagDict = {};
    for (const tag of tags) {
        tagDict[tag.name] = tag.description ?? tag.name;
    }
    return tagDict;
};

/** 是否为文件响应类型 */
const isFileResponse = (actionName, responseType) => {
    if (responseType == 'FileContentResult') {
        return true;
    }
    const an = actionName.toLowerCase();
    return an.includes('export') || an.includes('download');
}

// ----------------------------------------

/** 生成枚举 */
const generateEnums = (schemas) => {
    const enums = [];
    const importEnums = [];
    for (let ek in schemas) {
        if (schemas[ek].enum) {
            const desc = schemas[ek].description;
            if (!desc) {
                continue;
            }
            const s1 = desc.replaceAll('<br />', '').split('&nbsp;');
            if (s1 && s1.length > 1) {
                const enumComment = s1?.[0];
                const items = [];
                for (let j = 1; j < s1.length; j++) {
                    const s2 = s1[j].split(' ');
                    if (s2.length > 3) {
                        const itemComent = s2[0];
                        const itemCode = s2[1];
                        const itemValue = s2[3];
                        items.push(`\n    /** ${itemComent} */\n    ${itemCode} = ${itemValue},`);
                    }
                }
                enums.push(`\n/** ${enumComment} */\nexport enum ${ek} {${items.join('')}\n}`);
                importEnums.push(ek);
            }
        }
    }

    const fs = require('fs');
    const es = enums.join('\n');
    const ies = importEnums.map((t) => `    ${t}`).join(',\n');
    fs.writeFileSync(ENUM_FILE, `${UnifyComment}\n\n${es}\n\nexport default {\n${ies},\n};\n`);
    execSync(`prettier --write ${ENUM_FILE}`);
    return importEnums;
};

/** 生成类型定义文件 */
const generateEntity = (schemas, enums) => {
    // 生成类型
    let entities = [];
    for (const name in schemas) {
        const schema = schemas[name];
        if (
            name.includes('XnRestfulResult_') ||
            name.includes('PageResult_') ||
            schema.enum ||
            !schema.properties
        ) {
            continue;
        }

        const entity = getEntityType(name, schema);
        entities.push(entity);
    }

    const fs = require('fs');
    const ies = enums.map((t) => `    ${t}`).join(',\n');
    const es = entities.join('\n');
    fs.writeFileSync(
        TYPING_D,
        `${UnifyComment}

        import {
            ${ies},
        } from './enums';

        declare global {
            declare namespace API {
                /** 分页查询请求参数基类型 */
                type PageQueryType = {
                    /** 当前页值 */
                    pageIndex: number;
                    /** 每页大小 */
                    pageSize: number;

                    /** 搜索值 */
                    searchValue?: string;
                    /** 搜索开始时间 */
                    searchBeginTime?: string;
                    /** 搜索结束时间 */
                    searchEndTime?: string;
                    /** 排序字段 */
                    sortField?: string;
                    /** 排序方法，默认升序，否则降序（配合antd前端，约定参数为 Ascend，Dscend） */
                    sortOrder?: string;
                    /** 降序排序（不要问我为什么是descend不是desc，前端约定参数就是这样） */
                    descStr?: string;
                    /** 复杂查询条件 */
                    searchParameters?: Condition[];
                };

                /** 后端服务请求返回参数 */
                type ResponseType<T = any> = {
                    /** 执行成功 */
                    success?: boolean;
                    /** 状态码 */
                    code?: number;
                    /** 错误码 */
                    errorCode?: number;
                    /** 错误信息 */ 
                    errorMessage?: any;
                    /** 消息显示类型 */
                    showType?: number;
                    /** 数据 */
                    data?: T;
                    /** 附加数据 */
                    extras?: any;
                    /** 时间戳 */
                    timestamp?: number;
                };

                /** 分页数据对象 */
                type PageResponse<T = any> = {
                    /** 当前页值  */
                    pageIndex: number;
                    /** 每页大小 */
                    pageSize: number;
                    /** 数据总数 */
                    totalCount: number;
                    /** 总页数 */
                    totalPage?: number;
                    /** 数据行  */
                    items?: T[];
                };

                /** 文件响应类型 */
                type FileResponse = {
                    /** 文件名称 */
                    fileName: string;
                    /** 文件体 */
                    data: Blob;
                };
                
            ${es}}
        }`,
    );
    execSync(`prettier --write ${TYPING_D}`);
};

/** 生成控制器文件 */
const generateController = (paths, tagDict, enums) => {
    // 构造控制器
    let controllers = {};
    for (const apiPath in paths) {
        const actionName = getActionName(apiPath);
        const api = paths[apiPath];
        for (const verb in api) {
            // 只处理 post 和 get，其它忽略
            if (!['post', 'get'].includes(verb)) {
                continue;
            }
            const action = api[verb];
            const actionDesc = action.summary ?? actionName;
            // tag 作为控制器名称，如果没有 tag 则跳过
            const tags = action.tags ?? [];
            if (tags.length < 1) {
                continue;
            }
            for (const tag of tags) {
                const controllerName = getControllerName(tag);
                if (!controllers.hasOwnProperty(controllerName)) {
                    controllers[controllerName] = {
                        description: tagDict[tag],
                        actions: [],
                        actionNames: [],
                        enums: [],
                        blobActions: [],
                    };
                }
                let paramsType = '';
                let dataType = '';
                let responseType = '';
                if (action.parameters) {
                    paramsType = getParamsType(action.parameters);
                }
                if (action.requestBody) {
                    dataType = getRequestBodyType(action.requestBody);
                }
                if (action.responses) {
                    responseType = getResponseType(action.responses);
                }
                const es = enums.filter(t => paramsType.indexOf(t) !== -1 || dataType.indexOf(t) !== -1 || responseType.indexOf(t) !== -1);
                if (es.length > 0) {
                    controllers[controllerName].enums.push(es[0]);
                }

                const actionFullDesc = `/** ${actionDesc} ${verb.toUpperCase()} ${apiPath} */\n`;
                let actionBody = actionFullDesc;
                // file response api
                if (isFileResponse(actionName, responseType)) {
                    controllers[controllerName].blobActions.push(actionName);
                    actionBody = `${actionFullDesc}export async function ${actionName}(`;
                    if (verb == 'get') {
                        if (paramsType != '') {
                            actionBody = `${actionBody}params:${paramsType},`;
                        }
                    } else {
                        if (paramsType != '') {
                            actionBody = `${actionBody}params:${paramsType},`;
                        }
                        if (dataType != '') {
                            const prefix = isSimpleType(dataType) ? '' : 'API.';
                            actionBody = `${actionBody}data:${prefix}${dataType},`;
                        }
                    }
                    actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
                    actionBody = `${actionBody}const url='${apiPath}';\n`;
                    actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
                    if (verb == 'get' && paramsType != '') {
                        actionBody = `${actionBody}params,`;
                    }
                    if (verb == 'post') {
                        let pbv = '';
                        if (paramsType != '') {
                            pbv = 'params,';
                        }
                        if (dataType != '') {
                            pbv = `${pbv}data,`;
                        }
                        actionBody = `${actionBody}${pbv}`;
                    }
                    actionBody = `${actionBody}...(options||{}), responseType: 'blob', getResponse: true} as RequestOptions;\n`;
                    actionBody = `${actionBody}const res = await request(url, config);\n`;
                    actionBody = `${actionBody}const hcd = res.request.getResponseHeader('Content-Disposition');\n`;
                    actionBody = `${actionBody}if(!hcd){ return null; }\n`;
                    actionBody = `${actionBody}let fileName = '';\n`;
                    actionBody = `${actionBody}const cd = contentDisposition.parse(hcd)\n`;
                    actionBody = `${actionBody}if (cd?.parameters?.filename) { fileName = cd?.parameters?.filename; }\n`;
                    actionBody = `${actionBody}return { fileName, data:res.data as Blob } as API.FileResponse;\n}\n`;
                }
                // other api
                else {
                    actionBody = `${actionFullDesc}export async function ${actionName}(`;
                    if (verb == 'get') {
                        if (paramsType != '') {
                            actionBody = `${actionBody}params:${paramsType},`;
                        }
                    } else {
                        if (paramsType != '') {
                            actionBody = `${actionBody}params:${paramsType},`;
                        }
                        if (dataType != '') {
                            const prefix = isSimpleType(dataType) ? '' : 'API.';
                            actionBody = `${actionBody}data:${prefix}${dataType},`;
                        }
                    }
                    actionBody = `${actionBody}options?:{[key:string]:any}){\n`;
                    actionBody = `${actionBody}const url='${apiPath}';\n`;
                    actionBody = `${actionBody}const config={method:'${verb.toUpperCase()}',`;
                    if (verb == 'get' && paramsType != '') {
                        actionBody = `${actionBody}params,`;
                    }
                    if (verb == 'post') {
                        let pbv = '';
                        if (paramsType != '') {
                            pbv = 'params,';
                        }
                        if (dataType != '') {
                            pbv = `${pbv}data,`;
                        }
                        actionBody = `${actionBody}${pbv}`;
                    }
                    actionBody = `${actionBody}...(options||{})};\n`;
                    actionBody = `${actionBody}const res = await request<API.ResponseType<${responseType}>>(url, config);`;
                    actionBody = `${actionBody}return res?.data;\n}\n`;
                }

                controllers[controllerName].actions.push(actionBody);
                controllers[controllerName].actionNames.push(`${actionFullDesc}${actionName},\n`);
            }
        }
    }

    // 生成控制器文件
    let csnames = [];
    for (let ck in controllers) {
        const c = controllers[ck];
        const es = [...new Set(c.enums)].map((t) => `${t}, `);
        let importEnums = '';
        if (es.length > 0) {
            importEnums = `\nimport { ${es.join('')} } from '../enums';`;
        }
        csnames.push({ name: ck, description: c.description });
        const controllerFile = `${API_PATH}/${ck}.ts`;

        const hasBlob = c.blobActions.length > 0;

        fs.writeFileSync(
            controllerFile,
            `${UnifyComment}

            // --------------------------------------------------------------------------
            // ${c.description}
            // --------------------------------------------------------------------------

            import { request${hasBlob ? ', RequestOptions' : ''} } from '@umijs/max';${importEnums}
            ${hasBlob ? 'import contentDisposition from \'content-disposition\';' : ''}

            ${c.actions.join('\n')}

            
            export default {
                ${c.actionNames.join('')}
            };
            `,
        );
        execSync(`prettier --write ${controllerFile}`);
    }

    // 生成index.ts
    const ecs = csnames.map((t) => `import * as ${t.name} from './${t.name}';\n`);
    const edefaults = csnames.map((t) => `/** ${t.description} */\n${t.name},\n`);
    const indexFile = `${API_PATH}/index.ts`;
    fs.writeFileSync(
        indexFile,
        `${UnifyComment}

        ${ecs.join('')}

        export default {
            ${edefaults.join('')}
        };
        `,
    );
    execSync(`prettier --write ${indexFile}`);
};

// ----------------------------------------

/** 生成接口服务 */
const generate = (error, response, body) => {
    // OpenApi 描述文件
    const swagger = JSON.parse(body);
    // 实体模型
    const schemas = swagger['components']['schemas'];
    // API路径
    const apiPaths = swagger['paths'];

    // 开始处理
    console.info('生成后台接口文件');
    console.info(`-----------------\n${new Date().toLocaleString()}\n-----------------\n清理文件`);
    clearFiles(ROOT_PATH);
    if (!fs.existsSync(API_PATH)) {
        fs.mkdirSync(API_PATH);
    } else {
        clearFiles(API_PATH);
    }
    console.info('清理完成！');

    console.log('-----------------\n开始生成');

    // 生成枚举文件
    console.log('> 生成枚举文件');
    const enums = generateEnums(schemas);

    // 生成实体类型文件
    console.log('> 生成实体类型文件');
    generateEntity(schemas, enums);

    // 生成控制器文件
    console.log('> 生成控制器文件');
    generateController(apiPaths, getTagDescDict(swagger['tags'] ?? []), enums);

    console.info('生成完成！');
};

const request = require('request');
request.get(config.swaggerJson, generate);
