/* eslint-disable @typescript-eslint/ban-ts-comment */
import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js'
import { AmfMixin } from '@api-client/core/amf/AmfMixin.js'
import type {
  IAmfApi,
  IAmfCreativeWork,
  IAmfDocument,
  IAmfDocumentSourceMaps,
  IAmfDomainElement,
  IAmfEndPoint,
  IAmfOperation,
  IAmfSecurityScheme,
} from '@api-client/core/amf/definitions/Amf.js'
import { DocumentMeta } from '../../../src/amf/types.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>

  override _amfChanged(amf: 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 []
    }
    const fn = this._expandKey.bind(this) as (value: string) => string
    return target.map(fn)
  }

  /**
   * Gathers information about the loaded document.
   * This is mainly used by the `api-documentation` element to decide which documentation to render.
   */
  documentMeta(): DocumentMeta {
    const summary = this.summaryPartial()
    const result: 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(): IAmfDocument | null {
    const { amf } = this
    if (!amf) {
      return null
    }
    const result = {} as IAmfDocument
    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 as keyof IAmfDocument] as IAmfDomainElement[])
    if (declares) {
      // @ts-ignore
      result[declaresKey as keyof IAmfDocument] = declares
    }
    const encodesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.encodes)
    // @ts-ignore
    const encodes = this.summaryEncodes(amf[encodesKey])
    if (encodes) {
      // @ts-ignore
      result[encodesKey] = encodes
    }
    const referencesKey = this._getAmfKey(AmfNamespace.aml.vocabularies.document.references)
    // @ts-ignore
    const references = this.summaryReferences(amf[referencesKey])
    if (references) {
      // @ts-ignore
      result[referencesKey] = references
    }
    return JSON.parse(JSON.stringify(result))
  }

  summaryDeclares(declares: IAmfDomainElement[]): 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: 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: IAmfDomainElement = {
        '@id': '',
        '@type': [],
      }
      allowedKeys.forEach((key) => {
        if (key in item) {
          // @ts-ignore
          cp[key] = item[key]
        }
      })
      // @ts-ignore
      const sm = this.sourceMap(item[smKey])
      if (sm) {
        // @ts-ignore
        cp[smKey] = sm
      }
      if (examplesKey in item) {
        // @ts-ignore
        cp[examplesKey] = item[examplesKey].map((example) => ({ '@id': example['@id'] }))
      }
      if (dataNodeKey in item) {
        // @ts-ignore
        cp[dataNodeKey] = item[dataNodeKey].map((dn) => ({ '@id': dn['@id'] }))
      }
      if (variableKey in item) {
        // @ts-ignore
        cp[variableKey] = item[variableKey].map((variable) => variable['@value'])
      }
      result.push(cp)
    })
    return result
  }

  summaryEncodes(encodes: IAmfDomainElement[]): IAmfApi[] | null {
    if (!Array.isArray(encodes) || !encodes.length) {
      return null
    }
    const result: 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: IAmfApi = {
        '@id': '',
        '@type': [],
      }
      allowedKeys.forEach((key) => {
        if (key in item) {
          // @ts-ignore
          cp[key] = item[key]
        }
      })
      // @ts-ignore
      const sm = this.sourceMap(item[smKey])
      if (sm) {
        // @ts-ignore
        cp[smKey] = sm
      }
      if (serverKey in item) {
        // @ts-ignore
        cp[serverKey] = item[serverKey].map((server) => {
          const srv = { ...server }
          const srvSm = this.sourceMap(srv[smKey])
          if (srvSm) {
            srv[smKey] = srvSm
          }
          return srv
        })
      }
      // @ts-ignore
      if (Array.isArray(item[endpointKey])) {
        // @ts-ignore
        cp[endpointKey] = item[endpointKey].map((endpoint) => this.summaryEndpoint(endpoint))
      }
      result.push(cp)
    })
    return result
  }

  summaryReferences(references: IAmfDomainElement[]): IAmfDomainElement[] | null {
    if (!Array.isArray(references) || !references.length) {
      return null
    }
    const result: 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: IAmfDomainElement = {
        '@id': '',
        '@type': [],
      }
      allowedKeys.forEach((key) => {
        if (key in item) {
          // @ts-ignore
          unit[key] = item[key]
        }
      })
      // @ts-ignore
      const sm = this.sourceMap(item[smKey])
      if (sm) {
        // @ts-ignore
        unit[smKey] = sm
      }
      // @ts-ignore
      const declares = this.summaryDeclares(item[declaresKey])
      if (declares) {
        // @ts-ignore
        unit[declaresKey] = declares
      }
      // @ts-ignore
      const encodes = this.summaryEncodes(item[encodesKey])
      if (encodes) {
        // @ts-ignore
        unit[encodesKey] = encodes
      }
      // @ts-ignore
      const refs = this.summaryReferences(item[referencesKey])
      if (refs) {
        // @ts-ignore
        result[referencesKey] = refs
      }
      result.push(unit)
    })
    return result
  }

  summaryEndpoint(endpoint: IAmfEndPoint): 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: IAmfEndPoint = { '@id': '', '@type': [] }
    allowedKeys.forEach((key) => {
      if (key in endpoint) {
        // @ts-ignore
        result[key] = endpoint[key]
      }
    })
    // @ts-ignore
    const sm = this.sourceMap(endpoint[smKey])
    if (sm) {
      // @ts-ignore
      result[smKey] = sm
    }
    // @ts-ignore
    if (Array.isArray(endpoint[operationsKey])) {
      // @ts-ignore
      result[operationsKey] = endpoint[operationsKey].map((op) => this.summaryOperation(op))
    }
    return result
  }

  summaryOperation(operation: IAmfOperation): 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: IAmfOperation = {
      '@id': '',
      '@type': [],
      'http://a.ml/vocabularies/apiContract#method': [],
    }
    allowedKeys.forEach((key) => {
      if (key in operation) {
        // @ts-ignore
        result[key] = operation[key]
      }
    })
    // @ts-ignore
    const sm = this.sourceMap(operation[smKey])
    if (sm) {
      // @ts-ignore
      result[smKey] = sm
    }
    // @ts-ignore
    if (Array.isArray(operation[expectsKey])) {
      // @ts-ignore
      result[expectsKey] = operation[expectsKey].map((i) => ({ '@id': i['@id'] }))
    }
    // @ts-ignore
    if (Array.isArray(operation[securityKey])) {
      // @ts-ignore
      result[securityKey] = operation[securityKey].map((i) => ({ '@id': i['@id'] }))
    }
    return result
  }

  sourceMap(items: IAmfDomainElement[]): IAmfDocumentSourceMaps[] | null {
    if (!Array.isArray(items) || !items.length) {
      return null
    }
    const result: 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: IAmfDocumentSourceMaps = {
        '@id': '',
        '@type': [],
      }
      allowedKeys.forEach((key) => {
        if (key in item) {
          if (key === lexicalKey) {
            // @ts-ignore
            cp[key] = item[key].map((field) => this._getValue(field, valueKey))
            return
          }
          if (key === declaredElementKey) {
            // @ts-ignore
            cp[key] = item[key].map((field) => this._getValue(field, elementKey))
            return
          }
          if (key === trackedElementKey) {
            // @ts-ignore
            cp[key] = item[key].map((field) => this._getValue(field, valueKey))
            return
          }
          // @ts-ignore
          cp[key] = item[key]
        }
      })
      result.push(cp)
    })
    return result
  }

  summarySecurity(security: IAmfDomainElement): 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: IAmfDomainElement = {
      '@id': '',
      '@type': [],
    }
    Object.keys(security).forEach((key) => {
      if (allowedKeys.includes(key)) {
        // @ts-ignore
        result[key] = security[key]
        return
      }
      if (key === smKey) {
        // @ts-ignore
        result[smKey] = this.sourceMap(security[key])
        return
      }
      // @ts-ignore
      if (Array.isArray(security[key])) {
        const values: string[] = []
        // @ts-ignore
        security[key].forEach((value) => {
          if (typeof value === 'object' && value['@id']) {
            values.push(value['@id'] as string)
          }
        })
        if (values.length) {
          // @ts-ignore
          result[key] = values
        }
      }
    })

    return result
  }

  /**
   * @returns Model definition for an endpoint.
   */
  endpoint(domainId: string): 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: IAmfEndPoint = { ...ep }
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources, context)
    const operationsKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.supportedOperation, context)
    // @ts-ignore
    const sm = this.sourceMap(result[smKey])
    if (sm) {
      // @ts-ignore
      result[smKey] = sm
    }
    const srvKey = this._getAmfKey(AmfNamespace.aml.vocabularies.apiContract.server, context)
    // @ts-ignore
    const servers = webApi[srvKey]
    // @ts-ignore
    if (!Array.isArray(result[srvKey]) || (!result[srvKey].length && servers)) {
      // @ts-ignore
      result[srvKey] = [...servers]
    }
    // @ts-ignore
    if (Array.isArray(result[operationsKey])) {
      // @ts-ignore
      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): IAmfDomainElement | undefined {
    const { context } = this
    const schema = this.computeTypeApiModel(this.amf as 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)
    // @ts-ignore
    const sm = this.sourceMap(result[smKey])
    if (sm) {
      // @ts-ignore
      result[smKey] = sm
    }
    // @ts-ignore
    if (Array.isArray(result[propertyKey])) {
      // @ts-ignore
      result[propertyKey] = result[propertyKey].map((r) => this.summaryProperty(r))
    }
    if (context) {
      result['@context'] = context
    }
    return JSON.parse(JSON.stringify(result))
  }

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

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

  summaryRange(property: IAmfDomainElement): IAmfDomainElement {
    const result = { ...property }
    const smKey = this._getAmfKey(AmfNamespace.aml.vocabularies.docSourceMaps.sources)
    // @ts-ignore
    const sm = this.sourceMap(result[smKey])
    if (sm) {
      // @ts-ignore
      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: IAmfDocument, domainId: string): 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: IAmfDocument, domainId: string): 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: IAmfDocument, domainId: string): 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): 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): 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): 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
  }
}
