/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed 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 packageJson from '../../../../package.json' with { type: 'json' };
import V1_SYSTEM_MODELS from './v1/V1_DSL_DataSpace_SystemModels.json' with { type: 'json' };
import {
  V1_DataSpace,
  V1_DataSpaceDiagram,
  V1_DataSpaceElementPointer,
  V1_DataSpaceExecutionContext,
  V1_DataSpacePackageableElementExecutable,
  V1_DataSpaceSupportCombinedInfo,
  V1_DataSpaceSupportEmail,
  V1_DataSpaceTemplateExecutable,
} from './v1/model/packageableElements/dataSpace/V1_DSL_DataSpace_DataSpace.js';
import {
  type PlainObject,
  guaranteeNonEmptyString,
  guaranteeNonNullable,
  UnsupportedOperationError,
  isNonNullable,
  assertType,
  guaranteeType,
  returnUndefOnError,
} from '@finos/legend-shared';
import { deserialize } from 'serializr';
import {
  V1_DATA_SPACE_ELEMENT_PROTOCOL_TYPE,
  V1_deserializeDataSpace,
  V1_MAPPING_INCLUDE_DATASPACE_TYPE,
  V1_serializeDataSpace,
  V1_serializeMappingInclude,
  V1_deserializeMappingInclude,
} from './v1/transformation/pureProtocol/V1_DSL_DataSpace_ProtocolHelper.js';
import { V1_resolveDataSpace } from './v1/transformation/pureGraph/V1_DSL_DataSpace_GraphBuilderHelper.js';
import { getOwnDataSpace } from '../../DSL_DataSpace_GraphManagerHelper.js';
import {
  type DataSpaceElement,
  DataSpace,
  DataSpaceExecutionContext,
  DataSpaceSupportCombinedInfo,
  DataSpaceSupportEmail,
  DataSpaceDiagram,
  DataSpaceElementPointer,
  DataSpaceExecutableTemplate,
  DataSpacePackageableElementExecutable,
} from '../../../graph/metamodel/pure/model/packageableElements/dataSpace/DSL_DataSpace_DataSpace.js';
import {
  type PackageableElement,
  type V1_ElementProtocolClassifierPathGetter,
  type V1_ElementProtocolDeserializer,
  type V1_ElementProtocolSerializer,
  type V1_ElementTransformer,
  type V1_GraphBuilderContext,
  type V1_GraphTransformerContext,
  type V1_PackageableElement,
  type V1_PureModelContextData,
  type V1_TaggedValue,
  type Mapping,
  type MappingInclude,
  type PackageableElementReference,
  type V1_MappingInclude,
  type DSL_Mapping_PureProtocolProcessorPlugin_Extension,
  type V1_MappingIncludeBuilder,
  type V1_MappingIncludeTransformer,
  type V1_MappingIncludeProtocolSerializer,
  type V1_MappingIncludeProtocolDeserializer,
  type V1_MappingIncludeIdentifierBuilder,
  type QueryExecutionContextInfo,
  type V1_SavedQueryExecutionBuilder,
  type V1_ElementPointerType,
  type V1_RawLambda,
  V1_taggedValueModelSchema,
  PackageableElementExplicitReference,
  V1_PackageableElementPointer,
  PackageableElementPointerType,
  V1_buildTaggedValue,
  V1_transformStereotype,
  V1_transformTaggedValue,
  PureProtocolProcessorPlugin,
  V1_ElementBuilder,
  V1_initPackageableElement,
  V1_buildFullPath,
  Class,
  Enumeration,
  Association,
  Package,
  V1_RawValueSpecificationTransformer,
  V1_buildRawLambdaWithResolvedPaths,
  V1_buildEmbeddedData,
  V1_transformEmbeddedData,
  DataElementReference,
  V1_DataElementReference,
  QueryDataSpaceExecutionContextInfo,
  generateFunctionPrettyName,
  ConcreteFunctionDefinition,
} from '@finos/legend-graph';
import { V1_resolveDiagram } from '@finos/legend-extension-dsl-diagram/graph';
import { V1_MappingIncludeDataSpace } from './v1/model/packageableElements/mapping/V1_DSL_DataSpace_MappingIncludeDataSpace.js';
import { MappingIncludeDataSpace } from '../../../graph/metamodel/pure/model/packageableElements/mapping/DSL_DataSpace_MappingIncludeDataSpace.js';
import type { Entity } from '@finos/legend-storage';

export const DATA_SPACE_ELEMENT_CLASSIFIER_PATH =
  'meta::pure::metamodel::dataSpace::DataSpace';

export const DATA_SPACE_ELEMENT_POINTER = 'DATASPACE';

export class DSL_DataSpace_PureProtocolProcessorPlugin
  extends PureProtocolProcessorPlugin
  implements DSL_Mapping_PureProtocolProcessorPlugin_Extension
{
  constructor() {
    super(
      packageJson.extensions.pureProtocolProcessorPlugin,
      packageJson.version,
    );
  }

  override V1_getExtraElementBuilders(): V1_ElementBuilder<V1_PackageableElement>[] {
    return [
      new V1_ElementBuilder<V1_DataSpace>({
        elementClassName: 'DataSpace',
        _class: V1_DataSpace,
        firstPass: (
          elementProtocol: V1_PackageableElement,
          context: V1_GraphBuilderContext,
        ): PackageableElement => {
          assertType(elementProtocol, V1_DataSpace);
          const element = new DataSpace(elementProtocol.name);
          const path = V1_buildFullPath(
            elementProtocol.package,
            elementProtocol.name,
          );
          element.executionContexts = guaranteeNonNullable(
            elementProtocol.executionContexts,
            `Data product 'executionContexts' field is missing`,
          ).map((contextProtocol) => {
            const execContext = new DataSpaceExecutionContext();
            execContext.name = guaranteeNonEmptyString(
              contextProtocol.name,
              `Data product execution context 'name' field is missing or empty`,
            );
            execContext.title = contextProtocol.title;
            execContext.description = contextProtocol.description;
            execContext.mapping = PackageableElementExplicitReference.create(
              context.graph.getMapping(contextProtocol.mapping.path),
            );
            execContext.defaultRuntime =
              PackageableElementExplicitReference.create(
                context.graph.getRuntime(contextProtocol.defaultRuntime.path),
              );
            execContext.testData = contextProtocol.testData
              ? guaranteeType(
                  V1_buildEmbeddedData(contextProtocol.testData, context),
                  DataElementReference,
                )
              : undefined;
            return execContext;
          });
          element.defaultExecutionContext = guaranteeNonNullable(
            element.executionContexts.find(
              (execContext) =>
                execContext.name ===
                guaranteeNonEmptyString(
                  elementProtocol.defaultExecutionContext,
                  `Data product 'defaultExecutionContext' field is missing or empty`,
                ),
            ),
            `Can't find default execution context '${elementProtocol.defaultExecutionContext}'`,
          );
          context.currentSubGraph.setOwnElementInExtension(
            path,
            element,
            DataSpace,
          );
          return element;
        },
        secondPass: (
          elementProtocol: V1_PackageableElement,
          context: V1_GraphBuilderContext,
        ): void => {
          assertType(elementProtocol, V1_DataSpace);
          const path = V1_buildFullPath(
            elementProtocol.package,
            elementProtocol.name,
          );
          const element = getOwnDataSpace(path, context.currentSubGraph);
          element.stereotypes = elementProtocol.stereotypes
            .map((stereotype) => context.resolveStereotype(stereotype))
            .filter(isNonNullable);
          element.taggedValues = elementProtocol.taggedValues
            .map((taggedValue) => V1_buildTaggedValue(taggedValue, context))
            .filter(isNonNullable);
          element.title = elementProtocol.title;
          element.description = elementProtocol.description;
          if (elementProtocol.elements) {
            element.elements = elementProtocol.elements.map((pointer) => {
              const elementReference = context.resolveElement(
                pointer.path,
                true,
              );
              if (
                elementReference.value instanceof Package ||
                elementReference.value instanceof Class ||
                elementReference.value instanceof Enumeration ||
                elementReference.value instanceof Association
              ) {
                const elementPointer = new DataSpaceElementPointer();
                elementPointer.element =
                  elementReference as unknown as PackageableElementReference<DataSpaceElement>;
                elementPointer.exclude = pointer.exclude;
                return elementPointer;
              }
              throw new UnsupportedOperationError(
                `Can't find data product element (only allow packages, classes, enumerations, and associations) '${pointer.path}'`,
              );
            });
          }
          if (elementProtocol.executables) {
            element.executables = elementProtocol.executables.map(
              (executableProtocol) => {
                if (
                  executableProtocol instanceof V1_DataSpaceTemplateExecutable
                ) {
                  const executable = new DataSpaceExecutableTemplate();
                  if (executableProtocol.id) {
                    executable.id = executableProtocol.id;
                  }
                  executable.title = executableProtocol.title;
                  executable.description = executableProtocol.description;
                  executable.query = V1_buildRawLambdaWithResolvedPaths(
                    executableProtocol.query.parameters,
                    executableProtocol.query.body,
                    context,
                  );
                  if (executableProtocol.executionContextKey) {
                    executable.executionContextKey =
                      executableProtocol.executionContextKey;
                  }
                  return executable;
                } else if (
                  executableProtocol instanceof
                  V1_DataSpacePackageableElementExecutable
                ) {
                  const executable =
                    new DataSpacePackageableElementExecutable();
                  if (executableProtocol.id) {
                    executable.id = executableProtocol.id;
                  }
                  executable.title = executableProtocol.title;
                  executable.description = executableProtocol.description;
                  if (executableProtocol.executionContextKey) {
                    executable.executionContextKey =
                      executableProtocol.executionContextKey;
                  }
                  try {
                    executable.executable = context.resolveElement(
                      executableProtocol.executable.path,
                      false,
                    );
                  } catch {
                    try {
                      executable.executable =
                        PackageableElementExplicitReference.create(
                          guaranteeNonNullable(
                            context.graph.functions.find(
                              (fn) =>
                                generateFunctionPrettyName(fn, {
                                  fullPath: true,
                                  spacing: false,
                                  notIncludeParamName: true,
                                }) ===
                                executableProtocol.executable.path.replaceAll(
                                  /\s*/gu,
                                  '',
                                ),
                            ),
                          ),
                        );
                    } catch {
                      throw new UnsupportedOperationError(
                        `Can't analyze data product executable with element in path: ${executableProtocol.executable.path}`,
                        executableProtocol,
                      );
                    }
                  }
                  return executable;
                } else {
                  throw new UnsupportedOperationError(
                    `Can't build data product executable`,
                    executableProtocol,
                  );
                }
              },
            );
          }
          if (elementProtocol.diagrams) {
            element.diagrams = elementProtocol.diagrams.map(
              (diagramProtocol) => {
                const diagram = new DataSpaceDiagram();
                diagram.title = diagramProtocol.title;
                diagram.description = diagramProtocol.description;
                diagram.diagram = V1_resolveDiagram(
                  diagramProtocol.diagram.path,
                  context,
                );
                return diagram;
              },
            );
          }
          if (elementProtocol.supportInfo) {
            if (
              elementProtocol.supportInfo instanceof V1_DataSpaceSupportEmail
            ) {
              const supportEmail = new DataSpaceSupportEmail();
              supportEmail.documentationUrl =
                elementProtocol.supportInfo.documentationUrl;
              supportEmail.address = guaranteeNonEmptyString(
                elementProtocol.supportInfo.address,
                `Data product support email 'address' field is missing or empty`,
              );
              element.supportInfo = supportEmail;
            } else if (
              elementProtocol.supportInfo instanceof
              V1_DataSpaceSupportCombinedInfo
            ) {
              const combinedInfo = new DataSpaceSupportCombinedInfo();
              combinedInfo.documentationUrl =
                elementProtocol.supportInfo.documentationUrl;
              combinedInfo.emails = elementProtocol.supportInfo.emails;
              combinedInfo.website = elementProtocol.supportInfo.website;
              combinedInfo.faqUrl = elementProtocol.supportInfo.faqUrl;
              combinedInfo.supportUrl = elementProtocol.supportInfo.supportUrl;
              element.supportInfo = combinedInfo;
            } else {
              throw new UnsupportedOperationError(
                `Can't build data product support info`,
                elementProtocol.supportInfo,
              );
            }
          }
        },
      }),
    ];
  }

  override V1_getExtraSystemModels(): PlainObject<V1_PureModelContextData>[] {
    return [V1_SYSTEM_MODELS];
  }

  override V1_getExtraElementClassifierPathGetters(): V1_ElementProtocolClassifierPathGetter[] {
    return [
      (elementProtocol: V1_PackageableElement): string | undefined => {
        if (elementProtocol instanceof V1_DataSpace) {
          return DATA_SPACE_ELEMENT_CLASSIFIER_PATH;
        }
        return undefined;
      },
    ];
  }

  override V1_getExtraElementPointerTypes(): V1_ElementPointerType[] {
    return [
      (elementProtocol: PackageableElement): string | undefined => {
        if (elementProtocol instanceof DataSpace) {
          return DATA_SPACE_ELEMENT_POINTER;
        }
        return undefined;
      },
    ];
  }

  override V1_getExtraElementProtocolSerializers(): V1_ElementProtocolSerializer[] {
    return [
      (
        elementProtocol: V1_PackageableElement,
        plugins: PureProtocolProcessorPlugin[],
      ): PlainObject<V1_PackageableElement> | undefined => {
        if (elementProtocol instanceof V1_DataSpace) {
          return V1_serializeDataSpace(elementProtocol);
        }
        return undefined;
      },
    ];
  }

  override V1_getExtraElementProtocolDeserializers(): V1_ElementProtocolDeserializer[] {
    return [
      (
        json: PlainObject<V1_PackageableElement>,
        plugins: PureProtocolProcessorPlugin[],
      ): V1_PackageableElement | undefined => {
        if (json._type === V1_DATA_SPACE_ELEMENT_PROTOCOL_TYPE) {
          return V1_deserializeDataSpace(json);
        }
        return undefined;
      },
    ];
  }

  override V1_getExtraElementTransformers(): V1_ElementTransformer[] {
    return [
      (
        metamodel: PackageableElement,
        context: V1_GraphTransformerContext,
      ): V1_PackageableElement | undefined => {
        if (metamodel instanceof DataSpace) {
          const protocol = new V1_DataSpace();
          V1_initPackageableElement(protocol, metamodel);
          protocol.stereotypes = metamodel.stereotypes.map(
            V1_transformStereotype,
          );
          protocol.taggedValues = metamodel.taggedValues.map(
            V1_transformTaggedValue,
          );
          protocol.executionContexts = metamodel.executionContexts.map(
            (execContext) => {
              const contextProtocol = new V1_DataSpaceExecutionContext();
              contextProtocol.name = execContext.name;
              contextProtocol.title = execContext.title;
              contextProtocol.description = execContext.description;
              contextProtocol.mapping = new V1_PackageableElementPointer(
                PackageableElementPointerType.MAPPING,
                execContext.mapping.valueForSerialization ?? '',
              );
              contextProtocol.defaultRuntime = new V1_PackageableElementPointer(
                PackageableElementPointerType.RUNTIME,
                execContext.defaultRuntime.valueForSerialization ?? '',
              );
              contextProtocol.testData = execContext.testData
                ? guaranteeType(
                    V1_transformEmbeddedData(execContext.testData, context),
                    V1_DataElementReference,
                  )
                : undefined;
              return contextProtocol;
            },
          );
          protocol.defaultExecutionContext =
            metamodel.defaultExecutionContext.name;
          protocol.title = metamodel.title;
          protocol.description = metamodel.description;
          protocol.elements = metamodel.elements?.map((pointer) => {
            const elementPointer = new V1_DataSpaceElementPointer();
            elementPointer.exclude = pointer.exclude;
            elementPointer.path = pointer.element.valueForSerialization ?? '';
            return elementPointer;
          });
          protocol.executables = metamodel.executables?.map((executable) => {
            if (executable instanceof DataSpaceExecutableTemplate) {
              const executableProtocol = new V1_DataSpaceTemplateExecutable();
              if (executable.id) {
                executableProtocol.id = executable.id;
              }
              executableProtocol.title = executable.title;
              executableProtocol.description = executable.description;
              if (executable.executionContextKey) {
                executableProtocol.executionContextKey =
                  executable.executionContextKey;
              }
              executableProtocol.query =
                executable.query.accept_RawValueSpecificationVisitor(
                  new V1_RawValueSpecificationTransformer(context),
                ) as V1_RawLambda;
              return executableProtocol;
            } else if (
              executable instanceof DataSpacePackageableElementExecutable
            ) {
              const executableProtocol =
                new V1_DataSpacePackageableElementExecutable();
              if (executable.id) {
                executableProtocol.id = executable.id;
              }
              executableProtocol.title = executable.title;
              executableProtocol.description = executable.description;
              if (executable.executionContextKey) {
                executableProtocol.executionContextKey =
                  executable.executionContextKey;
              }
              if (
                executable.executable.value instanceof
                ConcreteFunctionDefinition
              ) {
                executableProtocol.executable =
                  new V1_PackageableElementPointer(
                    PackageableElementPointerType.FUNCTION,
                    generateFunctionPrettyName(executable.executable.value, {
                      fullPath: true,
                      spacing: false,
                      notIncludeParamName: true,
                    }),
                  );
              } else {
                executableProtocol.executable =
                  new V1_PackageableElementPointer(
                    undefined,
                    executable.executable.valueForSerialization ?? '',
                  );
              }
              return executableProtocol;
            } else {
              throw new UnsupportedOperationError(
                `Can't transform data product executable`,
                executable,
              );
            }
          });
          protocol.diagrams = metamodel.diagrams?.map((diagram) => {
            const diagramProtocol = new V1_DataSpaceDiagram();
            diagramProtocol.title = diagram.title;
            diagramProtocol.description = diagram.description;
            diagramProtocol.diagram = new V1_PackageableElementPointer(
              undefined,
              diagram.diagram.valueForSerialization ?? '',
            );
            return diagramProtocol;
          });
          if (metamodel.supportInfo) {
            if (metamodel.supportInfo instanceof DataSpaceSupportEmail) {
              const supportEmail = new V1_DataSpaceSupportEmail();
              supportEmail.documentationUrl =
                metamodel.supportInfo.documentationUrl;
              supportEmail.address = metamodel.supportInfo.address;
              protocol.supportInfo = supportEmail;
            } else if (
              metamodel.supportInfo instanceof DataSpaceSupportCombinedInfo
            ) {
              const combinedInfo = new V1_DataSpaceSupportCombinedInfo();
              combinedInfo.documentationUrl =
                metamodel.supportInfo.documentationUrl;
              combinedInfo.emails = metamodel.supportInfo.emails;
              combinedInfo.website = metamodel.supportInfo.website;
              combinedInfo.faqUrl = metamodel.supportInfo.faqUrl;
              combinedInfo.supportUrl = metamodel.supportInfo.supportUrl;
              protocol.supportInfo = combinedInfo;
            } else {
              throw new UnsupportedOperationError(
                `Can't transform data product support info`,
                metamodel.supportInfo,
              );
            }
          }
          return protocol;
        }
        return undefined;
      },
    ];
  }

  V1_getExtraMappingIncludeBuilders(): V1_MappingIncludeBuilder[] {
    return [
      (
        protocol: V1_MappingInclude,
        parentMapping: Mapping,
        context: V1_GraphBuilderContext,
      ): MappingInclude | undefined => {
        if (protocol instanceof V1_MappingIncludeDataSpace) {
          const dataSpace = V1_resolveDataSpace(
            protocol.includedDataSpace,
            context,
          );
          const includedMapping = new MappingIncludeDataSpace(
            parentMapping,
            dataSpace.value.defaultExecutionContext.mapping,
            dataSpace,
          );
          return includedMapping;
        }
        return undefined;
      },
    ];
  }

  V1_getExtraMappingIncludeTransformers(): V1_MappingIncludeTransformer[] {
    return [
      (
        metamodel: MappingInclude,
        context: V1_GraphTransformerContext,
      ): V1_MappingInclude | undefined => {
        if (metamodel instanceof MappingIncludeDataSpace) {
          const mappingInclude = new V1_MappingIncludeDataSpace();
          mappingInclude.includedDataSpace =
            metamodel.includedDataSpace.valueForSerialization ?? '';
          return mappingInclude;
        }
        return undefined;
      },
    ];
  }

  V1_getExtraMappingIncludeProtocolSerializers(): V1_MappingIncludeProtocolSerializer[] {
    return [
      (
        protocol: V1_MappingInclude,
      ): PlainObject<V1_MappingInclude> | undefined => {
        if (protocol instanceof V1_MappingIncludeDataSpace) {
          return V1_serializeMappingInclude(protocol);
        }
        return undefined;
      },
    ];
  }

  V1_getExtraMappingIncludeProtocolDeserializers(): V1_MappingIncludeProtocolDeserializer[] {
    return [
      (json: PlainObject<V1_MappingInclude>): V1_MappingInclude | undefined => {
        if (json._type === V1_MAPPING_INCLUDE_DATASPACE_TYPE) {
          return V1_deserializeMappingInclude(json);
        }
        return undefined;
      },
    ];
  }

  V1_getExtraMappingIncludeIdentifierBuilders(): V1_MappingIncludeIdentifierBuilder[] {
    return [
      (protocol: V1_MappingInclude): string | undefined => {
        if (protocol instanceof V1_MappingIncludeDataSpace) {
          return protocol.includedDataSpace;
        }
        return undefined;
      },
    ];
  }

  override V1_getExtraSavedQueryExecutionBuilder(): V1_SavedQueryExecutionBuilder[] {
    return [
      (
        queryExec: QueryExecutionContextInfo,
        entites: Entity[],
      ): { mapping: string; runtime: string } | undefined => {
        if (queryExec instanceof QueryDataSpaceExecutionContextInfo) {
          const dataSpace = queryExec.dataSpacePath;
          const dataSpaceEntity = entites.find((e) => e.path === dataSpace);
          if (dataSpaceEntity) {
            const content = dataSpaceEntity.content;
            if (content._type === V1_DATA_SPACE_ELEMENT_PROTOCOL_TYPE) {
              const v1DataSpace = returnUndefOnError(() =>
                V1_deserializeDataSpace(content),
              );
              if (v1DataSpace) {
                if (v1DataSpace.executionContexts.length === 1) {
                  const exec = guaranteeNonNullable(
                    v1DataSpace.executionContexts[0],
                  );
                  return {
                    mapping: exec.mapping.path,
                    runtime: exec.defaultRuntime.path,
                  };
                }
                const resvoled =
                  queryExec.executionKey ?? v1DataSpace.defaultExecutionContext;
                const resolvedExec = guaranteeNonNullable(
                  v1DataSpace.executionContexts.find(
                    (e) => e.name === resvoled,
                  ),
                );
                return {
                  mapping: resolvedExec.mapping.path,
                  runtime: resolvedExec.defaultRuntime.path,
                };
              }
            }
          }
        }
        return undefined;
      },
    ];
  }
}

export const extractDataSpaceTaxonomyNodes = (
  json: PlainObject<V1_DataSpace>,
): string[] => {
  const ENTERPRISE_PROFILE_PATH = `meta::pure::profiles::enterprise`;
  const TAXONOMY_NODES_TAG = `taxonomyNodes`;
  const TAXONOMY_NODES_TAG_VALUE_DELIMITER = `,`;

  const taxonomyNodes = new Set<string>();
  if (json._type === V1_DATA_SPACE_ELEMENT_PROTOCOL_TYPE) {
    if (Array.isArray(json.taggedValues)) {
      const taggedValues = (
        json.taggedValues as PlainObject<V1_TaggedValue>[]
      ).map((taggedValueJson) =>
        deserialize(V1_taggedValueModelSchema, taggedValueJson),
      );
      taggedValues
        .filter(
          (taggedValue) =>
            taggedValue.tag.profile === ENTERPRISE_PROFILE_PATH &&
            taggedValue.tag.value === TAXONOMY_NODES_TAG,
        )
        .forEach((taggedValue) => {
          taggedValue.value
            .split(TAXONOMY_NODES_TAG_VALUE_DELIMITER)
            .map((value) => value.trim())
            .filter((value) => Boolean(value))
            .forEach((value) => taxonomyNodes.add(value));
        });
    }
  }
  return Array.from(taxonomyNodes.values());
};
