/**
 * OpenAPI Body Extractors
 * 
 * Ported from Hoppscotch's openapi/index.ts
 * Handles request body parsing for v2 and v3
 */

/* eslint-disable @typescript-eslint/no-explicit-any */

import type {
  OpenAPI,
  OpenAPIV2,
  OpenAPIV3,
  OpenAPIV3_1 as OpenAPIV31,
} from 'openapi-types'
import type {
  BrainfishRESTReqBody,
  FormDataKeyValue,
} from '../../../types'
import { knownContentTypes, isOpenAPIV3Operation, type OpenAPIOperationType } from './index'

/**
 * Resolve a $ref reference in the schema
 */
function resolveRef(doc: OpenAPI.Document, ref: string): OpenAPIV3.SchemaObject | null {
  if (!ref.startsWith('#/')) return null
  
  try {
    const path = ref.slice(2).split('/')
    let current: any = doc
    
    for (const segment of path) {
      if (current && typeof current === 'object' && segment in current) {
        current = current[segment]
      } else {
        return null
      }
    }
    
    return current as OpenAPIV3.SchemaObject
  } catch {
    return null
  }
}

/**
 * Generate a sample value based on schema type
 */
function generateSampleValue(
  schema: OpenAPIV3.SchemaObject | OpenAPIV31.SchemaObject | OpenAPIV3.ReferenceObject,
  doc: OpenAPI.Document,
  required: boolean = false
): any {
  try {
    // Handle $ref
    if ('$ref' in schema) {
      const resolved = resolveRef(doc, schema.$ref)
      if (resolved) {
        return generateSampleValue(resolved, doc, required)
      }
      return null
    }

    const schemaObj = schema as OpenAPIV3.SchemaObject

    // Use example if provided
    if (schemaObj.example !== undefined) {
      return schemaObj.example
    }

    // Use default if provided
    if (schemaObj.default !== undefined) {
      return schemaObj.default
    }

    // Use enum first value if provided
    if (schemaObj.enum && schemaObj.enum.length > 0) {
      return schemaObj.enum[0]
    }

    // Generate based on type
    switch (schemaObj.type) {
      case 'string':
        if (schemaObj.format === 'date') return '2024-01-01'
        if (schemaObj.format === 'date-time') return '2024-01-01T00:00:00Z'
        if (schemaObj.format === 'email') return 'user@example.com'
        if (schemaObj.format === 'uri' || schemaObj.format === 'url') return 'https://example.com'
        if (schemaObj.format === 'uuid') return '00000000-0000-0000-0000-000000000000'
        return required ? 'string' : ''

      case 'number':
      case 'integer':
        if (schemaObj.minimum !== undefined) return schemaObj.minimum
        return 0

      case 'boolean':
        return false

      case 'array':
        if (schemaObj.items) {
          return [generateSampleValue(schemaObj.items, doc, true)]
        }
        return []

      case 'object':
        return generateObjectFromSchema(schemaObj, doc)

      default:
        // If no type but has properties, treat as object
        if (schemaObj.properties) {
          return generateObjectFromSchema(schemaObj, doc)
        }
        return null
    }
  } catch {
    return null
  }
}

/**
 * Generate a sample object from an OpenAPI schema
 * Required fields are listed first
 */
function generateObjectFromSchema(
  schema: OpenAPIV3.SchemaObject | OpenAPIV31.SchemaObject,
  doc: OpenAPI.Document
): Record<string, any> {
  try {
    const result: Record<string, any> = {}
    const requiredFields = new Set(schema.required || [])
    const properties = schema.properties || {}

    // Sort properties to put required fields first
    const sortedEntries = Object.entries(properties).sort(([keyA], [keyB]) => {
      const aRequired = requiredFields.has(keyA)
      const bRequired = requiredFields.has(keyB)
      if (aRequired && !bRequired) return -1
      if (!aRequired && bRequired) return 1
      return 0
    })

    for (const [key, propSchema] of sortedEntries) {
      const isRequired = requiredFields.has(key)
      const value = generateSampleValue(propSchema as OpenAPIV3.SchemaObject, doc, isRequired)
      if (value !== null) {
        result[key] = value
      }
    }

    return result
  } catch {
    return {}
  }
}

/**
 * Generate example from OpenAPI v3 media object
 */
function generateExampleFromMedia(
  media: OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject,
  doc: OpenAPI.Document
): any {
  try {
    // First check for explicit examples
    if (media.example !== undefined) {
      return media.example
    }

    if (media.examples && Object.keys(media.examples).length > 0) {
      const firstExampleKey = Object.keys(media.examples)[0]
      const firstExample = media.examples[firstExampleKey]
      if ('value' in firstExample) {
        return (firstExample as OpenAPIV3.ExampleObject).value
      }
    }

    // Generate from schema
    const schema = media.schema as OpenAPIV3.SchemaObject | OpenAPIV31.SchemaObject | OpenAPIV3.ReferenceObject | undefined
    if (!schema) return null

    // Handle $ref at schema level
    if ('$ref' in schema) {
      const resolved = resolveRef(doc, schema.$ref)
      if (resolved) {
        if (resolved.type === 'object' || resolved.properties) {
          return generateObjectFromSchema(resolved, doc)
        }
        return generateSampleValue(resolved, doc, true)
      }
      return null
    }

    if (schema.type === 'object' || schema.properties) {
      return generateObjectFromSchema(schema, doc)
    }

    return generateSampleValue(schema, doc, true)
  } catch {
    return null
  }
}

// Legacy function for v2 compatibility
function generateRequestBodyExampleFromOpenAPIV2Body(
  op: OpenAPIV2.OperationObject,
  doc: OpenAPI.Document
): string | null {
  try {
    const bodyParam = (op.parameters ?? []).find(
      (param) => (param as OpenAPIV2.Parameter).in === 'body'
    ) as OpenAPIV2.InBodyParameterObject | undefined

    if (!bodyParam?.schema) return null

    const schema = bodyParam.schema as OpenAPIV3.SchemaObject
    if (schema.type === 'object' || schema.properties) {
      const example = generateObjectFromSchema(schema, doc)
      return JSON.stringify(example, null, 2)
    }

    return null
  } catch {
    return null
  }
}

function generateV3Example(media: OpenAPIV3.MediaTypeObject, doc: OpenAPI.Document): any {
  return generateExampleFromMedia(media, doc)
}

function generateV31Example(media: OpenAPIV31.MediaTypeObject, doc: OpenAPI.Document): any {
  return generateExampleFromMedia(media, doc)
}

/**
 * Parses OpenAPI v2 request body
 */
export function parseOpenAPIV2Body(
  op: OpenAPIV2.OperationObject,
  doc?: OpenAPI.Document
): BrainfishRESTReqBody {
  const obj = (op.consumes ?? [])[0] as string | undefined

  // Not a content-type we support
  if (!obj || !(obj in knownContentTypes)) {
    return { contentType: null, body: null }
  }

  // For form data types, extract form fields
  if (
    obj === 'multipart/form-data' ||
    obj === 'application/x-www-form-urlencoded'
  ) {
    const formDataValues = ((op.parameters ?? []) as OpenAPIV2.Parameter[])
      .filter((param) => param.in === 'formData')
      .map(
        (param) =>
          <FormDataKeyValue>{
            key: param.name,
            isFile: param.type === 'file',
            value: '',
            active: true,
          }
      )

    return obj === 'application/x-www-form-urlencoded'
      ? {
          contentType: obj,
          body: formDataValues.map(({ key }) => `${key}: `).join('\n'),
        }
      : { contentType: obj, body: formDataValues }
  }

  // For other content types (JSON, XML, etc.)
  const bodyParam = (op.parameters ?? []).find(
    (param) => (param as OpenAPIV2.Parameter).in === 'body'
  ) as OpenAPIV2.InBodyParameterObject | undefined

  if (bodyParam && doc) {
    const result = generateRequestBodyExampleFromOpenAPIV2Body(op, doc)
    if (result) {
      return {
        contentType: obj as any,
        body: result,
      }
    }
  }

  // Fallback to empty body for textual content types
  return { contentType: obj as any, body: '' }
}

/**
 * Parses OpenAPI v3 form data body
 */
export function parseOpenAPIV3BodyFormData(
  contentType: 'multipart/form-data' | 'application/x-www-form-urlencoded',
  mediaObj: OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
): BrainfishRESTReqBody {
  const schema = mediaObj.schema as
    | OpenAPIV3.SchemaObject
    | OpenAPIV31.SchemaObject
    | undefined

  if (!schema || schema.type !== 'object') {
    return contentType === 'application/x-www-form-urlencoded'
      ? { contentType, body: '' }
      : { contentType, body: [] }
  }

  const keys = Object.keys(schema.properties ?? {})

  if (contentType === 'application/x-www-form-urlencoded') {
    return {
      contentType,
      body: keys.map((key) => `${key}: `).join('\n'),
    }
  }

  return {
    contentType,
    body: keys.map(
      (key) => <FormDataKeyValue>{ key, value: '', isFile: false, active: true }
    ),
  }
}

/**
 * Parses OpenAPI v3 request body
 */
export function parseOpenAPIV3Body(
  doc: OpenAPI.Document,
  op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
): BrainfishRESTReqBody {
  const objs = Object.entries(
    (
      op.requestBody as
        | OpenAPIV3.RequestBodyObject
        | OpenAPIV31.RequestBodyObject
        | undefined
    )?.content ?? {}
  )

  if (objs.length === 0) return { contentType: null, body: null }

  // We only take the first definition
  const [contentType, media]: [
    string,
    OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject,
  ] = objs[0]

  if (!(contentType in knownContentTypes)) {
    return { contentType: null, body: null }
  }

  // Handle form data types
  if (
    contentType === 'multipart/form-data' ||
    contentType === 'application/x-www-form-urlencoded'
  ) {
    return parseOpenAPIV3BodyFormData(contentType, media)
  }

  // For other content types (JSON, XML, etc.), try to generate sample from schema
  try {
    const docAny = doc as any
    const isV31 = docAny.openapi && docAny.openapi.startsWith('3.1')

    let sampleBody: any
    if (isV31) {
      sampleBody = generateV31Example(media as any, doc)
    } else {
      sampleBody = generateV3Example(media as any, doc)
    }

    if (sampleBody !== null && sampleBody !== undefined) {
      return {
        contentType: contentType as any,
        body:
          typeof sampleBody === 'string'
            ? sampleBody
            : JSON.stringify(sampleBody, null, 2),
      }
    }
  } catch {
    // Fall through to empty body
  }

  // Fallback to empty body for textual content types
  return { contentType: contentType as any, body: '' }
}

/**
 * Parses OpenAPI request body (v2 or v3)
 */
export function parseOpenAPIBody(
  doc: OpenAPI.Document,
  op: OpenAPIOperationType
): BrainfishRESTReqBody {
  return isOpenAPIV3Operation(doc, op)
    ? parseOpenAPIV3Body(doc, op)
    : parseOpenAPIV2Body(op as OpenAPIV2.OperationObject, doc)
}
