import * as fs from 'fs';
import * as path from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import * as morph from 'ts-morph';

import { ENT_DIR } from '../../consts';
import { TYPES, toCamel, schemaParamParser, uncapitalize } from './utils';

const { Project, QuoteKind } = morph;


const EntDir = path.resolve(ENT_DIR);
if (!fs.existsSync(EntDir)) {
    fs.mkdirSync(EntDir);
}

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

    openapi: Record<string, any>;

    schemas: Record<string, any>;

    schemaNames: string[];

    entities: morph.SourceFile[] = [];

    constructor(openapi: Record<string, any>) {
        this.openapi = openapi;
        this.schemas = openapi.components.schemas;
        this.schemaNames = Object.keys(this.schemas);
        this.generateEntities();
    }

    generateEntities = () => {
        this.schemaNames.forEach(this.generateEntity);
    };

    generateEntity = (sName: string) => {
        const { properties, type, oneOf } = this.schemas[sName];
        const notAClass = !properties && TYPES[type as keyof typeof TYPES];

        if (oneOf) {
            this.generateOneOf(sName);
            return;
        }

        if (notAClass) {
            this.generateEnum(sName);
        } else {
            this.generateClass(sName);
        } 
    };

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

        const { enum: enumMembers } = this.schemas[sName];
        entityFile.addEnum({
            name: sName,
            members: enumMembers.map((e: string) => ({ name: e.toUpperCase(), value: e })),
            isExported: true,
        });

        this.entities.push(entityFile);
    };

    generateOneOf = (sName: string) => {
        const entityFile = this.project.createSourceFile(`${EntDir}/${sName}.ts`);
        entityFile.addStatements([
            '// This file was autogenerated. Please do not change.',
            '// All changes will be overwrited on commit.',
            '',
        ]);
        const importEntities: { type: string, isClass: boolean }[] = [];
        const entities = this.schemas[sName].oneOf.map((elem: any) => {
            const [
                pType, isArray, isClass, isImport,
            ] = schemaParamParser(elem, this.openapi);
            importEntities.push({ type: pType, isClass });
            return { type: pType, isArray };
        });
        entityFile.addTypeAlias({
            name: sName,
            isExported: true,
            type: entities.map((e: any) => e.isArray ? `I${e.type}[]` : `I${e.type}`).join(' | '),
        })

        // add import
        importEntities.sort((a, b) => a.type > b.type ? 1 : -1).forEach((ie) => {
            const { type: pType, isClass } = ie;
            if (isClass) {
                entityFile.addImportDeclaration({
                    moduleSpecifier: `./${pType}`,
                    namedImports: [`I${pType}`],
                });
            } else {
                entityFile.addImportDeclaration({
                    moduleSpecifier: `./${pType}`,
                    namedImports: [pType],
                });
            }
        });
        this.entities.push(entityFile);
    }

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


        const { properties: sProps, required, $ref, additionalProperties } = this.schemas[sName];
        if ($ref) {
            const temp = $ref.split('/');
            const importSchemaName = `${temp[temp.length - 1]}`;
            entityFile.addImportDeclaration({
                defaultImport: importSchemaName,
                moduleSpecifier: `./${importSchemaName}`,
                namedImports: [`I${importSchemaName}`],
            });

            entityFile.addTypeAlias({
                name: `I${sName}`,
                type: `I${importSchemaName}`,
                isExported: true,
            })

            entityFile.addStatements(`export default ${importSchemaName};`);
            this.entities.push(entityFile);
            return;
        }

        const importEntities: { type: string, isClass: boolean }[] = [];
        const entityInterface = entityFile.addInterface({
            name: `I${sName}`,
            isExported: true,
        });
        
        const sortedSProps = Object.keys(sProps || {}).sort();
        const additionalPropsOnly = additionalProperties && sortedSProps.length === 0;

        // add server response interface to entityFile
        sortedSProps.forEach((sPropName) => {
            const [
                pType, isArray, isClass, isImport, isAdditional
            ] = schemaParamParser(sProps[sPropName], this.openapi);

            if (isImport) {
                importEntities.push({ type: pType, isClass });
            }
            const propertyType = isAdditional
                ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
                : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
            entityInterface.addProperty({
                name: sPropName,
                type: propertyType,
                hasQuestionToken: !(
                    (required && required.includes(sPropName)) || sProps[sPropName].required
                ),
            });
        });
        if (additionalProperties) {
            const [
                pType, isArray, isClass, isImport, isAdditional
            ] = schemaParamParser(additionalProperties, this.openapi);

            if (isImport) {
                importEntities.push({ type: pType, isClass });
            }
            const type = isAdditional
                ? `{ [key: string]: ${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''} }`
                : `${isClass ? 'I' : ''}${pType}${isArray ? '[]' : ''}`;
            entityInterface.addIndexSignature({
                keyName: 'key',
                keyType: 'string',
                returnType: additionalPropsOnly ? type : `${type} | undefined`,
            });
        }

        // add import
        const imports: { type: string, isClass: boolean }[] = [];
        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) {
                entityFile.addImportDeclaration({
                    defaultImport: pType,
                    moduleSpecifier: `./${pType}`,
                    namedImports: [`I${pType}`],
                });
            } else {
                entityFile.addImportDeclaration({
                    moduleSpecifier: `./${pType}`,
                    namedImports: [pType],
                });
            }
        });

        const entityClass = entityFile.addClass({
            name: sName,
            isDefaultExport: true,
        });

        // addProperties to class;
        sortedSProps.forEach((sPropName) => {
            const [pType, isArray, isClass, isImport, isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);

            const isRequred = (required && required.includes(sPropName))
                || sProps[sPropName].required;

            const propertyType = isAdditional
                ? `{ [key: string]: ${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'} }`
                : `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`;

            entityClass.addProperty({
                name: `_${sPropName}`,
                isReadonly: true,
                type: propertyType,
            });
            const getter = entityClass.addGetAccessor({
                name: toCamel(sPropName),
                returnType: propertyType,
                statements: [`return this._${sPropName};`],
            });
            const { description, example, minItems, maxItems, maxLength, minLength, maximum, minimum } = sProps[sPropName];
            if (description || example) {
                getter.addJsDoc(`${example ? `Description: ${description}` : ''}${example ? `\nExample: ${example}` : ''}`);
            }
            if (minItems) {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MinItems`,
                    statements: [`return ${minItems};`],
                });
            }
            if (maxItems) {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MaxItems`,
                    statements: [`return ${maxItems};`],
                });
            }
            if (typeof minLength === 'number') {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MinLength`,
                    statements: [`return ${minLength};`],
                });
            }
            if (maxLength) {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MaxLength`,
                    statements: [`return ${maxLength};`],
                });
            }
            if (typeof minimum === 'number') {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MinValue`,
                    statements: [`return ${minimum};`],
                });
            }
            if (maximum) {
                entityClass.addGetAccessor({
                    isStatic: true,
                    name: `${toCamel(sPropName)}MaxValue`,
                    statements: [`return ${maximum};`],
                });
            }

            if (!(isArray && isClass) && !isClass) {
                const isEnum = !isClass && isImport;
                const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
                const { maxLength, minLength, maximum, minimum } = sProps[sPropName];
                const haveValidationFields = maxLength || typeof minLength === 'number' || maximum || typeof minimum === 'number';
                if (isRequired || haveValidationFields) {
                    const prop = toCamel(sPropName);
                    const validateField = entityClass.addMethod({
                        isStatic: true,
                        name: `${prop}Validate`,
                        returnType: `boolean`,
                        parameters: [{
                            name: prop,
                            type: `${pType}${isArray ? '[]' : ''}${isRequred ? '' : ' | undefined'}`,
                        }],
                    })
    
                    validateField.setBodyText((w) => {
                        w.write('return ');
                            const nonRequiredCall = isRequired ? prop : `!${prop} ? true : ${prop}`;
                            if (pType === 'string') {
                                if (isArray) {
                                    w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && (typeof p === 'string' && !!p.trim()), true)`);
                                } else {
                                    if (typeof minLength === 'number' && maxLength) {
                                        w.write(`(${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength})`);
                                    }
                                    if (typeof minLength !== 'number' || !maxLength) {
                                        w.write(`${isRequired ? `typeof ${prop} === 'string'` : `!${prop} ? true : typeof ${prop} === 'string'`} && !!${nonRequiredCall}.trim()`);
                                    }
                                }
                            } else if (pType === 'number') {
                                if (isArray) {
                                    w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && typeof p === 'number', true)`);
                                } else {
                                    if (typeof minimum === 'number' && maximum) {
                                        w.write(`${isRequired ? `${prop} >= ${minimum} && ${prop} <= ${maximum}` : `!${prop} ? true : ((${prop} >= ${minimum}) && (${prop} <= ${maximum}))`}`);
                                    }
                                    if (typeof minimum !== 'number' || !maximum) {
                                        w.write(`${isRequired ? `typeof ${prop} === 'number'` : `!${prop} ? true : typeof ${prop} === 'number'`}`);
                                    }
                                }
                            } else if (pType === 'boolean') {
                                w.write(`${isRequired ? `typeof ${prop} === 'boolean'` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
                            } else if (isEnum) {
                                if (isArray){
                                    w.write(`${nonRequiredCall}.reduce<boolean>((result, p) => result && Object.keys(${pType}).includes(${prop}), true)`);
                                } else {
                                    w.write(`${isRequired ? `Object.keys(${pType}).includes(${prop})` : `!${prop} ? true : typeof ${prop} === 'boolean'`}`);
                                }
                            }
                            
                            w.write(';');
                        });
                }
            }
        });
        if (additionalProperties) {
            const [
                pType, isArray, isClass, isImport, isAdditional
            ] = schemaParamParser(additionalProperties, this.openapi);
            const type = `Record<string, ${pType}${isArray ? '[]' : ''}>`;
                
            entityClass.addProperty({
                name: additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`,
                isReadonly: true,
                type: type,
            });
        }
        // add constructor;
        const ctor = entityClass.addConstructor({
            parameters: [{
                name: 'props',
                type: `I${sName}`,
            }],
        });
        ctor.setBodyText((w) => {
            if (additionalProperties) {
                const [
                    pType, isArray, isClass, isImport, isAdditional
                ] = schemaParamParser(additionalProperties, this.openapi);
                w.writeLine(`this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`} = Object.entries(props).reduce<Record<string, ${pType}>>((prev, [key, value]) => {`);
                if (isClass) {
                    w.writeLine(`    prev[key] = new ${pType}(value!);`);
                } else {
                    w.writeLine('    prev[key] = value!;')
                }
                w.writeLine('    return prev;');
                w.writeLine('}, {})');
                return;
            }
            sortedSProps.forEach((sPropName) => {
                const [
                    pType, isArray, isClass, , isAdditional
                ] = schemaParamParser(sProps[sPropName], this.openapi);
                const req = (required && required.includes(sPropName))
                    || sProps[sPropName].required;
                if (!req) {
                    if ((pType === 'boolean' || pType === 'number' || pType ==='string') && !isClass && !isArray) {
                        w.writeLine(`if (typeof props.${sPropName} === '${pType}') {`);
                    } else {
                        w.writeLine(`if (props.${sPropName}) {`);
                    }
                }
                if (isAdditional) {
                    if (isArray && isClass) {
                        w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = props.${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => {
                            return { ...prev, [key]: new ${pType}(p[key])};
                        },{}))`);
                    } else if (isClass) {
                        w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
                            return { ...prev, [key]: new ${pType}(props.${sPropName}[key])};
                        },{})`);
                    } else {
                        if (pType === 'string' && !isArray) {
                            w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
                                return { ...prev, [key]: props.${sPropName}[key].trim()};
                            },{})`);
                        } else {
                            w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = Object.keys(props.${sPropName}).reduce((prev, key) => {
                                return { ...prev, [key]: props.${sPropName}[key]};
                            },{})`);
                        }
                    }
                } else {
                    if (isArray && isClass) {
                        w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = props.${sPropName}.map((p) => new ${pType}(p));`);
                    } else if (isClass) {
                        w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = new ${pType}(props.${sPropName});`);
                    } else {
                        if (pType === 'string' && !isArray) {
                            w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = props.${sPropName}.trim();`);
                        } else {
                            w.writeLine(`${!req ? '    ' : ''}this._${sPropName} = props.${sPropName};`);
                        }
                    }
                }
                if (!req) {
                    w.writeLine('}');
                }
            });

        });

        // add serialize method;
        const serialize = entityClass.addMethod({
            isStatic: false,
            name: 'serialize',
            returnType: `I${sName}`,
        });
        serialize.setBodyText((w) => {
            if (additionalProperties) {
                const [
                    pType, isArray, isClass, isImport, isAdditional
                ] = schemaParamParser(additionalProperties, this.openapi);
                w.writeLine(`return Object.entries(this.${additionalPropsOnly ? 'data' : `${uncapitalize(pType)}Data`}).reduce<Record<string, ${isClass ? 'I' : ''}${pType}>>((prev, [key, value]) => {`);
                if (isClass) {
                    w.writeLine(`    prev[key] = value.serialize();`);
                } else {
                    w.writeLine('    prev[key] = value;')
                }
                w.writeLine('    return prev;');
                w.writeLine('}, {})');
                return;
            }
            w.writeLine(`const data: I${sName} = {`);
            const unReqFields: string[] = [];
            sortedSProps.forEach((sPropName) => {
                const req = (required && required.includes(sPropName))
                    || sProps[sPropName].required;
                const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
                if (!req) {
                    unReqFields.push(sPropName);
                    return;
                }
                if (isAdditional) {
                    if (isArray && isClass) {
                        w.writeLine(`    ${sPropName}: this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }))),`);
                    } else if (isClass) {
                        w.writeLine(`    ${sPropName}: Object.keys(this._${sPropName}).reduce<Record<string, any>>((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {}),`);
                    } else {
                        w.writeLine(`    ${sPropName}: Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] })),`);
                    }
                } else {
                    if (isArray && isClass) {
                        w.writeLine(`    ${sPropName}: this._${sPropName}.map((p) => p.serialize()),`);
                    } else if (isClass) {
                        w.writeLine(`    ${sPropName}: this._${sPropName}.serialize(),`);
                    } else {
                        w.writeLine(`    ${sPropName}: this._${sPropName},`);
                    }
                }

            });
            w.writeLine('};');
            unReqFields.forEach((sPropName) => {
                const [, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);
                w.writeLine(`if (typeof this._${sPropName} !== 'undefined') {`);
                if (isAdditional) {
                    if (isArray && isClass) {
                        w.writeLine(`    data.${sPropName} = this._${sPropName}.map((p) => Object.keys(p).reduce((prev, key) => ({ ...prev, [key]: p[key].serialize() }), {}));`);
                    } else if (isClass) {
                        w.writeLine(`    data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key].serialize() }), {});`);
                    } else {
                        w.writeLine(`    data.${sPropName} = Object.keys(this._${sPropName}).reduce((prev, key) => ({ ...prev, [key]: this._${sPropName}[key] }), {});`);
                    }
                } else {
                    if (isArray && isClass) {
                        w.writeLine(`    data.${sPropName} = this._${sPropName}.map((p) => p.serialize());`);
                    } else if (isClass) {
                        w.writeLine(`    data.${sPropName} = this._${sPropName}.serialize();`);
                    } else {
                        w.writeLine(`    data.${sPropName} = this._${sPropName};`);
                        
                    }
                }
                
                w.writeLine(`}`);
            });
            w.writeLine('return data;');
        });

        // add validate method
        const validate = entityClass.addMethod({
            isStatic: false,
            name: 'validate',
            returnType: `string[]`,
        })
        validate.setBodyText((w) => {
            if (additionalPropsOnly) {
                w.writeLine('return []')
                return;
            }
            w.writeLine('const validate = {');
            Object.keys(sProps || {}).forEach((sPropName) => {
                const [pType, isArray, isClass, , isAdditional] = schemaParamParser(sProps[sPropName], this.openapi);

                const { maxLength, minLength, maximum, minimum } = sProps[sPropName];

                const isRequired = (required && required.includes(sPropName)) || sProps[sPropName].required;
                const nonRequiredCall = isRequired ? `this._${sPropName}` : `!this._${sPropName} ? true : this._${sPropName}`;

                if (isArray && isClass) {
                    w.writeLine(`    ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && p.validate().length === 0, true),`);
                } else if (isClass && !isAdditional) {
                    w.writeLine(`    ${sPropName}: ${nonRequiredCall}.validate().length === 0,`);        
                } else {
                    if (pType === 'string') {
                        if (isArray) {
                            w.writeLine(`    ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'string', true),`);
                        } else {
                            if (typeof minLength === 'number' && maxLength) {
                                w.writeLine(`    ${sPropName}: (${nonRequiredCall}.length >${minLength > 0 ? '=' : ''} ${minLength}) && (${nonRequiredCall}.length <= ${maxLength}),`);
                            }
                            if (typeof minLength !== 'number' || !maxLength) {
                                w.writeLine(`    ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'string'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'string'`} && !this._${sPropName} ? true : this._${sPropName},`);
                            }
                        }
                    } else if (pType === 'number') {
                        if (isArray) {
                            w.writeLine(`    ${sPropName}: ${nonRequiredCall}.reduce((result, p) => result && typeof p === 'number', true),`);
                        } else {
                            if (typeof minimum === 'number' && maximum) {
                                w.writeLine(`    ${sPropName}: ${isRequired ? `this._${sPropName} >= ${minimum} && this._${sPropName} <= ${maximum}` : `!this._${sPropName} ? true : ((this._${sPropName} >= ${minimum}) && (this._${sPropName} <= ${maximum}))`},`);
                            }
                            if (typeof minimum !== 'number' || !maximum) {
                                w.writeLine(`    ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'number'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'number'`},`);
                            }
                        }
                    } else if (pType === 'boolean') {
                        w.writeLine(`    ${sPropName}: ${isRequired ? `typeof this._${sPropName} === 'boolean'` : `!this._${sPropName} ? true : typeof this._${sPropName} === 'boolean'`},`);
                    }
                }
            });
            w.writeLine('};');
            w.writeLine('const isError: string[] = [];')
            w.writeLine('Object.keys(validate).forEach((key) => {');
            w.writeLine('    if (!(validate as any)[key]) {');
            w.writeLine('        isError.push(key);');
            w.writeLine('    }');
            w.writeLine('});');
            w.writeLine('return isError;');
            
        });

        // add update method;
        const update = entityClass.addMethod({
            isStatic: false,
            name: 'update',
            returnType: `${sName}`,
        });
        update.addParameter({
            name: 'props',
            type: additionalPropsOnly ? `I${sName}` : `Partial<I${sName}>`,
        });
        update.setBodyText((w) => { w.writeLine(`return new ${sName}({ ...this.serialize(), ...props });`); });

        this.entities.push(entityFile);
    };

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

export default EntitiesGenerator;
