import { AmfNamespace } from '@api-client/core/amf/definitions/Namespace.js'
import type { IApiParameter } from '@api-client/core/amf/definitions/Api.js'
import type {
  IApiArrayShape,
  IApiDataNodeUnion,
  IApiObjectNode,
  IApiScalarNode,
  IApiScalarShape,
  IApiShape,
  IShapeUnion,
} from '@api-client/core/amf/definitions/Shapes.js'

/**
 * Computes a data model for custom definition for the OAuth 2 scheme
 * According to the annotation published at
 * https://github.com/raml-org/raml-annotations/tree/master/annotations/security-schemes
 */
export class Oauth2RamlCustomData {
  readParams(properties: Record<string, IApiDataNodeUnion>): IApiParameter[] {
    const result: IApiParameter[] = []
    Object.keys(properties).forEach((key) => {
      const definition = properties[key]
      if (definition.types.includes(AmfNamespace.aml.vocabularies.data.Object)) {
        const property = this.getProperty(definition as IApiObjectNode)
        result.push(property)
      } else if (definition.types.includes(AmfNamespace.aml.vocabularies.data.Scalar)) {
        const property = this.getPropertyScalar(definition as IApiScalarNode)
        result.push(property)
      }
    })
    return result
  }

  /**
   * Creates an IApiParameter for an annotation that has properties.
   * This expects the properties to be defined like RAML's type definition.
   */
  getProperty(definition: IApiObjectNode): IApiParameter {
    const { properties = {}, id, name } = definition
    const result: IApiParameter = {
      id,
      name,
      examples: [],
      payloads: [],
      types: [AmfNamespace.aml.vocabularies.apiContract.Parameter],
      customDomainProperties: [],
    }
    const schema = this.readSchema(definition)
    if (schema) {
      result.schema = schema
    }
    if (properties.required) {
      const req = properties.required as IApiScalarNode
      result.required = req.value === 'true'
    }
    return result
  }

  /**
   * Creates an IApiParameter for an annotation that has no properties but rather a simplified
   * notation of `propertyName: dataType`.
   */
  getPropertyScalar(definition: IApiScalarNode): IApiParameter {
    const { dataType = '', id, name } = definition
    const result: IApiParameter = {
      id,
      name,
      examples: [],
      payloads: [],
      types: [AmfNamespace.aml.vocabularies.apiContract.Parameter],
      customDomainProperties: [],
    }
    const schema = this.createSchema() as IApiScalarShape
    schema.types = [AmfNamespace.aml.vocabularies.shapes.ScalarShape]
    schema.id = id
    schema.name = name
    schema.dataType = this.typeToSchemaType(dataType)
    result.schema = schema
    return result
  }

  readSchema(property: IApiObjectNode): IShapeUnion | undefined {
    const { properties = {}, name, id } = property
    // const { example, examples, } = properties;
    const isArray = this.readIsSchemaArray(properties.type as IApiScalarNode, properties.items as IApiScalarNode)
    const type = this.readSchemaType(properties.type as IApiScalarNode, properties.items as IApiScalarNode)
    let schema: IShapeUnion | undefined
    if (isArray) {
      const s = this.createSchema() as IApiArrayShape
      s.types = [AmfNamespace.aml.vocabularies.shapes.ArrayShape]
      s.id = id
      s.name = name
      s.items = this.createTypedSchema(type, property)
      schema = s
    } else {
      schema = this.createTypedSchema(type, property)
    }
    return schema
  }

  createTypedSchema(type: string, object: IApiObjectNode): IShapeUnion | undefined {
    switch (type) {
      case 'string':
      case 'number':
      case 'integer':
      case 'float':
      case 'double':
      case 'long':
      case 'date-only':
      case 'date-time':
      case 'time-only':
      case 'nil':
      case 'date':
      case 'boolean':
        return this.createScalarSchema(object, type)
      default:
        return undefined
    }
  }

  createSchema(): IApiShape {
    return {
      id: '',
      types: [],
      and: [],
      name: '',
      inherits: [],
      or: [],
      values: [],
      xone: [],
      customDomainProperties: [],
    }
  }

  createScalarSchema(object: IApiObjectNode, type: string): IApiScalarShape {
    const { properties = {}, name, id } = object
    const schema = this.createSchema() as IApiScalarShape
    schema.types = [AmfNamespace.aml.vocabularies.shapes.ScalarShape]
    schema.id = id
    schema.name = name
    schema.dataType = this.typeToSchemaType(type)
    if (properties.format) {
      const item = properties.format as IApiScalarNode
      schema.format = item.value
    }
    if (properties.default) {
      const item = properties.default as IApiScalarNode
      schema.defaultValueStr = item.value
      // schema.defaultValue = item.value;
    }
    if (properties.description) {
      const item = properties.description as IApiScalarNode
      schema.description = item.value
    }
    if (properties.displayName) {
      const item = properties.displayName as IApiScalarNode
      schema.displayName = item.value
    }
    if (properties.pattern) {
      const item = properties.pattern as IApiScalarNode
      schema.pattern = item.value
    }
    if (properties.maximum) {
      const item = properties.maximum as IApiScalarNode
      const value = Number(item.value)
      if (!Number.isNaN(value)) {
        schema.maximum = value
      }
    }
    if (properties.minimum) {
      const item = properties.minimum as IApiScalarNode
      const value = Number(item.value)
      if (!Number.isNaN(value)) {
        schema.minimum = value
      }
    }
    if (properties.multipleOf) {
      const item = properties.multipleOf as IApiScalarNode
      const value = Number(item.value)
      if (!Number.isNaN(value)) {
        schema.multipleOf = value
      }
    }
    if (properties.maxLength) {
      const item = properties.maxLength as IApiScalarNode
      const value = Number(item.value)
      if (!Number.isNaN(value)) {
        schema.maxLength = value
      }
    }
    if (properties.minLength) {
      const item = properties.minLength as IApiScalarNode
      const value = Number(item.value)
      if (!Number.isNaN(value)) {
        schema.minLength = value
      }
    }
    return schema
  }

  /**
   * @param object
   * @param items The definition of the `items` property that corresponds to RAML's items property of an array
   */
  readSchemaType(object: IApiScalarNode, items?: IApiScalarNode): string {
    if (object.dataType !== AmfNamespace.w3.xmlSchema.string) {
      return AmfNamespace.w3.xmlSchema.string
    }
    let inputType = object.value || ''
    if (inputType.endsWith('[]')) {
      inputType = inputType.replace('[]', '')
    }
    if (inputType === 'array' && items) {
      return this.readSchemaType(items)
    }
    return inputType || 'string'
  }

  typeToSchemaType(type: string): string {
    switch (type) {
      case 'boolean':
        return AmfNamespace.w3.xmlSchema.boolean
      case 'number':
        return AmfNamespace.w3.xmlSchema.number
      case 'integer':
        return AmfNamespace.w3.xmlSchema.integer
      case 'float':
        return AmfNamespace.w3.xmlSchema.float
      case 'double':
        return AmfNamespace.w3.xmlSchema.double
      case 'long':
        return AmfNamespace.w3.xmlSchema.long
      case 'date-only':
        return AmfNamespace.w3.xmlSchema.date
      case 'date-time':
        return AmfNamespace.w3.xmlSchema.dateTime
      case 'time-only':
        return AmfNamespace.w3.xmlSchema.time
      case 'nil':
        return AmfNamespace.w3.xmlSchema.nil
      default:
        return AmfNamespace.w3.xmlSchema.string
    }
  }

  /**
   * Checks whether the custom property represents an array.
   * @param type
   * @param items The definition of the `items` property that corresponds to RAML's items property of an array
   * @returns True when the schema is an array.
   */
  readIsSchemaArray(type: IApiScalarNode, items: IApiScalarNode): boolean {
    if (!type && items) {
      return true
    }
    if (!type) {
      return false
    }
    const inputType = type.value || ''
    if (inputType.endsWith('[]') || inputType === 'array') {
      return true
    }
    return false
  }
}
