/* eslint-disable default-param-last */
/* eslint-disable max-classes-per-file */
import { AmfDefinitions, AmfMixin, AmfNamespace, AmfSerializer, AmfShapes, ApiDefinitions, ServersQueryOptions } from '@api-client/core/build/browser.js';
import { AmfStore, AmfStoreDomEventsMixin, AmfTypes } from '../../../src/index.js';
import { AmfPartialModel } from './AmfPartialModel.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
   */
  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?: AmfDefinitions.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(): AmfDefinitions.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.
   */
  async documentMeta(): Promise<AmfTypes.DocumentMeta> {
    if (this.cache.documentMeta) {
      return this.cache.documentMeta as AmfTypes.DocumentMeta;
    }
    const meta = this.partial.documentMeta();
    this.cache.documentMeta = meta;
    return meta;
  }

  /**
   * @returns The API base definition 
   */
  async apiSummary(): Promise<ApiDefinitions.IApiSummary | undefined> {
    if (this.cache.summary) {
      return this.cache.summary as ApiDefinitions.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.
   */
  async getEndpoint(id: string): Promise<ApiDefinitions.IApiEndPoint | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as ApiDefinitions.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.
   */
  async getOperation(operationId: string, endpointId?: string): Promise<ApiDefinitions.IApiOperation | undefined> {
    if (this.cache[operationId]) {
      return this.cache[operationId] as ApiDefinitions.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
   */
  async getOperationParent(id: string): Promise<ApiDefinitions.IApiEndPoint | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as ApiDefinitions.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).
   */
  async getType(domainId: string): Promise<AmfShapes.IShapeUnion | undefined> {
    if (this.cache[domainId]) {
      return this.cache[domainId] as AmfShapes.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.
   */
  async queryServers(query: ServersQueryOptions = {}): Promise<ApiDefinitions.IApiServer[]> {
    const key = `queryServers${JSON.stringify(query)}`;
    if (this.cache[key]) {
      return this.cache[key] as ApiDefinitions.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
   */
  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
   */
  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.
   */
  async listEndpointsWithOperations(): Promise<ApiDefinitions.IApiEndPointWithOperationsListItem[]> {
    if (this.cache.endpointsListWithOperation) {
      return this.cache.endpointsListWithOperation as ApiDefinitions.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.
   */
  async getDocumentation(id: string): Promise<ApiDefinitions.IApiDocumentation | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as ApiDefinitions.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
   */
  async getSecurityScheme(id: string): Promise<ApiDefinitions.IApiSecurityScheme | undefined> {
    if (this.cache[id]) {
      return this.cache[id] as ApiDefinitions.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;
  }
}
