/* eslint-disable */
const fs = require('fs');
const glob = require('glob');
const rimraf = require('rimraf');
const nunjucks = require('nunjucks');
const path = require('path');
const lodash = require('lodash');
const reservedWords = require('reserved-words');
const log = require('./log');
const util = require('./util');
const BASE_DIRS = ['service', 'services'];
const getPath = () => {
    const cwd = process.cwd();
    return fs.existsSync(path.join(cwd, 'src')) ? path.join(cwd, 'src') : cwd;
};

const resolveTypeName = (typeName) => {
    if (reservedWords.check(typeName)) {
        return `__openAPI__${typeName}`;
    }
    return typeName;
};

function getRefName(refObject) {
    if (typeof refObject !== 'object' || !refObject.$ref) {
        return refObject;
    }
    const refPaths = refObject.$ref.split('/');
    return resolveTypeName(refPaths[refPaths.length - 1]);
}

const getType = (schemaObject, namespace = '') => {
    if (schemaObject === undefined || schemaObject === null) {
        return 'any';
    }
    if (typeof schemaObject !== 'object') {
        return schemaObject;
    }
    if (schemaObject.$ref) {
        return [namespace, getRefName(schemaObject)].filter((s) => s).join('.');
    }
    let { type } = schemaObject;
    const numberEnum = [
        'int64',
        'integer',
        'long',
        'float',
        'double',
        'number',
        'int',
        'float',
        'double',
        'int32',
        'int64',
    ];
    const dateEnum = ['Date', 'date', 'dateTime', 'date-time', 'datetime'];
    const stringEnum = ['string', 'email', 'password', 'url', 'byte', 'binary'];
    if (numberEnum.includes(schemaObject.format)) {
        type = 'number';
    }
    if (schemaObject.enum) {
        type = 'enum';
    }
    if (numberEnum.includes(type)) {
        return 'number';
    }
    if (dateEnum.includes(type)) {
        return 'Date';
    }
    if (stringEnum.includes(type)) {
        return 'string';
    }
    if (type === 'boolean') {
        return 'boolean';
    }
    if (type === 'array') {
        let { items } = schemaObject;
        if (schemaObject.schema) {
            items = schemaObject.schema.items;
        }
        if (Array.isArray(items)) {
            const arrayItemType = items
                .map((subType) => getType(subType.schema || subType, namespace))
                .toString();
            return `[${arrayItemType}]`;
        }
        return `${getType(items, namespace)}[]`;
    }
    if (type === 'enum') {
        return Array.isArray(schemaObject.enum)
            ? Array.from(new Set(schemaObject.enum.map((v) => typeof v === 'string' ? `"${v.replace(/"/g, '"')}"` : getType(v)))).join(' | ')
            : 'string';
    }
    if (schemaObject.oneOf && schemaObject.oneOf.length) {
        return schemaObject.oneOf.map((item) => getType(item, namespace)).join(' | ');
    }
    if (schemaObject.type === 'object' || schemaObject.properties) {
        if (!Object.keys(schemaObject.properties || {}).length) {
            return 'Record<string, any>';
        }
        return `{ ${Object.keys(schemaObject.properties)
            .map((key) => {
            const required = 'required' in (schemaObject.properties[key] || {})
                ? (schemaObject.properties[key] || {}).required
                : false;
            return `${key}${required ? '' : '?'}: ${getType(schemaObject.properties && schemaObject.properties[key], namespace)}; `;
        })
            .join('')}}`;
    }
    return 'any';
};

const getGenInfo = (isDirExist, appName, absSrcPath) => {
    // dir 不存在，则没有占用，且为第一次
    if (!isDirExist) {
        return [false, true];
    }
    const indexList = glob.sync(`@(${BASE_DIRS.join('|')})/${appName}/index.@(js|ts)`, {
        cwd: absSrcPath,
    });
    // dir 存在，且 index 存在
    if (indexList && indexList.length) {
        const indexFile = path.join(absSrcPath, indexList[0]);
        try {
            const line = (fs.readFileSync(indexFile, 'utf-8') || '').split(/\r?\n/).slice(0, 3).join('');
            // dir 存在，index 存在， 且 index 是我们生成的。则未占用，且不是第一次
            if (line.includes('// API 更新时间：')) {
                return [false, false];
            }
            // dir 存在，index 存在，且 index 内容不是我们生成的。此时如果 openAPI 子文件存在，就不是第一次，否则是第一次
            return [true, !fs.existsSync(path.join(indexFile, 'openAPI'))];
        }
        catch (e) {
            // 因为 glob 已经拿到了这个文件，但没权限读，所以当作 dirUsed, 在子目录重新新建，所以当作 firstTime
            return [true, true];
        }
    }
    // dir 存在，index 不存在, 冲突，第一次要看 dir 下有没有 openAPI 文件夹
    return [
        true,
        !(fs.existsSync(path.join(absSrcPath, BASE_DIRS[0], appName, 'openAPI')) ||
            fs.existsSync(path.join(absSrcPath, BASE_DIRS[1], appName, 'openAPI'))),
    ];
};

const DEFAULT_SCHEMA = {
    type: 'object',
    properties: { id: { type: 'number' } },
};

class ServiceGenerator {
    constructor(config, openAPIData) {
        this.apiData = {};
        this.classNameList = [];
        this.mappings = [];
        this.finalPath = '';
        this.config = {
            projectName: 'api',
            ...config,
        };
        this.openAPIData = openAPIData;
        const { info } = openAPIData;
        const basePath = config.basePath || '';
        this.version = info.version;
        Object.keys(openAPIData.paths || {}).forEach((p) => {
            const pathItem = openAPIData.paths[p];
            ['get', 'put', 'post', 'delete', 'patch'].forEach((method) => {
                const operationObject = pathItem[method];
                if (!operationObject) {
                    return;
                }
                (operationObject.tags || [p.replace('/', '').split('/')[1]]).forEach((tag) => {
                    if (!this.apiData[tag]) {
                        this.apiData[tag] = [];
                    }
                    this.apiData[tag].push({
                        path: `${basePath}${p}`,
                        method,
                        ...operationObject,
                    });
                });
            });
        });
    }
    genFile() {
        const basePath = this.config.serversPath || './src/api/generateAPI';
        try {
            const finalPath = path.join(basePath, this.config.projectName);
            this.finalPath = finalPath;
                glob
                .sync(`${finalPath}/**/*`)
                .filter((ele) => !ele.includes('_deperated'))
                .forEach((ele) => {
                rimraf.sync(ele);
            });
        }
        catch (error) {
            log(`🚥 serves 生成失败: ${error}`);
        }
        // // 生成 ts 类型声明
        // this.genFileFromTemplate('typings.d.ts', 'interface', {
        //     namespace: this.config.namespace,
        //     // namespace: 'API',
        //     list: this.getInterfaceTP(),
        //     disableTypeCheck: false,
        // });
        // 生成 controller 文件
        const prettierError = [];
        // 生成 service 统计
        this.getServiceTP().forEach((tp) => {
            // 根据当前数据源类型选择恰当的 controller 模版
            const template = 'serviceController';
            const hasError = this.genFileFromTemplate(this.getFinalFileName(`${tp.className}.js`), template, {
                namespace: this.config.namespace,
                requestImportStatement: this.config.requestImportStatement,
                disableTypeCheck: false,
                ...tp,
            });
            prettierError.push(hasError);
        });
        if (prettierError.includes(true)) {
            log(`🚥 格式化失败，请检查 service 文件内可能存在的语法错误`);
        }
        // 生成 index 文件
        // this.genFileFromTemplate(`index.ts`, 'serviceIndex', {
        //     list: this.classNameList,
        //     disableTypeCheck: false,
        // });
        this.genFileFromTemplate(`index.js`, 'serviceIndex', {
            list: this.classNameList,
            disableTypeCheck: false,
        });

        // 打印日志
        log(`✅ 成功生成 service 文件`);
    }
    getServiceTP() {
        return Object.keys(this.apiData)
            .map((tag) => {
            // functionName tag 级别防重
            const tmpFunctionRD = {};
            const genParams = this.apiData[tag]
                .filter((api) =>
            // 暂不支持变量
            !api.path.includes('${'))
                .map((api) => {
                const newApi = api;
                try {
                    const allParams = this.getParamsTP(newApi.parameters);
                    const { file, ...params } = allParams || {};
                    const body = this.getBodyTP(newApi.requestBody);
                    const response = this.getResponseTP(newApi.responses);
                    let formData = false;
                    if ((body && (body.mediaType || '').includes('form')) || file) {
                        formData = true;
                    }
                    const operationId = newApi.operationId ? newApi.operationId: (
                        newApi.path.substring(newApi.path.lastIndexOf("\/") + 1, newApi.path.length)
                    );
                    let functionName = this.config.hook && this.config.hook.customFunctionName
                        ? this.config.hook.customFunctionName(newApi)
                        : this.resolveFunctionName(util.stripDot(operationId), newApi.method);
                    if (functionName && tmpFunctionRD[functionName]) {
                        functionName = `${functionName}_${(tmpFunctionRD[functionName] += 1)}`;
                    }
                    else if (functionName) {
                        tmpFunctionRD[functionName] = 1;
                    }
                    let formattedPath = newApi.path.replace(/:([^/]*)|{([^}]*)}/gi, (_, str, str2) => `$\{${str || str2}}`);
                    if (newApi.extensions && newApi.extensions['x-antTech-description']) {
                        const { extensions } = newApi;
                        const { apiName, antTechVersion, productCode, antTechApiName } = extensions['x-antTech-description'];
                        formattedPath = antTechApiName || formattedPath;
                        this.mappings.push({
                            antTechApi: formattedPath,
                            popAction: apiName,
                            popProduct: productCode,
                            antTechVersion,
                        });
                        newApi.antTechVersion = antTechVersion;
                    }
                    // 为 path 中的 params 添加 alias
                    const escapedPathParams = ((params || {}).path || []).map((ele, index) => ({
                        ...ele,
                        alias: `param${index}`,
                    }));
                    if (escapedPathParams.length) {
                        escapedPathParams.forEach((param) => {
                            formattedPath = formattedPath.replace(`$\{${param.name}}`, `$\{${param.alias}}`);
                        });
                    }
                    const finalParams = escapedPathParams && escapedPathParams.length
                        ? { ...params, path: escapedPathParams }
                        : params;
                    // 处理 query 中的复杂对象
                    if (finalParams && finalParams.query) {
                        finalParams.query = finalParams.query.map((ele) => ({
                            ...ele,
                            isComplexType: ele.isObject,
                        }));
                    }
                    const getPrefixPath = () => {
                        if (!this.config.apiPrefix) {
                            return formattedPath;
                        }
                        // 静态 apiPrefix
                        const prefix = typeof this.config.apiPrefix === 'function'
                            ? `${this.config.apiPrefix({
                                path: formattedPath,
                                method: newApi.method,
                                namespace: tag,
                                functionName,
                            })}`.trim()
                            : this.config.apiPrefix.trim();
                        if (!prefix) {
                            return formattedPath;
                        }
                        if (prefix.startsWith("'") || prefix.startsWith('"') || prefix.startsWith('`')) {
                            const finalPrefix = prefix.slice(1, prefix.length - 1);
                            if (formattedPath.startsWith(finalPrefix) ||
                                formattedPath.startsWith(`/${finalPrefix}`)) {
                                return formattedPath;
                            }
                            return `${finalPrefix}${formattedPath}`;
                        }
                        // prefix 变量
                        return `$\{${prefix}}${formattedPath}`;
                    };

                    return {
                        ...newApi,
                        functionName,
                        path: getPrefixPath(),
                        pathInComment: formattedPath.replace(/\*/g, '&#42;'),
                        hasPathVariables: formattedPath.includes('{'),
                        hasApiPrefix: !!this.config.apiPrefix,
                        method: newApi.method,
                        // 如果 functionName 和 summary 相同，则不显示 summary
                        desc: functionName === newApi.summary
                            ? newApi.description
                            : [newApi.summary, newApi.description].filter((s) => s).join(' '),
                        hasHeader: !!(params && params.header) || !!(body && body.mediaType),
                        params: finalParams,
                        hasParams: Boolean(Object.keys(finalParams || {}).length),
                        body,
                        file,
                        hasFormData: formData,
                        response,
                    };
                }
                catch (error) {
                    // eslint-disable-next-line no-console
                    console.error('[GenSDK] gen service param error:', error);
                    throw error;
                }
            });
            const fileName = this.replaceDot(tag);
            if (genParams.length) {
                this.classNameList.push({
                    fileName,
                    controllerName: fileName,
                });
            }
            return {
                genType: 'js',
                className: fileName,
                instanceName: `${fileName[0].toLowerCase()}${fileName.substr(1)}`,
                list: genParams,
            };
        })
            .filter((ele) => !!ele.list.length);
    }
    getBodyTP(requestBody = {}) {
        const reqBody = this.resolveRefObject(requestBody);
        if (!reqBody) {
            return null;
        }
        const reqContent = reqBody.content;
        if (typeof reqContent !== 'object') {
            return null;
        }
        let mediaType = Object.keys(reqContent)[0];
        const schema = reqContent[mediaType].schema || DEFAULT_SCHEMA;
        if (mediaType === '*/*') {
            mediaType = '';
        }
        // 如果 requestBody 有 required 属性，则正常展示；如果没有，默认非必填
        const required = typeof requestBody.required === 'boolean' ? requestBody.required : false;
        if (schema.type === 'object' && schema.properties) {
            const propertiesList = Object.keys(schema.properties).map((p) => {
                if (schema.properties && schema.properties[p]) {
                    return {
                        key: p,
                        schema: {
                            ...schema.properties[p],
                            type: getType(schema.properties[p], this.config.namespace),
                        },
                    };
                }
                return undefined;
            });
            return {
                mediaType,
                ...schema,
                required,
                propertiesList,
            };
        }
        return {
            mediaType,
            required,
            type: getType(schema, this.config.namespace),
        };
    }
    getResponseTP(responses = {}) {
        const response = responses && this.resolveRefObject(responses.default || responses['200']);
        const defaultResponse = {
            mediaType: '*/*',
            type: 'any',
        };
        if (!response) {
            return defaultResponse;
        }
        const resContent = response.content;
        const mediaType = Object.keys(resContent || {})[0];
        if (typeof resContent !== 'object' || !mediaType) {
            return defaultResponse;
        }
        const schema = resContent[mediaType].schema || DEFAULT_SCHEMA;
        let responseType = mediaType;
        if (mediaType === 'application/json') {
            responseType = '';
        }
        else if (mediaType === 'text/plain') {
            responseType = 'text';
        }
        return {
            mediaType,
            responseType,
            type: getType(schema, this.config.namespace),
        };
    }
    getParamsTP(parameters = []) {
        if (!parameters || !parameters.length) {
            return {};
        }
        const templateParams = {};
        ['query', 'header', 'path', 'cookie', 'file'].forEach((source) => {
            const params = parameters
                .map((p) => this.resolveRefObject(p))
                .filter((p) => p.in === source)
                .map((p) => {
                const isDirectObject = ((p.schema || {}).type || p.type) === 'object';
                const refList = ((p.schema || {}).$ref || p.$ref || '').split('/');
                const ref = refList[refList.length - 1];
                const deRefObj = (Object.entries(this.openAPIData.components.schemas || {}).find(([k]) => k === ref) || []);
                const isRefObject = (deRefObj[1] || {}).type === 'object';
                return {
                    ...p,
                    isObject: isDirectObject || isRefObject,
                    type: getType(p.schema || DEFAULT_SCHEMA, this.config.namespace),
                };
            });
            if (params.length) {
                templateParams[source] = params;
            }
        });
        return templateParams;
    }
    getInterfaceTP() {
        const { components } = this.openAPIData;
        const data = components &&
            [components.schemas].map((defines) => {
                if (!defines) {
                    return null;
                }
                return Object.keys(defines).map((typeName) => {
                    const result = this.resolveObject(defines[typeName]);
                    const getDefinesType = () => {
                        if (result.type) {
                            return defines[typeName].type === 'object' || result.type;
                        }
                        return 'Record<string, any>';
                    };
                    return {
                        typeName: resolveTypeName(typeName),
                        type: getDefinesType(),
                        parent: result.parent,
                        props: result.props || [],
                    };
                });
            });
        return data && data.reduce((p, c) => p && c && p.concat(c), []);
    }
    genFileFromTemplate(fileName, type, params) {
        try {
            const template = this.getTemplate(type);
            // 设置输出不转义
            nunjucks.configure({
                autoescape: false,
            });
            return util.writeFile(this.finalPath, fileName, nunjucks.renderString(template, params));
        }
        catch (error) {
            // eslint-disable-next-line no-console
            console.error('[GenSDK] file gen fail:', fileName, 'type:', type);
            throw error;
        }
    }
    getTemplate(type) {
        return fs.readFileSync(path.join(__dirname, 'templates', `${type}.njk`), 'utf8');
    }
    // 获取 TS 类型的属性列表
    getProps(schemaObject) {
        const requiredPropKeys = schemaObject.required;
        return schemaObject.properties
            ? Object.keys(schemaObject.properties).map((propName) => {
                const schema = (schemaObject.properties && schemaObject.properties[propName]) || DEFAULT_SCHEMA;
                return {
                    ...schema,
                    name: propName,
                    type: getType(schema),
                    desc: [schema.title, schema.description].filter((s) => s).join(' '),
                    // 如果没有 required 信息，默认全部是非必填
                    required: requiredPropKeys ? requiredPropKeys.some((key) => key === propName) : false,
                };
            })
            : [];
    }
    resolveObject(schemaObject) {
        // 引用类型
        if (schemaObject.$ref) {
            return this.resolveRefObject(schemaObject);
        }
        // 枚举类型
        if (schemaObject.enum) {
            return this.resolveEnumObject(schemaObject);
        }
        // 继承类型
        if (schemaObject.allOf && schemaObject.allOf.length) {
            return this.resolveAllOfObject(schemaObject);
        }
        // 对象类型
        if (schemaObject.properties) {
            return this.resolveProperties(schemaObject);
        }
        // 数组类型
        if (schemaObject.items && schemaObject.type === 'array') {
            return this.resolveArray(schemaObject);
        }
        return schemaObject;
    }
    resolveArray(schemaObject) {
        if (schemaObject.items.$ref) {
            const refObj = schemaObject.items.$ref.split('/');
            return {
                type: `${refObj[refObj.length - 1]}[]`,
            };
        }
        // TODO: 这里需要解析出具体属性，但由于 parser 层还不确定，所以暂时先返回 any
        return 'any[]';
    }
    resolveProperties(schemaObject) {
        return {
            props: this.getProps(schemaObject),
        };
    }
    resolveEnumObject(schemaObject) {
        const enumArray = schemaObject.enum;
        const enumStr = Array.from(new Set(enumArray.map((v) => (typeof v === 'string' ? `"${v.replace(/"/g, '"')}"` : getType(v))))).join(' | ');
        return {
            type: Array.isArray(enumArray) ? enumStr : 'string',
        };
    }
    resolveAllOfObject(schemaObject) {
        const allOf = schemaObject.allOf || [];
        // 暂时只支持单继承，且父类必须是第一个元素
        const parent = allOf[0] && allOf[0].$ref ? getType(allOf[0]) : undefined;
        let props = [];
        if (allOf.length > 1) {
            props = lodash.flatten(allOf.slice(1).map((item) => this.getProps(item)));
        }
        return {
            parent,
            // 属性合并: 根据属性名进行去重
            props: lodash.uniqBy(props, 'name'),
        };
    }
    resolveRefObject(refObject) {
        if (!refObject || !refObject.$ref) {
            return refObject;
        }
        const refPaths = refObject.$ref.split('/');
        if (refPaths[0] === '#') {
            refPaths.shift();
            let obj = this.openAPIData;
            refPaths.forEach((node) => {
                obj = obj[node];
            });
            if (!obj) {
                throw new Error(`[GenSDK] Data Error! Notfoud: ${refObject.$ref}`);
            }
            return {
                ...this.resolveRefObject(obj),
                type: obj.$ref ? this.resolveRefObject(obj).type : obj,
            };
        }
        return refObject;
    }
    getFinalFileName(s) {
        // 支持下划线、中划线和空格分隔符，注意分隔符枚举值的顺序不能改变，否则正则匹配会报错
        return s.replace(/[-_ ](\w)/g, (_all, letter) => letter.toUpperCase());
    }
    replaceDot(s) {
        return s.replace(/\./g, '_').replace(/[-_ ](\w)/g, (_all, letter) => letter.toUpperCase());
    }
    resolveFunctionName(functionName, methodName) {
        // 类型声明过滤关键字
        if (reservedWords.check(functionName)) {
            return `${functionName}Using${methodName.toUpperCase()}`;
        }
        return functionName;
    }
}

module.exports = {
  getPath,
  getGenInfo,
  ServiceGenerator
};
