import { readFile } from 'fs/promises'
import { AmfMixin } from '../../../src/amf/AmfMixin.js'
import { AmfSerializer } from '../../../src/amf/AmfSerializer.js'
import { AmfNamespace as ns } from '../../../src/amf/definitions/Namespace.js'
import {
  IAmfDocument,
  IAmfCreativeWork,
  IAmfEndPoint,
  IAmfOperation,
  IAmfParameter,
  IAmfPayload,
  IAmfRequest,
  IAmfResponse,
  IAmfSecurityRequirement,
  IAmfSecurityScheme,
  IAmfServer,
  IAmfShape,
  IAmfWebApi,
} from '../../../src/amf/definitions/Amf.js'
import {
  IApiDocumentation,
  IApiEndPoint,
  IApiOperation,
  IApiParameter,
  IApiPayload,
  IApiRequest,
  IApiResponse,
  IApiSecurityScheme,
  IApiServer,
} from '../../../src/amf/definitions/Api.js'
import { IShapeUnion } from '../../../src/amf/definitions/Shapes.js'

export interface EndpointOperation {
  endpoint: IAmfEndPoint
  operation: IAmfOperation
}

export interface GraphLoadOptions {
  noExpand?: boolean
}

export class AmfLoader extends AmfMixin(Object) {
  /**
   * Reads AMF graph model as string
   */
  async getGraph(fileName = 'demo-api', opts: GraphLoadOptions = {}): Promise<IAmfDocument> {
    const file = `${fileName}.json`
    const loc = new URL(`../../../data/models/${file}`, import.meta.url)
    let result = JSON.parse(await readFile(loc, 'utf8'))
    if (Array.isArray(result)) {
      ;[result] = result
    }
    if (opts.noExpand) {
      return result
    }
    let amf = this._expand(result)
    if (Array.isArray(amf)) {
      ;[amf] = amf
    }
    return amf
  }

  lookupEndpoint(model: IAmfDocument, path: string): IAmfEndPoint {
    const amf = this._expand(model)
    this.amf = amf
    const webApi = this._computeApi(amf)
    if (!webApi) {
      throw new Error('This AMF model does not contain API definition.')
    }
    // @ts-expect-error Used in testing
    const endpoints = webApi[this._getAmfKey(ns.aml.vocabularies.apiContract.endpoint)]
    if (!Array.isArray(endpoints) || !endpoints.length) {
      throw new Error('This API does not contain endpoints.')
    }
    const pathKey = ns.aml.vocabularies.apiContract.path
    const ep = endpoints.find((i) => this._getValue(i, pathKey) === path)
    if (!ep) {
      throw new Error(`An endpoint with path ${path} does not exist in this API.`)
    }
    return ep
  }

  getEndpoint(model: IAmfDocument, path: string): IApiEndPoint {
    const amf = this._expand(model)
    const op = this.lookupEndpoint(amf, path)
    if (!op) {
      throw new Error(`Unknown endpoint for path ${path}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.endPoint(op)
  }

  lookupOperation(model: IAmfDocument, endpoint: string, operation: string): IAmfOperation {
    const amf = this._expand(model)
    const endPoint = this.lookupEndpoint(amf, endpoint)
    const opKey = this._getAmfKey(ns.aml.vocabularies.apiContract.supportedOperation)
    // @ts-expect-error Used in testing
    const ops = this._ensureArray(endPoint[opKey]) as IAmfOperation[]
    const result = ops.find(
      (item) => (this._getValue(item, ns.aml.vocabularies.apiContract.method) as string) === operation
    )
    if (!result) {
      throw new Error(`Unknown operation ${operation} for endpoint ${endpoint}`)
    }
    return result
  }

  getOperation(model: IAmfDocument, endpoint: string, operation: string): IApiOperation {
    const amf = this._expand(model)
    const op = this.lookupOperation(amf, endpoint, operation)
    if (!op) {
      throw new Error(`Unknown operation for path ${endpoint} and method ${operation}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.operation(op)
  }

  lookupEndpointOperation(model: IAmfDocument, path: string, operation: string): EndpointOperation {
    const endpoint = this.lookupEndpoint(model, path)
    const opKey = this._getAmfKey(ns.aml.vocabularies.apiContract.supportedOperation)
    // @ts-expect-error Used in testing
    const ops = this._ensureArray(endpoint[opKey]) as IAmfOperation[]
    const op = ops.find((item) => this._getValue(item, ns.aml.vocabularies.apiContract.method) === operation)
    if (!op) {
      throw new Error(`Unknown operation for path ${endpoint} and method ${operation}`)
    }
    return {
      endpoint,
      operation: op,
    }
  }

  lookupExpects(model: IAmfDocument, path: string, operation: string): IAmfRequest {
    const op = this.lookupOperation(model, path, operation)
    if (!op) {
      throw new Error(`Unknown operation for path ${path} and method ${operation}`)
    }
    // @ts-expect-error Used in testing
    let expects = op[this._getAmfKey(ns.aml.vocabularies.apiContract.expects)]
    if (!expects) {
      throw new Error(`IAmfOperation has no "expects" value.`)
    }
    if (Array.isArray(expects)) {
      ;[expects] = expects
    }
    return expects
  }

  lookupPayloads(model: IAmfDocument, endpoint: string, operation: string): IAmfPayload[] {
    const expects = this.lookupExpects(model, endpoint, operation)
    // @ts-expect-error Used in testing
    let payloads = expects[this._getAmfKey(ns.aml.vocabularies.apiContract.payload)] as IAmfPayload[]
    if (payloads && !Array.isArray(payloads)) {
      payloads = [payloads]
    }
    return payloads
  }

  getPayloads(model: IAmfDocument, endpoint: string, operation: string): IApiPayload[] {
    const amf = this._expand(model)

    const payloads = this.lookupPayloads(amf, endpoint, operation)
    if (!payloads) {
      throw new Error(`No payloads for path ${endpoint} and operation ${operation}`)
    }
    const serializer = new AmfSerializer(amf)
    return payloads.map((i) => serializer.payload(i))
  }

  lookupSecurity(model: IAmfDocument, name: string): IAmfSecurityScheme | undefined {
    const amf = this._expand(model)

    this.amf = amf
    const declares = this._computeDeclares(amf) || []
    let security = declares.find((item) => {
      if (Array.isArray(item)) {
        ;[item] = item
      }
      if (this._getValue(item, ns.aml.vocabularies.core.displayName) === name) {
        return true
      }
      if (this._getValue(item, ns.aml.vocabularies.core.name) === name) {
        return true
      }
      return this._getValue(item, ns.aml.vocabularies.security.name) === name
    })
    if (Array.isArray(security)) {
      ;[security] = security
    }
    if (!security) {
      const references = this._computeReferences(amf) || []
      for (let i = 0, len = references.length; i < len; i++) {
        if (!this._hasType(references[i], ns.aml.vocabularies.document.Module)) {
          continue
        }
        security = this.lookupSecurity(references[i], name)
        if (security) {
          break
        }
      }
    }
    return security
  }

  getSecurity(model: IAmfDocument, name: string): IApiSecurityScheme {
    const amf = this._expand(model)
    const security = this.lookupSecurity(amf, name)
    if (!security) {
      throw new Error(`No security named ${name}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.securityScheme(security)
  }

  lookupShape(model: IAmfDocument, name: string): IAmfShape | undefined {
    const amf = this._expand(model)
    this.amf = amf
    const declares = this._computeDeclares(amf) || []
    let shape = declares.find((item) => {
      if (Array.isArray(item)) {
        ;[item] = item
      }
      return this._getValue(item, ns.w3.shacl.name) === name
    })
    if (Array.isArray(shape)) {
      ;[shape] = shape
    }
    if (!shape) {
      const references = this._computeReferences(amf) || []
      for (let i = 0, len = references.length; i < len; i++) {
        if (!this._hasType(references[i], ns.aml.vocabularies.document.Module)) {
          continue
        }
        shape = this.lookupShape(references[i], name)
        if (shape) {
          break
        }
      }
    }
    return shape
  }

  getShape(model: IAmfDocument, name: string): IShapeUnion {
    const amf = this._expand(model)
    const shape = this.lookupShape(amf, name)
    if (!shape) {
      throw new Error(`No API shape named ${name}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.unknownShape(shape)
  }

  lookupDocumentation(model: IAmfDocument, name: string): IAmfCreativeWork | undefined {
    const amf = this._expand(model)
    this.amf = amf
    const webApi = this._computeApi(amf)
    if (!webApi) {
      return undefined
    }
    const key = this._getAmfKey(ns.aml.vocabularies.core.documentation)
    // @ts-expect-error Used in testing
    const docs = this._ensureArray(webApi[key]) as IAmfCreativeWork[]
    return docs.find((item) => {
      if (Array.isArray(item)) {
        ;[item] = item
      }
      return this._getValue(item, ns.aml.vocabularies.core.title) === name
    })
  }

  getDocumentation(model: IAmfDocument, name: string): IApiDocumentation {
    const amf = this._expand(model)
    const shape = this.lookupDocumentation(amf, name)
    if (!shape) {
      throw new Error(`No documentation named ${name}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.documentation(shape)
  }

  lookupEncodes(model: IAmfDocument): IAmfWebApi {
    const amf = this._expand(model)
    this.amf = amf
    const key = this._getAmfKey(ns.aml.vocabularies.document.encodes) as string
    // @ts-expect-error Used in testing
    let result = amf[key]
    if (Array.isArray(result)) {
      ;[result] = result
    }
    return result
  }

  lookupResponses(model: IAmfDocument, endpoint: string, operation: string): IAmfResponse[] {
    const method = this.lookupOperation(model, endpoint, operation)
    // @ts-expect-error Used in testing
    return method[this._getAmfKey(ns.aml.vocabularies.apiContract.returns) as string]
  }

  getResponses(model: IAmfDocument, endpoint: string, operation: string): IApiResponse[] {
    const amf = this._expand(model)
    const responses = this.lookupResponses(amf, endpoint, operation)
    const serializer = new AmfSerializer(amf)
    return responses.map((i) => serializer.response(i))
  }

  lookupResponse(model: IAmfDocument, path: string, operation: string, code: string): IAmfResponse {
    const responses = this.lookupResponses(model, path, operation)
    if (!Array.isArray(responses) || !responses.length) {
      throw new Error(`No responses for path ${path} and operation ${operation}`)
    }
    const response = responses.find((item) => {
      if (this._getValue(item, ns.aml.vocabularies.apiContract.statusCode) === String(code)) {
        return true
      }
      return false
    })
    if (!response) {
      throw new Error(`No responses the status code ${code}`)
    }
    return response
  }

  /**
   * @param code The response's status code
   */
  getResponse(model: IAmfDocument, endpoint: string, operation: string, code: string): IApiResponse {
    const amf = this._expand(model)
    const response = this.lookupResponse(amf, endpoint, operation, code)
    const serializer = new AmfSerializer(amf)
    return serializer.response(response)
  }

  lookupRequest(model: IAmfDocument, endpoint: string, operation: string): IAmfRequest {
    const method = this.lookupOperation(model, endpoint, operation)
    // @ts-expect-error Used in testing
    let requests = method[this._getAmfKey(ns.aml.vocabularies.apiContract.expects)]
    if (Array.isArray(requests)) {
      ;[requests] = requests
    }
    if (!requests) {
      throw new Error(`No request found in operation ${operation} and path ${endpoint}`)
    }
    return requests
  }

  getRequest(model: IAmfDocument, endpoint: string, operation: string): IApiRequest {
    const amf = this._expand(model)
    const request = this.lookupRequest(amf, endpoint, operation)
    if (!request) {
      throw new Error(`No request found in operation ${operation} and path ${endpoint}`)
    }
    const serializer = new AmfSerializer(amf)
    return serializer.request(request)
  }

  /**
   * @param path The endpoint path
   * @param operation The operation path
   * @param code The response's status code
   */
  lookupResponsePayloads(model: IAmfDocument, path: string, operation: string, code: string): IAmfPayload[] {
    const response = this.lookupResponse(model, path, operation, code)
    const pKey = this._getAmfKey(ns.aml.vocabularies.apiContract.payload)
    // @ts-expect-error Used in testing
    const payloads = response[pKey]
    return this._ensureArray(payloads) as IAmfPayload[]
  }

  /**
   * @param path The endpoint path
   * @param operation The operation path
   * @param code The response's status code
   */
  getResponsePayloads(model: IAmfDocument, path: string, operation: string, code: string): IApiPayload[] {
    const amf = this._expand(model)
    const payloads = this.lookupResponsePayloads(amf, path, operation, code)
    const serializer = new AmfSerializer(amf)
    return payloads.map((p) => serializer.payload(p))
  }

  lookupServers(model: IAmfDocument): IAmfServer[] | undefined {
    const amf = this._expand(model)
    this.amf = amf
    const webApi = this._computeApi(amf)
    if (!webApi) {
      return undefined
    }
    const key = this._getAmfKey(ns.aml.vocabularies.apiContract.server)
    // @ts-expect-error Used in testing
    let result = webApi[key]
    if (result && !Array.isArray(result)) {
      result = [result]
    }
    return result
  }

  getServers(model: IAmfDocument): IApiServer[] | undefined {
    const amf = this._expand(model)
    const servers = this.lookupServers(amf)
    if (servers) {
      const serializer = new AmfSerializer(amf)
      return servers.map((s) => serializer.server(s))
    }
    return undefined
  }

  lookupReturns(model: IAmfDocument, path: string, operation: string): IAmfResponse[] {
    const op = this.lookupOperation(model, path, operation)
    if (!op) {
      throw new Error(`Unknown operation for path ${path} and method ${operation}`)
    }
    // @ts-expect-error Used in testing
    let returns = op[this._getAmfKey(ns.aml.vocabularies.apiContract.returns)]
    if (!returns) {
      throw new Error(`IAmfOperation has no "returns" value.`)
    }
    if (!Array.isArray(returns)) {
      returns = [returns]
    }
    return returns
  }

  /**
   * Lookups a shape object from the declares array
   */
  lookupDeclaredShape(model: IAmfDocument, name: string): IAmfShape {
    this.amf = model
    const items = this._computeDeclares(model)
    if (!items) {
      throw new Error(`Unable to find "declares" in the model.`)
    }
    const result = items.find((item) => {
      const typed = item as IAmfShape
      const objectName = this._getValue(typed, ns.w3.shacl.name) as string
      return objectName === name
    })
    if (!result) {
      throw new Error(`Unable to find shape ${name} in the model.`)
    }
    return result
  }

  lookupOperationSecurity(model: IAmfDocument, path: string, operation: string): IAmfSecurityRequirement[] {
    const op = this.lookupOperation(model, path, operation)
    if (!op) {
      throw new Error(`Unknown operation for path ${path} and method ${operation}`)
    }
    // @ts-expect-error Used in testing
    let security = op[this._getAmfKey(ns.aml.vocabularies.security.security) as string]
    if (!security) {
      throw new Error(`IAmfOperation has no "security" value.`)
    }
    if (!Array.isArray(security)) {
      security = [security]
    }
    return security
  }

  lookupRequestPayloads(model: IAmfDocument, path: string, operation: string): IAmfPayload[] {
    const request = this.lookupExpects(model, path, operation)
    // @ts-expect-error Used in testing
    const payload = request[this._getAmfKey(ns.aml.vocabularies.apiContract.payload) as string]
    if (!payload || !payload.length) {
      throw new Error(`IAmfOperation ${operation} of endpoint ${payload} has no request payload.`)
    }
    return payload
  }

  lookupRequestPayload(model: IAmfDocument, path: string, operation: string, mime: string): IAmfPayload {
    const payloads = this.lookupRequestPayloads(model, path, operation)
    const payload = payloads.find((i) => this._getValue(i, ns.aml.vocabularies.core.mediaType) === mime)
    if (!payload) {
      throw new Error(`IAmfOperation ${operation} of endpoint ${payload} has no request payload for ${mime}.`)
    }
    return payload
  }

  /**
   * Reads a request parameter from an operation for: URI, query params, headers, and cookies.
   *
   * @param endpoint The endpoint path
   * @param operation The operation path
   * @param param The param name
   */
  getParameter(model: IAmfDocument, endpoint: string, operation: string, param: string): IApiParameter {
    const amf = this._expand(model)
    const expects = this.lookupExpects(amf, endpoint, operation)
    if (!expects) {
      throw new Error(`The operation ${operation} of endpoint ${endpoint} has no request.`)
    }
    const serializer = new AmfSerializer(amf)
    const request = serializer.request(expects)
    if (!request) {
      throw new Error(`The operation ${operation} of endpoint ${endpoint} has no request.`)
    }
    let pool: IApiParameter[] = []
    if (Array.isArray(request.uriParameters)) {
      pool = pool.concat(request.uriParameters)
    }
    if (Array.isArray(request.cookieParameters)) {
      pool = pool.concat(request.cookieParameters)
    }
    if (Array.isArray(request.queryParameters)) {
      pool = pool.concat(request.queryParameters)
    }
    if (Array.isArray(request.headers)) {
      pool = pool.concat(request.headers)
    }
    const result = pool.find((i) => i.name === param)
    if (!result) {
      throw new Error(`IAmfParameter ${param} not found.`)
    }
    return result
  }

  readHeaders(source: IAmfRequest | IAmfResponse): IAmfParameter[] | undefined {
    const key = this._getAmfKey(ns.aml.vocabularies.apiContract.header)
    // @ts-expect-error Used in testing
    let values = source[key]
    if (values && !Array.isArray(values)) {
      values = [values]
    }
    return values
  }

  /**
   * Computes a list of query parameters
   */
  readQueryParameters(source: IAmfRequest): IAmfParameter[] | undefined {
    const key = this._getAmfKey(ns.aml.vocabularies.apiContract.parameter)
    // @ts-expect-error Used in testing
    let values = source[key]
    if (values && !Array.isArray(values)) {
      values = [values]
    }
    return values
  }
}
