/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import * as yaml from 'js-yaml';
import { Beans, CamelElement, Integration } from '../model/IntegrationDefinition';
import { BeanFactoryDefinition, RouteConfigurationDefinition, RouteDefinition } from '../model/CamelDefinition';
import { CamelUtil } from './CamelUtil';
import { CamelDefinitionYamlStep } from './CamelDefinitionYamlStep';

export class CamelDefinitionYaml {
    private constructor() {
    }

    static integrationToYaml = (integration: Integration): string => {
        const clone: any = CamelUtil.cloneIntegration(integration);
        const flows = integration.spec.flows;
        clone.spec.flows = flows
            ?.map((f: any) => CamelDefinitionYaml.cleanupElement(f))
            .filter(x => Object.keys(x).length !== 0);
        if (integration.type === 'crd') {
            delete clone.type;
            const i = JSON.parse(JSON.stringify(clone, (key, value) => CamelDefinitionYaml.replacer(key, value), 3)); // fix undefined in string attributes
            return CamelDefinitionYaml.yamlDump(i);
        } else if (integration.type === 'kamelet') {
            delete clone.type;
            // turn array of flows to object properties in template for Kamelet
            const template: any = {route: {}}
            const route: RouteDefinition = clone.spec.flows.filter((f: any) => f.dslName === 'RouteDefinition')?.[0];
            if (route) {
                template.route = Object.assign(template.route, route);
            } else if (clone.spec.template?.route) {
                template.route = clone.spec.template.route;
            } else if (clone.spec.template?.from) {
                template.route = {from: clone.spec.template?.from};
            }
            const from: RouteDefinition = clone.spec.flows.filter((f: any) => f.dslName === 'FromDefinition')?.[0];
            if (from) {
                template.from = {from: from};
            }
            const beans = clone.spec.flows.filter((f: any) => f.dslName === 'Beans')?.at(0)?.beans;
            if (beans) {
                template.beans = beans;
            } else if (clone.spec.template?.beans){
                template.beans = clone.spec.template.beans;
            }
            clone.spec.template = template;
            delete clone.spec.flows;
            const i = JSON.parse(JSON.stringify(clone, (key, value) => CamelDefinitionYaml.replacer(key, value, true), 3)); // fix undefined in string attributes
            return CamelDefinitionYaml.yamlDump(i);
        } else {
            const f = JSON.parse(
                JSON.stringify(clone.spec.flows, (key, value) => CamelDefinitionYaml.replacer(key, value), 3),
            );
            return CamelDefinitionYaml.yamlDump(f);
        }
    };

    static isEmpty = (value?: string): boolean => {
        return value === undefined || (value.trim && value.trim().length === 0);
    };

    static isEmptyObject(obj: any): boolean {
        // Check if it's an object and not null
        if (obj && typeof obj === 'object') {
            // Get all enumerable property names
            const keys = Object.keys(obj);
            // Get all non-enumerable property names
            const nonEnumProps = Object.getOwnPropertyNames(obj);
            // Check if there are no properties
            return keys.length === 0 && nonEnumProps.length === 0;
        }
        return false;
    }


    static cleanupElement = (element: CamelElement, inArray?: boolean, inSteps?: boolean): CamelElement => {
        const result: any = {};
        const object: any = { ...element };

        if (inArray) {
            object.inArray = inArray;
            object.inSteps = !!inSteps;
        }

        if (object.dslName.endsWith('Expression')) {
            delete object.language;
            delete object.expressionName;
        } else if (object.dslName.endsWith('DataFormat')) {
            delete object.dataFormatName;
        } else if (object.dslName === 'BeanFactoryDefinition') {
            if (object.properties && Object.keys(object.properties).length === 0) {
                delete object.properties;
            }
            if (object.constructors && CamelDefinitionYaml.isEmptyObject(object.constructors)) {
                delete object.constructors;
            }
        } else if (object.dslName === 'CatchDefinition' && object?.onWhen?.stepName !== undefined) {
            object.onWhen.stepName = 'onWhen';  // https://github.com/apache/camel-karavan/issues/1420
        }

        delete object.uuid;
        delete object.showChildren;

        for (const [key, value] of Object.entries(object) as [string, any][]) {
            if (value instanceof CamelElement || (typeof value === 'object' && value?.dslName)) {
                result[key] = CamelDefinitionYaml.cleanupElement(value);
            } else if (Array.isArray(value)) {
                if (value.length > 0) {
                    result[key] = CamelDefinitionYaml.cleanupElements(value, key === 'steps');
                }
            } else if (key === 'parameters' && typeof value === 'object') {
                const parameters = Object.entries(value || {})
                    .filter(([_, v]: [string, any]) => !CamelDefinitionYaml.isEmpty(v))
                    .reduce((x: any, [k, v]) => ({ ...x, [k]: v }), {});
                if (Object.keys(parameters).length > 0) {
                    result[key] = parameters;
                }
            } else {
                if (!CamelDefinitionYaml.isEmpty(value)) {
                    result[key] = value;
                }
            }
        }
        return result as CamelElement;
    };

    static cleanupElements = (elements: CamelElement[], inSteps?: boolean): CamelElement[] => {
        const result: any[] = [];
        for (const element of elements) {
            if (typeof element === 'object') {
                result.push(CamelDefinitionYaml.cleanupElement(element, true, inSteps));
            } else {
                result.push(element);
            }
        }
        return result;
    };

    static yamlDump = (integration: any): string => {
        return yaml.dump(integration, {
            noRefs: false,
            noArrayIndent: false,
            // forceQuotes: true,
            quotingType: '"',
            sortKeys: function(a: any, b: any) {
                if (a === 'steps') return 1;
                else if (b === 'steps') return -1;
                else return 0;
            },
        });
    };

    static replacer = (key: string, value: any, isKamelet: boolean = false): any => {
        if (
            typeof value === 'object' &&
            (value.hasOwnProperty('stepName') || value.hasOwnProperty('inArray') || value.hasOwnProperty('inSteps'))
        ) {
            const stepNameField = value.hasOwnProperty('stepName') ? 'stepName' : 'step-name';
            const stepName = value[stepNameField];
            const dslName = value.dslName;
            let newValue: any = JSON.parse(JSON.stringify(value));
            delete newValue.dslName;
            delete newValue[stepNameField];

            if (
                value.inArray &&
                !value.inSteps &&
                ['intercept', 'interceptFrom', 'interceptSendToEndpoint', 'onCompletion', 'onException'].includes(
                    stepName,
                )
            ) {
                delete newValue.inArray;
                delete newValue.inSteps;
                const xValue: any = {};
                xValue[stepName] = newValue;
                return xValue;
            } else if (
                (value.inArray && !value.inSteps) ||
                dslName === 'ExpressionSubElementDefinition' ||
                dslName === 'ExpressionDefinition' ||
                dslName?.endsWith('Expression') ||
                stepName === 'otherwise' ||
                stepName === 'doFinally' ||
                stepName === 'resilience4jConfiguration' ||
                stepName === 'faultToleranceConfiguration' ||
                stepName === 'errorHandler' ||
                stepName === 'onWhen' || // https://github.com/apache/camel-karavan/issues/1420
                stepName === 'deadLetterChannel' ||
                stepName === 'defaultErrorHandler' ||
                stepName === 'jtaTransactionErrorHandler' ||
                stepName === 'noErrorHandler' ||
                stepName === 'springTransactionErrorHandler' ||
                stepName === 'redeliveryPolicy' ||
                stepName === 'securityDefinitions' ||
                stepName === 'apiKey' ||
                stepName === 'basicAuth' ||
                stepName === 'bearer' ||
                stepName === 'mutualTls' ||
                stepName === 'oauth2' ||
                stepName === 'openIdConnect' ||
                stepName === 'openApi' ||
                key === 'from'
            ) {
                delete newValue.inArray;
                delete newValue.inSteps;
                return newValue;
            } else if (isKamelet && dslName === 'RouteDefinition') {
                delete value?.dslName;
                delete value?.stepName;
                return value;
            } else {
                delete newValue.inArray;
                delete newValue.inSteps;
                const xValue: any = {};
                xValue[stepName] = newValue;
                return xValue;
            }
        } else {
            delete value?.dslName;
            return value;
        }
    };

    static yamlToIntegration = (filename: string, text: string): Integration => {
        const integration: Integration = Integration.createNew(filename);
        const fromYaml: any = yaml.load(text);
        const camelized: any = CamelUtil.camelizeObject(fromYaml);
        if (camelized?.apiVersion && camelized.apiVersion.startsWith('camel.apache.org') && camelized.kind) {
            if (camelized?.metadata) {
                integration.metadata = camelized?.metadata;
            }
            if (camelized?.spec) {
                integration.spec.definition = camelized?.spec.definition;
                integration.spec.dependencies = camelized?.spec.dependencies;
                integration.spec.types = camelized?.spec.types;
            }
            const int: Integration = new Integration({ ...camelized });
            if (camelized.kind === 'Integration') {
                integration.type = 'crd';
                integration.spec.flows?.push(...CamelDefinitionYaml.flowsToCamelElements(int.spec.flows || []));
            } else if (camelized.kind === 'Kamelet') {
                integration.type = 'kamelet';
                integration.kind = 'Kamelet';
                const flows: any[] = [];
                // turn kamelet template object properties to array of flows
                const beans = int.spec.template?.beans;
                if (beans) {
                    flows.push(new Beans({beans: beans}))
                }
                const from = int.spec.template?.from;
                if (from) {
                    flows.push(new RouteDefinition({from: from}))
                } else {
                    const route = int.spec.template?.route;
                    flows.push(route);
                }
                integration.spec.flows?.push(...CamelDefinitionYaml.flowsToCamelElements(flows || []));
            }
        } else if (Array.isArray(camelized)) {
            integration.type = 'plain';
            const flows: any[] = camelized;
            integration.spec.flows?.push(...CamelDefinitionYaml.flowsToCamelElements(flows));
        }
        return integration;
    };

    static yamlIsIntegration = (text: string): 'crd' | 'plain' | 'kamelet' | 'none' => {
        try {
            const fromYaml: any = yaml.load(text);
            const camelized: any = CamelUtil.camelizeObject(fromYaml);
            if (camelized?.apiVersion && camelized.apiVersion.startsWith('camel.apache.org') && camelized.kind) {
                if (camelized.kind === 'Integration') {
                    return 'crd';
                } else if (camelized.kind === 'Kamelet') {
                    return 'kamelet';
                }
            } else if (Array.isArray(camelized)) {
                return 'plain';
            } else {
                return 'none';
            }
        } catch (e) {
        }
        return 'none';
    };
    static flowsToCamelElements = (flows: any[]): any[] => {
        const result: any[] = [];
        flows.filter((e: any) => e.hasOwnProperty('restConfiguration'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRestConfigurationDefinition(f.restConfiguration)));
        flows.filter((e: any) => e.hasOwnProperty('rest'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRestDefinition(f.rest)));
        flows.filter((e: any) => e.hasOwnProperty('route'))
            .forEach((f: any) => result.push(CamelDefinitionYamlStep.readRouteDefinition(f.route)));
        flows.filter((e: any) => e.hasOwnProperty('from'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteDefinition(new RouteDefinition({from: f.from}))));
        flows.filter((e: any) => e.hasOwnProperty('beans'))
            .forEach((b: any) => result.push(CamelDefinitionYaml.readBeanDefinition(b)));
        flows.filter((e: any) => e.hasOwnProperty('routeConfiguration'))
            .forEach((e: any) => result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(e.routeConfiguration)));
        flows.filter((e: any) => e.hasOwnProperty('errorHandler'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({errorHandler: f.errorHandler}))));
        flows.filter((e: any) => e.hasOwnProperty('onException'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({onException: f.onException}))));
        flows.filter((e: any) => e.hasOwnProperty('intercept'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({intercept: f.intercept}))));
        flows.filter((e: any) => e.hasOwnProperty('interceptFrom'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({interceptFrom: f.interceptFrom}))));
        flows.filter((e: any) => e.hasOwnProperty('interceptSendToEndpoint'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({interceptSendToEndpoint: f.interceptSendToEndpoint}))));
        flows.filter((e: any) => e.hasOwnProperty('onCompletion'))
            .forEach((f: any) =>  result.push(CamelDefinitionYamlStep.readRouteConfigurationDefinition(new RouteConfigurationDefinition({onCompletion: f.onCompletion}))));

        return result;
    };

    static readBeanDefinition = (beans: any): Beans => {
        const result: Beans = new Beans();
        for (const bean of beans.beans) {
            const props: any = {};
            if (bean && bean.properties) {
                // convert map style to properties if requires
                for (const [key, value] of Object.entries(bean.properties)) {
                    CamelDefinitionYaml.flatMapProperty(key, value, new Map<string, any>()).forEach(
                        (v, k) => (props[k] = v),
                    );
                }
            }
            if (bean && bean.property && Array.isArray(bean.property)) {
                // convert map style to properties if requires
                Array.from(bean.property).forEach((val: any) => {
                    props[val.key] = val.value;
                })
                delete bean.property;
            }
            bean.properties = props;
            result.beans.push(new BeanFactoryDefinition(bean));
        }
        return result;
    };

    // convert map style to properties if requires
    static flatMapProperty = (key: string, value: any, properties: Map<string, any>): Map<string, any> => {
        if (value === undefined) {
            return properties;
        }

        if (typeof value === 'object') {
            for (const k in value) {
                const key2 = key + '.' + k;
                const value2: any = value[k];
                CamelDefinitionYaml.flatMapProperty(key2, value2, new Map<string, any>()).forEach((value1, key1) =>
                    properties.set(key1, value1),
                );
            }
        } else {
            properties.set(key, value);
        }
        return properties;
    };

    // add generated Integration YAML into existing Integration YAML
    static addYamlToIntegrationYaml = (
        filename: string,
        camelYaml: string | undefined,
        restYaml: string,
        addREST: boolean,
        addRoutes: boolean,
    ): string => {
        const existing =
            camelYaml !== undefined
                ? CamelDefinitionYaml.yamlToIntegration(filename, camelYaml)
                : Integration.createNew(filename);
        const generated = CamelDefinitionYaml.yamlToIntegration(filename, restYaml);

        const flows: CamelElement[] =
            existing.spec.flows?.filter(f => !['RouteDefinition', 'RestDefinition'].includes(f.dslName)) || [];

        const restE: CamelElement[] = existing.spec.flows?.filter(f => f.dslName === 'RestDefinition') || [];
        const restG: CamelElement[] = generated.spec.flows?.filter(f => f.dslName === 'RestDefinition') || [];

        if (addREST) {
            flows.push(...restG);
        } else {
            flows.push(...restE);
        }

        const routeE: CamelElement[] = existing.spec.flows?.filter(f => f.dslName === 'RouteDefinition') || [];
        const routeG: CamelElement[] = generated.spec.flows?.filter(f => f.dslName === 'RouteDefinition') || [];

        if (addRoutes) {
            flows.push(...routeG);
        } else {
            flows.push(...routeE);
        }

        existing.spec.flows = flows;
        return CamelDefinitionYaml.integrationToYaml(existing);
    };
}
