/*
 * 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 {
    DeleteDefinition,
    FromDefinition,
    GetDefinition,
    HeadDefinition,
    PatchDefinition,
    PostDefinition,
    PutDefinition,
    RestDefinition, RouteConfigurationDefinition, RouteDefinition, SagaDefinition,
} from '../model/CamelDefinition';
import {
    CamelElement,
    Integration,
} from '../model/IntegrationDefinition';
import {
    TopologyIncomingNode,
    TopologyOutgoingNode,
    TopologyRestNode, TopologyRouteConfigurationNode,
    TopologyRouteNode,
} from '../model/TopologyDefinition';
import { ComponentApi, INTERNAL_COMPONENTS } from './ComponentApi';
import { CamelDefinitionApiExt } from './CamelDefinitionApiExt';
import { CamelDisplayUtil } from './CamelDisplayUtil';
import { CamelUtil } from './CamelUtil';

const outgoingDefinitions: string[] = ['ToDefinition', 'KameletDefinition', 'ToDynamicDefinition', 'PollEnrichDefinition', 'EnrichDefinition', 'WireTapDefinition', 'SagaDefinition', 'PollDefinition'];

export class ChildElement {
    constructor(public name: string = '', public className: string = '', public multiple: boolean = false) {
    }
}

export class TopologyUtils {
    private constructor() {
    }

    static getOutgoingDefinitions = (): string[] => {
        return outgoingDefinitions;
    };

    static isElementInternalComponent = (element: CamelElement): boolean => {
        const uri = (element as any).uri;
        const component = ComponentApi.findByName(uri);
        if (INTERNAL_COMPONENTS.includes(uri?.split(':')?.[0])) return true;
        return component !== undefined && component.component.remote !== true;
    };

    static getConnectorType = (element: CamelElement): 'component' | 'kamelet' => {
        return CamelUtil.isKameletComponent(element) ? 'kamelet' : 'component';
    };

    static cutKameletUriSuffix = (uri: string): string => {
        if (uri.endsWith('-sink')) {
            return uri.substring(0, uri.length - 5);
        } else if (uri.endsWith('-source')) {
            return uri.substring(0, uri.length - 7);
        } else if (uri.endsWith('-action')) {
            return uri.substring(0, uri.length - 7);
        } else {
            return uri;
        }
    };

    static getUniqueUri = (element: CamelElement): string => {
        const uri: string = (element as any).uri || '';
        let result = uri.startsWith('kamelet') ? TopologyUtils.cutKameletUriSuffix(uri).concat(':') : uri.concat(':');
        const className = element.dslName;
        if (className === 'FromDefinition' || className === 'ToDefinition') {
            if (!CamelUtil.isKameletComponent(element)) {
                const requiredProperties = CamelUtil.getComponentProperties(element).filter(p => p.required);
                for (const property of requiredProperties) {
                    const value = CamelDefinitionApiExt.getParametersValue(element, property.name, property.kind === 'path');
                    if (value !== undefined && property.type === 'string' && value.trim().length > 0) {
                        result = result + property.name + '=' + value + '&';
                    }
                }
            } else {
                const requiredProperties = CamelUtil.getKameletProperties(element, true);
                for (const property of requiredProperties) {
                    const value = CamelDefinitionApiExt.getParametersValue(element, property.id);
                    if (value !== undefined && property.type === 'string' && value.toString().trim().length > 0) {
                        result = result + property.id + '=' + value + '&';
                    }
                }
            }
        }
        return result.endsWith('&') ? result.substring(0, result.length - 1) : result;
    };

    static hasDirectUri = (element: CamelElement): boolean => {
        return this.hasUriStartWith(element, 'direct');
    };

    static hasSedaUri = (element: CamelElement): boolean => {
        return this.hasUriStartWith(element, 'seda');
    };

    static hasUriStartWith = (element: CamelElement, text: string): boolean => {
        if ((element as any).uri && typeof (element as any).uri === 'string') {
            return (element as any).uri.startsWith(text);
        } else if (element.dslName === 'SagaDefinition') {
            const completion = (element as SagaDefinition).completion || '';
            const compensation = (element as SagaDefinition).compensation || '';
            return completion.startsWith(text) || compensation.startsWith(text);
        } else {
            return false;
        }
    };

    static findTopologyRestNodes = (integration: Integration[]): TopologyRestNode[] => {
        const result: TopologyRestNode[] = [];
        integration.forEach(i => {
            try {
                const filename = i.metadata.name;
                const routes = i.spec.flows?.filter(flow => flow.dslName === 'RestDefinition');
                routes?.forEach((rest: RestDefinition) => {
                    const uris: string[] = [];
                    rest?.get?.forEach((d: GetDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    rest?.post?.forEach((d: PostDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    rest?.put?.forEach((d: PutDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    rest?.delete?.forEach((d: DeleteDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    rest?.patch?.forEach((d: PatchDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    rest?.head?.forEach((d: HeadDefinition) => {
                        if (d.to) uris.push(d.to);
                    });
                    const title = '' + (rest.description ? rest.description : rest.id);
                    result.push(new TopologyRestNode(rest.path || '', '' + rest.id, uris, title, filename, rest));
                });
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findTopologyIncomingNodes = (integration: Integration[]): TopologyIncomingNode[] => {
        const result: TopologyIncomingNode[] = [];
        integration.forEach(i => {
            try {
                const filename = i.metadata.name;
                const routes = i.spec.flows?.filter(flow => flow.dslName === 'RouteDefinition');
                const routeElements = routes?.map(r => {
                    const id = 'incoming-' + r.id;
                    const title = CamelDisplayUtil.getStepDescription(r.from);
                    const type = TopologyUtils.isElementInternalComponent(r.from) ? 'internal' : 'external';
                    const connectorType = TopologyUtils.getConnectorType(r.from);
                    const uniqueUri = TopologyUtils.getUniqueUri(r.from);
                    return new TopologyIncomingNode(id, type, connectorType, r.id, title, filename, r.from, uniqueUri);
                }) || [];
                result.push(...routeElements);
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findTopologyRouteNodes = (integration: Integration[]): TopologyRouteNode[] => {
        const result: TopologyRouteNode[] = [];
        integration.forEach(i => {
            try {
                const filename = i.metadata.name;
                const routes = i.spec.flows?.filter(flow => flow.dslName === 'RouteDefinition');
                const routeElements = routes?.map(r => {
                    const id = 'route-' + r.id;
                    const title = '' + (r.description ? r.description : r.id);
                    return new TopologyRouteNode(id, r.id, title, filename, r.from, r);
                }) || [];
                result.push(...routeElements);
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findTopologyRouteConfigurationNodes = (integration: Integration[]): TopologyRouteConfigurationNode[] => {
        const result: TopologyRouteConfigurationNode[] = [];
        integration.forEach(i => {
            try {
                const filename = i.metadata.name;
                const routes = i.spec.flows?.filter(flow => flow.dslName === 'RouteConfigurationDefinition');
                const routeElements = routes?.map(r => {
                    const id = 'route-' + r.id;
                    const title = '' + (r.description ? r.description : r.id);
                    return new TopologyRouteConfigurationNode(id, r.id, title, filename, r);
                }) || [];
                result.push(...routeElements);
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findTopologyRouteOutgoingNodes = (integrations: Integration[]): TopologyOutgoingNode[] => {
        const result: TopologyOutgoingNode[] = [];
        integrations.forEach(i => {
            try {
                const filename = i.metadata.name;
                const routes = i.spec.flows?.filter(flow => flow.dslName === 'RouteDefinition');
                routes?.forEach(route => {
                    const from: FromDefinition = route.from;
                    const elements = TopologyUtils.findOutgoingInStep(from, []);
                    elements.forEach((e: any) => {
                        const id = 'outgoing-' + route.id + '-' + e.id;
                        const title = CamelDisplayUtil.getStepDescription(e);
                        const type = TopologyUtils.isElementInternalComponent(e) ? 'internal' : 'external';
                        const connectorType = TopologyUtils.getConnectorType(e);
                        const uniqueUri = TopologyUtils.getUniqueUri(e);
                        if (
                            connectorType !== 'kamelet' ||
                            CamelUtil.getKamelet(e)?.metadata.labels['camel.apache.org/kamelet.type'] !== 'action'
                        ) {
                            result.push(new TopologyOutgoingNode(id, type, connectorType, route.id, title, filename, e, uniqueUri));
                        }
                    });
                    result.push(...TopologyUtils.findDeadLetterChannelNodes(route, filename));
                });
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findDeadLetterChannelNodes(route: RouteDefinition, filename: string): TopologyOutgoingNode[] {
        const result: TopologyOutgoingNode[] = [];
        try {
            const deadLetterChannel = route.errorHandler?.deadLetterChannel;
            const deadLetterUri = deadLetterChannel?.deadLetterUri;
            if (deadLetterChannel !== undefined && deadLetterUri !== undefined) {
                const parts = deadLetterUri.split(':');
                if (parts.length > 1 && INTERNAL_COMPONENTS.includes(parts[0])) {
                    const id = 'outgoing-' + route.id + '-' + deadLetterChannel?.id;
                    const title = CamelDisplayUtil.getStepDescription(deadLetterChannel);
                    const type = 'internal';
                    const connectorType = 'component';
                    result.push(new TopologyOutgoingNode(id, type, connectorType, route.id || '', title, filename, deadLetterChannel, deadLetterUri));
                }
            }
        } catch (e) {
            console.error(e);
        }
        return result;
    }

    static findTopologyRouteConfigurationOutgoingNodes = (integrations: Integration[]): TopologyOutgoingNode[] => {
        const result: TopologyOutgoingNode[] = [];
        integrations.forEach(i => {
            try {
                const filename = i.metadata.name;
                const rcs = i.spec.flows?.filter(flow => flow.dslName === 'RouteConfigurationDefinition');
                rcs?.forEach((rc: RouteConfigurationDefinition) => {
                    const children: CamelElement[] = [];
                    children.push(...rc.intercept || []);
                    children.push(...rc.interceptFrom || []);
                    children.push(...rc.interceptSendToEndpoint || []);
                    children.push(...rc.onCompletion || []);
                    children.push(...rc.onException || []);
                    children.forEach(child => {
                        const elements = TopologyUtils.findOutgoingInStep(child, []);
                        elements.forEach((e: any) => {
                            const id = 'outgoing-' + rc.id + '-' + e.id;
                            const title = CamelDisplayUtil.getStepDescription(e);
                            const type = TopologyUtils.isElementInternalComponent(e) ? 'internal' : 'external';
                            const connectorType = TopologyUtils.getConnectorType(e);
                            const uniqueUri = TopologyUtils.getUniqueUri(e);
                            result.push(new TopologyOutgoingNode(id, type, connectorType, rc.id || 'undefined', title, filename, e, uniqueUri));
                        });
                    });
                    if (rc.errorHandler?.deadLetterChannel) {
                        const e = rc.errorHandler?.deadLetterChannel;
                        const id = 'outgoing-' + rc.id + '-' + e.id;
                        const title = CamelDisplayUtil.getStepDescription(e);
                        const comp = e?.deadLetterUri?.split(':')?.[0];
                        const type = INTERNAL_COMPONENTS.includes(comp) ? 'internal' : 'external';
                        const connectorType = 'component';
                        const uniqueUri = e?.deadLetterUri;
                        result.push(new TopologyOutgoingNode(id, type, connectorType, rc.id || 'undefined', title, filename, e, uniqueUri));
                    }
                });
            } catch (e) {
                console.error(e);
            }
        });
        return result;
    };

    static findOutgoingInStep = (step: CamelElement, result: CamelElement[]): CamelElement[] => {
        if (step !== undefined) {
            const el = (step as any);
            try {
                if (outgoingDefinitions.includes(el.dslName)) {
                    result.push(step);
                } else {
                    const childElements = CamelDefinitionApiExt.getElementChildrenDefinition(el.dslName);
                    childElements.forEach(child => {
                        if (child.multiple) {
                            const sub = (el[child.name] as CamelElement[]);
                            TopologyUtils.findOutgoingInSteps(sub, result);
                        } else {
                            const sub = (el[child.name] as CamelElement);
                            TopologyUtils.findOutgoingInStep(sub, result);
                        }
                    });
                }
            } catch (e) {
                console.error(e);
            }
        }
        return result;
    };

    static findOutgoingInSteps = (steps: CamelElement[], result: CamelElement[]): CamelElement[] => {
        if (steps !== undefined && steps.length > 0) {
            steps.forEach(step => TopologyUtils.findOutgoingInStep(step, result));
        }
        return result;
    };

    static getNodeIdByUriAndName(tins: TopologyIncomingNode[], uri: string, name: string): string | undefined {
        if (uri && name) {
            const node = tins
                .filter(r => r.from.uri === uri
                    && (r?.from?.parameters?.name === name || r?.from?.parameters?.address === name),
                ).at(0);
            if (node) {
                return node.id;
            }
        }
    }

    static getNodeIdByUri(tins: TopologyIncomingNode[], uri: string): string | undefined {
        const parts = uri.split(':');
        if (parts.length > 1) {
            return TopologyUtils.getNodeIdByUriAndName(tins, parts[0], parts[1]);
        }
    }

    static getRouteIdByUriAndName(tins: TopologyIncomingNode[], uri: string, name: string): string | undefined {
        if (uri && name) {
            const node = tins
                .filter(r => r.from.uri === uri
                    && (r?.from?.parameters?.name === name || r?.from?.parameters?.address === name),
                ).at(0);
            if (node) {
                return 'route-' + node.routeId;
            }
        }
    }

    static getNodeIdByUniqueUri(tins: TopologyIncomingNode[], uniqueUri: string): string [] {
        const result: string[] = [];
        tins.filter(r => r.uniqueUri === uniqueUri)
            ?.forEach(node => result.push(node.id));
        return result;
    }

    static getRouteIdByUri(tins: TopologyIncomingNode[], uri: string): string | undefined {
        const parts = uri.split(':');
        if (parts.length > 1) {
            return TopologyUtils.getRouteIdByUriAndName(tins, parts[0], parts[1]);
        }
    }
}
