/* eslint-disable no-param-reassign */
import { AmfMixin, AmfNamespace, AmfDefinitions } from '@api-client/core/build/browser.js';
import { expandKey } from '@api-client/core/build/src/amf/AmfMixin.js';
import { AmfTypes } from '../../../src/index.js';

/**
 * A demo class that pretends to be a server for a partial model.
 * Instead, it takes the AMF graph model and on demand splits it into 
 * parts, like the AMF service do.
 */
export class AmfPartialModel extends AmfMixin(Object) {
  context?: Record<string, string>;

  _amfChanged(amf: AmfDefinitions.IAmfDocument): void {
    this.context = amf['@context'];
    super._amfChanged(amf);
  }

  /**
   * @param types The list of graph object types. When not defined it returns an empty array.
   * @returns The expanded types.
   */
  readTypes(types: string[]): string[] {
    let target = types;
    if (typeof target === 'string') {
      target = [target];
    }
    if (!Array.isArray(target)) {
      return [];
    }
    return target.map(this[expandKey].bind(this));
  }

  /**
   * Gathers information about the loaded document.
   * This is mainly used by the `api-documentation` element to decide which documentation to render.
   */
  documentMeta(): AmfTypes.DocumentMeta {
    const summary = this.summaryPartial();
    const result: AmfTypes.DocumentMeta = ({
      isApi: false,
      isAsync: false,
      isFragment: false,
      isLibrary: false,
      types: summary && this.readTypes(summary['@type']) || [],
      encodesId: undefined,
    });
    if (!summary) {
      return result;
    }
    const api = this._computeApi(summary, summary['@context']);
    const isApi = !!api;
    result.isApi = isApi;
    if (isApi) {
      result.isAsync = this._isAsyncAPI(summary);
    } else if (result.types[0] === AmfNamespace.aml.vocabularies.document.Module) {
      result.isLibrary = true;
    } else {
      const fragmentTypes = [
        AmfNamespace.aml.vocabularies.security.SecuritySchemeFragment,
        AmfNamespace.aml.vocabularies.apiContract.UserDocumentationFragment,
        AmfNamespace.aml.vocabularies.shapes.DataTypeFragment,
      ];
      result.isFragment = fragmentTypes.some(type => result.types.includes(type));
    }
    return result;
  }

  summaryPartial(): AmfDefinitions.IAmfDocument | null {
    const { amf } = this;
    if (!amf) {
      return null;
    }
    const result = {};
    result['@id'] = amf['@id'];
    result['@type'] = amf['@type'];
    if (amf['@context']) {
      result['@context'] = amf['@context'];
    }
    const declaresKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.declares);
    const declares = this.summaryDeclares(amf[declaresKey]);
    if (declares) {
      result[declaresKey] = declares;
    }
    const encodesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.encodes);
    const encodes = this.summaryEncodes(amf[encodesKey]);
    if (encodes) {
      result[encodesKey] = encodes;
    }
    const referencesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.references);
    const references = this.summaryReferences(amf[referencesKey]);
    if (references) {
      result[referencesKey] = references;
    }
    return JSON.parse(JSON.stringify(result));
  }

  summaryDeclares(declares: AmfDefinitions.IAmfDomainElement[]): AmfDefinitions.IAmfDomainElement[] | null {
    if (!Array.isArray(declares) || !declares.length) {
      return null;
    }
    const closedKey = this._getAmfKey(AmfNamespace.w3.shacl.closed);
    const nameKey = this._getAmfKey(AmfNamespace.w3.shacl.name);
    const coreNameKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.name);
    const descKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.description);
    const examplesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.examples);
    const dataNodeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.dataNode);
    const variableKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.variable);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const securitySchemeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.security.SecurityScheme);
    const result: AmfDefinitions.IAmfDomainElement[] = [];
    const allowedKeys = ['@id', '@type', closedKey, nameKey, coreNameKey, descKey];
    declares.forEach((item) => {
      if (item['@type'].includes(securitySchemeKey)) {
        result.push(this.summarySecurity(item));
        return;
      }
      const cp: AmfDefinitions.IAmfDomainElement = {
        "@id": '',
        "@type": [],
      };
      allowedKeys.forEach((key) => {
        if (key in item) {
          cp[key] = item[key];
        }
      });
      const sm = this.sourceMap(item[smKey]);
      if (sm) {
        cp[smKey] = sm;
      }
      if (examplesKey in item) {
        cp[examplesKey] = item[examplesKey].map(example => ({ '@id': example['@id'] }));
      }
      if (dataNodeKey in item) {
        cp[dataNodeKey] = item[dataNodeKey].map(dn => ({ '@id': dn['@id'] }));
      }
      if (variableKey in item) {
        cp[variableKey] = item[variableKey].map(variable => variable['@value']);
      }
      result.push(cp);
    });
    return result;
  }

  summaryEncodes(encodes: AmfDefinitions.IAmfDomainElement[]): AmfDefinitions.IAmfApi[] | null {
    if (!Array.isArray(encodes) || !encodes.length) {
      return null;
    }
    const result: AmfDefinitions.IAmfApi[] = [];
    const acceptsKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.accepts);
    const contentTypeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.contentType);
    const mediaTypeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.mediaType);
    const schemeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.scheme);
    const serverKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.server);
    const endpointKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.endpoint);
    const descKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.description);
    const nameKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.name);
    const versionKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.version);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const allowedKeys = ['@id', '@type', acceptsKey, contentTypeKey, schemeKey, descKey, nameKey, versionKey, mediaTypeKey];

    encodes.forEach((item) => {
      const cp: AmfDefinitions.IAmfApi = {
        '@id': '',
        "@type": [],
      };
      allowedKeys.forEach((key) => {
        if (key in item) {
          cp[key] = item[key];
        }
      });
      const sm = this.sourceMap(item[smKey]);
      if (sm) {
        cp[smKey] = sm;
      }
      if (serverKey in item) {
        cp[serverKey] = item[serverKey].map((server) => {
          const srv = { ...server };
          const srvSm = this.sourceMap(srv[smKey]);
          if (srvSm) {
            srv[smKey] = srvSm;
          }
          return srv;
        });
      }
      if (Array.isArray(item[endpointKey])) {
        cp[endpointKey] = item[endpointKey].map(endpoint => this.summaryEndpoint(endpoint));
      }
      result.push(cp);
    });
    return result;
  }

  summaryReferences(references: AmfDefinitions.IAmfDomainElement[]): AmfDefinitions.IAmfDomainElement[] | null {
    if (!Array.isArray(references) || !references.length) {
      return null;
    }
    const result: AmfDefinitions.IAmfDomainElement[] = [];
    const rootKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.root);
    const usageKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.usage);
    const versionKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.version);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const allowedKeys = ['@id', '@type', rootKey, usageKey, versionKey];

    const encodesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.encodes);
    const declaresKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.declares);
    const referencesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.references);

    references.forEach((item) => {
      const unit: AmfDefinitions.IAmfDomainElement = {
        "@id": '',
        "@type": [],
      };
      allowedKeys.forEach((key) => {
        if (key in item) {
          unit[key] = item[key];
        }
      });
      const sm = this.sourceMap(item[smKey]);
      if (sm) {
        unit[smKey] = sm;
      }
      const declares = this.summaryDeclares(item[declaresKey]);
      if (declares) {
        unit[declaresKey] = declares;
      }
      const encodes = this.summaryEncodes(item[encodesKey]);
      if (encodes) {
        unit[encodesKey] = encodes;
      }
      const refs = this.summaryReferences(item[referencesKey]);
      if (refs) {
        result[referencesKey] = refs;
      }
      result.push(unit);
    });
    return result;
  }

  summaryEndpoint(endpoint: AmfDefinitions.IAmfEndPoint): AmfDefinitions.IAmfEndPoint {
    const pathKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.path);
    const operationsKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.supportedOperation);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const allowedKeys = ['@id', '@type', pathKey];
    const result: AmfDefinitions.IAmfEndPoint = ({ '@id': '', '@type': [] });
    allowedKeys.forEach((key) => {
      if (key in endpoint) {
        result[key] = endpoint[key];
      }
    });
    const sm = this.sourceMap(endpoint[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    if (Array.isArray(endpoint[operationsKey])) {
      result[operationsKey] = endpoint[operationsKey].map(op => this.summaryOperation(op));
    }
    return result;
  }

  summaryOperation(operation: AmfDefinitions.IAmfOperation): AmfDefinitions.IAmfOperation {
    const methodKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.method);
    const expectsKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.expects);
    const securityKey = this._getAmfKey(AmfNamespace.aml.vocabularies.security.security);
    const descKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.description);
    const nameKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.name);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const allowedKeys = ['@id', '@type', methodKey, nameKey, descKey];
    const result: AmfDefinitions.IAmfOperation = {
      '@id': '',
      '@type': [],
      "http://a.ml/vocabularies/apiContract#method": [],
    };
    allowedKeys.forEach((key) => {
      if (key in operation) {
        result[key] = operation[key];
      }
    });
    const sm = this.sourceMap(operation[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    if (Array.isArray(operation[expectsKey])) {
      result[expectsKey] = operation[expectsKey].map(i => ({ '@id': i['@id'] }));
    }
    if (Array.isArray(operation[securityKey])) {
      result[securityKey] = operation[securityKey].map(i => ({ '@id': i['@id'] }));
    }
    return result;
  }

  sourceMap(items: AmfDefinitions.IAmfDomainElement[]): AmfDefinitions.IAmfDocumentSourceMaps[] | null {
    if (!Array.isArray(items) || !items.length) {
      return null;
    }
    const result: AmfDefinitions.IAmfDocumentSourceMaps[] = [];
    const lexicalKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.lexical);
    const declaredElementKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.declaredElement);
    const elementKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.element);
    const trackedElementKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.trackedElement);
    const valueKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.value);
    const synthesizedFieldKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.synthesizedField);
    const allowedKeys = ['@id', lexicalKey, declaredElementKey, elementKey, trackedElementKey, valueKey, synthesizedFieldKey];
    items.forEach((item) => {
      const cp: AmfDefinitions.IAmfDocumentSourceMaps = {
        "@id": '',
        "@type": [],
      };
      allowedKeys.forEach((key) => {
        if (key in item) {
          if (key === lexicalKey) {
            cp[key] = item[key].map((field) => this._getValue(field, valueKey));
            return;
          }
          if (key === declaredElementKey) {
            cp[key] = item[key].map((field) => this._getValue(field, elementKey));
            return;
          }
          if (key === trackedElementKey) {
            cp[key] = item[key].map((field) => this._getValue(field, valueKey));
            return;
          }
          cp[key] = item[key];
        }
      });
      result.push(cp);
    });
    return result;
  }

  summarySecurity(security: AmfDefinitions.IAmfDomainElement): AmfDefinitions.IAmfDomainElement {
    const coreNameKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.name);
    const coreDisplayNameKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.displayName);
    const descKey = this._getAmfKey(AmfNamespace.aml.vocabularies.core.description);
    const typeKey = this._getAmfKey(AmfNamespace.aml.vocabularies.security.type);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const allowedKeys = ['@id', '@type', coreNameKey, coreDisplayNameKey, descKey, typeKey];

    const result: AmfDefinitions.IAmfDomainElement = {
      "@id": '',
      "@type": [],
    }
    Object.keys(security).forEach((key) => {
      if (allowedKeys.includes(key)) {
        result[key] = security[key];
        return;
      }
      if (key === smKey) {
        result[smKey] = this.sourceMap(security[key]);
        return;
      }
      if (Array.isArray(security[key])) {
        const values: string[] = [];
        security[key].forEach((value) => {
          if (typeof value === 'object' && value['@id']) {
            values.push(value['@id'] as string);
          }
        });
        if (values.length) {
          result[key] = values;
        }
      }
    });

    return result;
  }

  /**
   * @returns Model definition for an endpoint.
   */
  endpoint(domainId: string): AmfDefinitions.IAmfEndPoint | undefined {
    const { context } = this;
    const webApi = this._computeApi(this.amf, context);
    const ep = this._computeEndpointModel(webApi, domainId, context);
    if (!ep || !webApi) {
      return undefined;
    }
    const result: AmfDefinitions.IAmfEndPoint = { ...ep };
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources, context);
    const operationsKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.supportedOperation, context);
    const sm = this.sourceMap(result[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    const srvKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.server, context);
    const servers = webApi[srvKey];
    if (!Array.isArray(result[srvKey]) || !result[srvKey].length && servers) {
      result[srvKey] = [...servers];
    }
    if (Array.isArray(result[operationsKey])) {
      result[operationsKey] = result[operationsKey].map((op) => {
        const item = { ...op };
        const opSm = this.sourceMap(item[smKey]);
        if (opSm) {
          item[smKey] = opSm;
        }
        return item;
      });
    }
    if (context) {
      result['@context'] = context;
    }
    return JSON.parse(JSON.stringify(result));
  }

  /**
   * @returns Model definition for a type.
   */
  schema(domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const { context } = this;
    const schema = this.computeTypeApiModel(this.amf as AmfDefinitions.IAmfDocument, domainId);
    if (!schema) {
      return undefined;
    }
    const result = { ...schema };
    const propertyKey = this._getAmfKey(AmfNamespace.w3.shacl.property);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const sm = this.sourceMap(result[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    if (Array.isArray(result[propertyKey])) {
      result[propertyKey] = result[propertyKey].map(r => this.summaryProperty(r));
    }
    if (context) {
      result['@context'] = context;
    }
    return JSON.parse(JSON.stringify(result));
  }

  securityRequirement(domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const { context } = this;
    const schema = this.computeSecurityApiModel(this.amf as AmfDefinitions.IAmfDocument, domainId);
    if (!schema) {
      return undefined;
    }
    const result = { ...schema };
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const sm = this.sourceMap(result[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    if (context) {
      result['@context'] = context;
    }
    return JSON.parse(JSON.stringify(result));
  }


  summaryProperty(property: AmfDefinitions.IAmfDomainElement): AmfDefinitions.IAmfDomainElement {
    const result = { ...property };
    const rangeKey = this._getAmfKey(AmfNamespace.w3.shacl.property);
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const sm = this.sourceMap(result[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    if (Array.isArray(property[rangeKey])) {
      result[rangeKey] = property[rangeKey].map(r => this.summaryRange(r));
    }
    return result;
  }

  summaryRange(property: AmfDefinitions.IAmfDomainElement): AmfDefinitions.IAmfDomainElement {
    const result = { ...property };
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources);
    const sm = this.sourceMap(result[smKey]);
    if (sm) {
      result[smKey] = sm;
    }
    return result;
  }

  /**
   * Computes type definition model from web API and current selection.
   * It looks for the definition in both `declares` and `references` properties.
   * Returned value is already resolved AMF model (references are resolved).
   *
   * @param model WebApi AMF model. Do not use an array here.
   * @param domainId Currently selected `@id`.
   * @returns Model definition for a type.
   */
  computeTypeApiModel(model: AmfDefinitions.IAmfDocument, domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const declares = this._computeDeclares(model);
    const references = this._computeReferences(model);
    return this._computeType(declares, references, domainId);
  }

  /**
   * Computes security scheme definition model from web API and current selection.
   * It looks for the definition in both `declares` and `references` properties.
   * Returned value is already resolved AMF model (references are resolved).
   *
   * @param model WebApi AMF model. Do not use an array here.
   * @param domainId Currently selected `@id`.
   * @returns Model definition for the security scheme.
   */
  computeSecurityApiModel(model: AmfDefinitions.IAmfDocument, domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const declares = this._computeDeclares(model);
    if (declares) {
      const result = declares.find((item) => item['@id'] === domainId);
      if (result) {
        return this._resolve(result);
      }
    }
    const references = this._computeReferences(model);
    if (Array.isArray(references) && references.length) {
      for (const reference of references) {
        if (this._hasType(reference, AmfNamespace.aml.vocabularies.document.Module)) {
          const result = this.computeReferenceSecurity(reference, domainId);
          if (result) {
            return this._resolve(result);
          }
        }
      }
    }
    return undefined;
  }

  /**
   * Computes a security model from a reference (library for example).
   * @param reference AMF model for a reference to extract the data from
   * @param domainId Node ID to look for
   * @returns Type definition or undefined if not found.
   */
  computeReferenceSecurity(reference: AmfDefinitions.IAmfDocument, domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const declare = this._computeDeclares(reference);
    if (!declare) {
      return undefined;
    }
    let result = declare.find((item) => {
      if (Array.isArray(item)) {
        [item] = item;
      }
      return item['@id'] === domainId;
    });
    if (Array.isArray(result)) {
      [result] = result;
    }
    return this._resolve(result);
  }

  /**
   * @returns Model definition for a type.
   */
  partialOperationEndpoint(domainId: string): AmfDefinitions.IAmfDomainElement | undefined {
    const { context } = this;
    const api = this._computeApi(this.amf, context);
    const endpoint = this._computeMethodEndpoint(api, domainId, context);
    if (!endpoint) {
      return undefined;
    }
    return this.endpoint(endpoint['@id']);
  }

  /**
   * Reads the documentation object from the store.
   * @param id The domain id of the documentation object
   * @returns The read documentation.
   */
  partialDocumentation(id: string): AmfDefinitions.IAmfCreativeWork | undefined {
    const { context } = this;
    const { amf } = this;
    if (!amf) {
      return undefined;
    }
    const api = this._computeApi(amf, context);
    if (!api) {
      return undefined;
    }
    const creative = this._computeDocument(api, id, context);
    if (!creative) {
      throw new Error(`Documentation ${id} does not exist.`);
    }
    if (context) {
      creative['@context'] = context;
    }
    return creative;
  }

  /**
   * Reads the SecurityScheme object from the graph.
   * @param id The domain id of the SecurityScheme
   */
  partialSecurityScheme(id: string): AmfDefinitions.IAmfSecurityScheme {
    const object = this.findSecurityScheme(id);
    if (!object) {
      throw new Error(`No SecurityRequirement for ${id}`);
    }
    const { context } = this;
    if (context) {
      object['@context'] = context;
    }
    return object;
  }
}
