/* eslint-disable no-template-curly-in-string */
/* eslint-disable @typescript-eslint/no-unused-expressions */
import * as fs from 'fs';
import * as path from 'path';
import { stringify } from 'qs';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';

import {
    API_DIR as API_DIR_CONST,
    GENERATOR_ENTITY_ALLIAS,
} from '../../consts';
import { toCamel, capitalize, schemaParamParser } from './utils';


const API_DIR = path.resolve(API_DIR_CONST);
if (!fs.existsSync(API_DIR)) {
    fs.mkdirSync(API_DIR);
}

const { Project, QuoteKind } = morph;


class ApiGenerator {
    project = new Project({
        tsConfigFilePath: './tsconfig.json',
        addFilesFromTsConfig: false,
        manipulationSettings: {
            quoteKind: QuoteKind.Single,
            usePrefixAndSuffixTextForRename: false,
            useTrailingCommas: true,
        },
    });

    openapi: Record<string, any>;

    serverUrl: string;

    paths: any;

    /* interface Controllers {
        [controller: string]: {
            [operationId: string]: { parameters - from opneApi, responses - from opneApi, method }
        }
    } */
    controllers: Record<string, any> = {};

    apis: morph.SourceFile[] = [];

    constructor(openapi: Record<string, any>) {
        this.openapi = openapi;
        this.paths = openapi.paths;
        this.serverUrl = openapi.servers[0].url;

        Object.keys(this.paths).forEach((pathKey) => {
            Object.keys(this.paths[pathKey]).forEach((method) => {
                const {
                    tags, operationId, parameters, responses, requestBody, security,
                } = this.paths[pathKey][method];
                const controller = toCamel((tags ? tags[0] : pathKey.split('/')[1]).replace('-controller', ''));

                if (this.controllers[controller]) {
                    this.controllers[controller][operationId] = {
                        parameters,
                        responses,
                        method,
                        requestBody,
                        security,
                        pathKey: pathKey.replace(/{/g, '${'),
                    };
                } else {
                    this.controllers[controller] = { [operationId]: {
                        parameters,
                        responses,
                        method,
                        requestBody,
                        security,
                        pathKey: pathKey.replace(/{/g, '${'),
                    } };
                }
            });
        });

        this.generateApiFiles();
    }

    generateApiFiles = () => {
        Object.keys(this.controllers).forEach(this.generateApiFile);
    };

    generateApiFile = (cName: string) => {
        const apiFile = this.project.createSourceFile(`${API_DIR}/${cName}.ts`);
        apiFile.addStatements([
            '// This file was autogenerated. Please do not change.',
            '// All changes will be overwrited on commit.',
            '',
        ]);

        // const schemaProperties = schemas[schemaName].properties;
        const importEntities: any[] = [];

        // add api class to file
        const apiClass = apiFile.addClass({
            name: `${capitalize(cName)}Api`,
            isDefaultExport: true,
        });

        // get operations of controller
        const controllerOperations = this.controllers[cName]; 
        const operationList = Object.keys(controllerOperations).sort();
        // for each operation add fetcher
        operationList.forEach((operation) => {
            const {
                requestBody, responses, parameters, method, pathKey, security,
            } = controllerOperations[operation];

            const queryParams: any[] = []; // { name, type }
            const bodyParam: any[] = []; // { name, type }

            let hasResponseBodyType: /* boolean | ReturnType<schemaParamParser> */ false | [string, boolean, boolean, boolean, boolean] = false;
            let contentType = '';
            if (parameters) {
                parameters.forEach((p: any) => {
                    const [
                        pType, isArray, isClass, isImport,
                    ] = schemaParamParser(p.schema, this.openapi);

                    if (isImport) {
                        importEntities.push({ type: pType, isClass });
                    }
                    if (p.in === 'query') {
                        queryParams.push({
                            name: p.name, type: `${pType}${isArray ? '[]' : ''}`, hasQuestionToken: !p.required });
                    }
                });
            }
            if (queryParams.length > 0) {
                const imp = apiFile.getImportDeclaration((i) => {
                    return i.getModuleSpecifierValue() === 'qs';
                }); if (!imp) {
                    apiFile.addImportDeclaration({
                        moduleSpecifier: 'qs',
                        defaultImport: 'qs',
                    });
                }
            }
            if (requestBody) {
                let content = requestBody.content;
                const { $ref }: { $ref: string } = requestBody;

                if (!content && $ref) {
                    const name = $ref.split('/').pop() as string;
                    content = this.openapi.components.requestBodies[name].content;
                }
                
                [contentType] = Object.keys(content);
                const data = content[contentType];

                const [
                    pType, isArray, isClass, isImport,
                ] = schemaParamParser(data.schema, this.openapi);

                if (isImport) {
                    importEntities.push({ type: pType, isClass });
                    bodyParam.push({ name: pType.toLowerCase(), type: `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`, isClass, pType });
                } else {
                    bodyParam.push({ name: 'data', type: `${pType}${isArray ? '[]' : ''}` });
                    
                }
            }
            if (responses['200']) {
                const { content, headers } = responses['200'];
                if (content && (content['*/*'] || content['application/json'])) {
                    const { schema, examples } = content['*/*'] || content['application/json'];

                    if (!schema) {
                        process.exit(0);
                    }

                    const propType = schemaParamParser(schema, this.openapi);
                    const [pType, , isClass, isImport] = propType;

                    if (isImport) {
                        importEntities.push({ type: pType, isClass });
                    }
                    hasResponseBodyType = propType;
                }
            }
            let returnType = '';
            if (hasResponseBodyType) {
                const [pType, isArray, isClass] = hasResponseBodyType as any;
                let data = `Promise<${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
                returnType = data;
            } else {
                returnType = 'Promise<number';
            } 
            const shouldValidate = bodyParam.filter(b => b.isClass);
            if (shouldValidate.length > 0) {
                returnType += ' | string[]';
            }
            // append Error to default type return;
            returnType += ' | Error>';

            const fetcher = apiClass.addMethod({
                isAsync: true,
                isStatic: true,
                name: operation,
                returnType,
            });
            const params = [...queryParams, ...bodyParam].sort((a, b) => (Number(!!a.hasQuestionToken) - Number(!!b.hasQuestionToken)));
            fetcher.addParameters(params);

            fetcher.setBodyText((w) => {
                // Add data to URLSearchParams
                if (contentType === 'text/plain') {
                    bodyParam.forEach((b) => {
                        w.writeLine(`const params =  String(${b.name});`);
                    });
                } else {
                    if (shouldValidate.length > 0) {
                        w.writeLine(`const haveError: string[] = [];`);
                        shouldValidate.forEach((b) => {
                            w.writeLine(`const ${b.name}Valid = new ${b.pType}(${b.name});`);
                            w.writeLine(`haveError.push(...${b.name}Valid.validate());`);
                        });
                        w.writeLine(`if (haveError.length > 0) {`);
                        w.writeLine(`    return Promise.resolve(haveError);`)
                        w.writeLine(`}`);
                    }
                }
                // Switch return of fetch in case on queryParams
                if (queryParams.length > 0) {
                    w.writeLine('const queryParams = {');
                    queryParams.forEach((q) => {
                        w.writeLine(`    ${q.name}: ${q.name},`);
                    });
                    w.writeLine('}');
                    w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}?\${qs.stringify(queryParams, { arrayFormat: 'comma' })}\`, {`);
                } else {
                    w.writeLine(`return await fetch(\`${this.serverUrl}${pathKey}\`, {`);
                }
                // Add method
                w.writeLine(`    method: '${method.toUpperCase()}',`);

                // add Fetch options
                if (contentType && contentType !== 'multipart/form-data') {
                    w.writeLine('    headers: {');
                    w.writeLine(`        'Content-Type': '${contentType}',`);
                    w.writeLine('    },');
                }
                if (contentType) {
                    switch (contentType) {
                        case 'text/plain':
                            w.writeLine('    body: params,');
                            break;
                        default:
                            w.writeLine(`    body: JSON.stringify(${bodyParam.map((b) => b.isClass ? `${b.name}Valid.serialize()` : b.name).join(', ')}),`);
                            break;
                    }
                }

                // Handle response
                if (hasResponseBodyType) {
                    w.writeLine('}).then(async (res) => {');
                    w.writeLine('    if (res.status === 200) {');
                    w.writeLine('        return res.json();');
                } else {
                    w.writeLine('}).then(async (res) => {');
                    w.writeLine('    if (res.status === 200) {');
                    w.writeLine('        return res.status;');
                }

                // Handle Error
                w.writeLine('    } else {');
                w.writeLine('        return new Error(String(res.status));');
                w.writeLine('    }');
                w.writeLine('})');
            });
        });

        const imports: any[] = [];
        const types: string[] = [];
        importEntities.forEach((i) => {
            const { type } = i;
            if (!types.includes(type)) {
                imports.push(i);
                types.push(type);
            }
        });
        imports.sort((a,b) => a.type > b.type ? 1 : -1).forEach((ie) => {
            const { type: pType, isClass } = ie;
            if (isClass) {
                apiFile.addImportDeclaration({
                    moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
                    defaultImport: pType,
                    namedImports: [`I${pType}`],
                });
            } else {
                apiFile.addImportDeclaration({
                    moduleSpecifier: `${GENERATOR_ENTITY_ALLIAS}${pType}`,
                    namedImports: [pType],
                });
            }
        });

        this.apis.push(apiFile);
    };

    save = () => {
        this.apis.forEach(async (e) => {
            await e.saveSync();
        });
    };
}


export default ApiGenerator;