import { AmfMixin, type ServersQueryOptions } from '@api-client/core/amf/AmfMixin.js'
import { AmfPartialModel } from './AmfPartialModel.js'
import { AmfSerializer } from '@api-client/core/amf/AmfSerializer.js'
import type { IAmfDocument } from '@api-client/core/amf/definitions/Amf.js'
import type {
  IApiDocumentation,
  IApiEndPoint,
  IApiEndPointWithOperationsListItem,
  IApiOperation,
  IApiSecurityScheme,
  IApiServer,
  IApiSummary,
} from '@api-client/core/amf/definitions/Api.js'
import type { IShapeUnion } from '@api-client/core/amf/definitions/Shapes.js'
import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js'
import { AmfStoreDomEventsMixin } from '../../../src/amf/store/mixins/AmfStoreDomEventsMixin.js'
import { AmfStore } from '../../../src/amf/store/AmfStore.js'
import { DocumentMeta } from '../../../src/amf/types.js'

class AmfHelper extends AmfMixin(Object) {}

/**
 * This is a demo class that represents how a store can be implemented that serves the AMF model
 * in parts from a backend.
 *
 * In here we use `AmfPartialModel` class but in a real-life scenario this class could be a connector
 * to an HTTP service.
 */
export class AmfPartialGraphStore extends AmfStoreDomEventsMixin(AmfStore) {
  /**
   * The API serializer
   */
  override serializer: AmfSerializer

  partial: AmfPartialModel

  cache: Record<string, unknown>

  /**
   * @param eventsTarget The event target to dispatch the events on.
   * @param graph The full API model.
   */
  constructor(eventsTarget: EventTarget = window, graph?: IAmfDocument) {
    super()
    this.target = eventsTarget
    let amf = graph
    if (Array.isArray(graph)) {
      ;[amf] = graph
    }

    this.serializer = new AmfSerializer(amf)
    this.partial = new AmfPartialModel(amf)

    this.cache = {}
  }

  get amf(): IAmfDocument | undefined {
    return this.partial.amf
  }

  set amf(graph) {
    let amf = graph
    if (Array.isArray(graph)) {
      ;[amf] = graph
    }
    this.cache = {}
    this.partial.amf = amf
  }

  /**
   * Gathers information about the loaded document.
   * This is mainly used by the `api-documentation` element to decide which documentation to render.
   */
  override async documentMeta(): Promise<DocumentMeta> {
    if (this.cache.documentMeta) {
      return this.cache.documentMeta as DocumentMeta
    }
    const meta = this.partial.documentMeta()
    this.cache.documentMeta = meta
    return meta
  }

  /**
   * @returns The API base definition
   */
  override async apiSummary(): Promise<IApiSummary | undefined> {
    if (this.cache.summary) {
      return this.cache.summary as IApiSummary
    }
    const model = this.partial.summaryPartial()
    if (!model) {
      return undefined
    }
    const helper = new AmfHelper()
    helper.amf = model
    const wa = helper._computeApi(model)
    if (!wa) {
      return undefined
    }
    const result = this.serializer.apiSummary(wa)
    this.cache.summary = result
    return result
  }

  /**
   * Reads an endpoint by its id.
   * @param id The domain id of the endpoint.
   */
  override async getEndpoint(id: string): Promise<IApiEndPoint | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as IApiEndPoint
    }
    const model = this.partial.endpoint(id)
    if (!model) {
      return undefined
    }
    const result = this.serializer.endPoint(model, model['@context'])
    this.cache[id] = result
    result.operations.forEach((op) => {
      this.cache[op.id] = op
    })
    return result
  }

  /**
   * Reads the operation model.
   * @param operationId The domain id of the operation to read.
   * @param endpointId Optional endpoint id. When not set it searches through all endpoints.
   */
  override async getOperation(operationId: string, endpointId?: string): Promise<IApiOperation | undefined> {
    if (this.cache[operationId]) {
      return this.cache[operationId] as IApiOperation
    }
    if (!endpointId) {
      return undefined
    }
    const model = this.partial.endpoint(endpointId)
    if (!model) {
      return undefined
    }
    const endpoint = this.serializer.endPoint(model, model['@context'])
    const op = endpoint.operations.find((i) => i.id === operationId)
    this.cache[operationId] = op
    return op
  }

  /**
   * Finds an endpoint that has the operation.
   * @param id Method name or the domain id of the operation to find
   */
  override async getOperationParent(id: string): Promise<IApiEndPoint | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as IApiEndPoint
    }
    const model = this.partial.partialOperationEndpoint(id)
    if (!model) {
      return undefined
    }
    const result = this.serializer.endPoint(model, model['@context'])
    this.cache[id] = result
    return result
  }

  /**
   * @param domainId The domain id of the API type (schema).
   */
  override async getType(domainId: string): Promise<IShapeUnion | undefined> {
    if (this.cache[domainId]) {
      return this.cache[domainId] as IShapeUnion
    }
    const model = this.partial.schema(domainId)
    if (!model) {
      return undefined
    }
    const result = this.serializer.unknownShape(model, undefined, model['@context'])
    this.cache[domainId] = result
    return result
  }

  /**
   * Queries for the list of servers for method, if defined, or endpoint, if defined, or root level
   * @param query Server query options
   * @returns The list of servers for given query.
   */
  override async queryServers(query: ServersQueryOptions = {}): Promise<IApiServer[]> {
    const key = `queryServers${JSON.stringify(query)}`
    if (this.cache[key]) {
      return this.cache[key] as IApiServer[]
    }
    const model = this.partial.summaryPartial()
    if (!model) {
      return []
    }
    const helper = new AmfHelper()
    helper.amf = model
    const servers = helper._getServers(query, model['@context'])
    if (!Array.isArray(servers)) {
      return []
    }
    const result = servers.map((s) => this.serializer.server(s, model['@context']))
    this.cache[key] = result
    return result
  }

  /**
   * @returns Currently loaded API's protocols
   */
  override async apiProtocols(): Promise<string[] | undefined> {
    if (this.cache.protocols) {
      return this.cache.protocols as string[]
    }
    const model = this.partial.summaryPartial()
    if (!model) {
      return undefined
    }
    const helper = new AmfHelper()
    helper.amf = model
    const wa = helper._computeApi(model, model['@context'])
    if (!wa) {
      return []
    }
    const protocols = helper._getValueArray(wa, AmfNamespace.aml.vocabularies.apiContract.scheme) as string[]
    this.cache.protocols = protocols
    return protocols
  }

  /**
   * @returns Currently loaded API's version
   */
  override async apiVersion(): Promise<string | undefined> {
    if (this.cache.version) {
      return this.cache.version as string
    }
    const model = this.partial.summaryPartial()
    if (!model) {
      return undefined
    }
    const helper = new AmfHelper()
    helper.amf = model
    const version = helper._computeApiVersion(model, model['@context'])
    this.cache.version = version
    return version
  }

  /**
   * Lists all endpoints with operations included into the result.
   */
  override async listEndpointsWithOperations(): Promise<IApiEndPointWithOperationsListItem[]> {
    if (this.cache.endpointsListWithOperation) {
      return this.cache.endpointsListWithOperation as IApiEndPointWithOperationsListItem[]
    }
    const model = this.partial.summaryPartial()
    if (!model) {
      return []
    }
    const helper = new AmfHelper()
    helper.amf = model
    const api = helper._computeApi(model, model['@context'])
    if (!api) {
      return []
    }
    const endpoints = helper._computeEndpoints(api, model['@context'])
    if (!Array.isArray(endpoints) || !endpoints.length) {
      return []
    }
    const result = endpoints.map((ep) => this.serializer.endPointWithOperationsListItem(ep, model['@context']))
    this.cache.endpointsListWithOperation = result
    return result
  }

  /**
   * Reads the documentation object from the store.
   * @param id The domain id of the documentation object
   * @returns The read documentation.
   */
  override async getDocumentation(id: string): Promise<IApiDocumentation | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as IApiDocumentation
    }
    const creative = this.partial.partialDocumentation(id)
    if (!creative) {
      return undefined
    }
    const result = this.serializer.documentation(creative, creative['@context'])
    this.cache[id] = result
    return result
  }

  /**
   * Reads the SecurityScheme object from the graph.
   * @param id The domain id of the SecurityScheme
   */
  override async getSecurityScheme(id: string): Promise<IApiSecurityScheme | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as IApiSecurityScheme
    }
    const model = this.partial.partialSecurityScheme(id)
    if (!model) {
      throw new Error(`No SecurityRequirement for ${id}`)
    }
    const result = this.serializer.securityScheme(model, model['@context'])
    this.cache[id] = result
    return result
  }
}
