import { ValueType, ObjectType, StringType } from 'yaschva'
import { map } from 'microtil'
import { validate, isValidationError } from './jsonSchema.js'
import {
  CrudContract, CrudAuthAll, CrudAuthSome, OutputSuccess, Output, ManageableFields
} from './types.js'
import {
  HttpMethods,
  SearchTypes
} from 'declarapi-runtime'
import { loadJSON, baseSchemaLocation } from '../util.js'

const contractOptions = (input: ValueType | ValueType[]): ValueType[] => {
  if (Array.isArray(input)) {
    if (input.some(x => x === '?')) { return input }

    return input.concat(['?'])
  }
  return [input, '?']
}

const searchToType =
  (idType: 'string'| StringType, dataType: ObjectType, search?: SearchTypes): ObjectType => {
    if (search === 'idOnly') {
      return { id: [idType, { $array: idType }, '?'] }
    } else if (search === 'textSearch') {
      return { search: ['string', '?'], id: [idType, { $array: idType }, '?'] }
    } else if (search === 'full') {
      return map(dataType, value => contractOptions(value))
    } else if (!search) {
      return {}
    }

    return search
  }

const checkIdField = (contract:CrudContract) :Output | false => {
  if (contract.dataType.id === undefined) {
    return {
      type: 'error',
      errors: 'id field does not exist in the data declaration'
    }
  }

  const idType: any = contract.dataType.id
  if (!(idType === 'string' || idType.$string)) {
    return {
      type: 'error',
      errors: 'Type of id field must be string'
    }
  }

  return false
}
const checkManageFields = (contract: CrudContract): Output|false => {
  for (const [key, value] of Object.entries(contract.manageFields || {})) {
    if (value) {
      const fieldType: any = contract.dataType[key]
      if (fieldType === undefined) {
        return {
          type: 'error',
          errors: `managed field "${key}" is not present on data type`
        }
      }

      if (!(fieldType === 'string' || fieldType.$string)) {
        return {
          type: 'error',
          errors: `managed field "${key}" must be a string, current type :${fieldType}`
        }
      }
    }
  }

  return false
}

const removeManaged = (args: ObjectType, manageFields?: ManageableFields):ObjectType => {
  const result = { ...args }
  for (const [key, value] of Object.entries(manageFields || {})) {
    if (value) {
      delete result[key]
    }
  }

  return result
}
const isCrudAuth = (tbd: any): tbd is CrudAuthAll => tbd.post !== undefined
const isCrudAuthSome = (tbd: any): tbd is CrudAuthSome => tbd.modify !== undefined
const transformForPost = (tbd: any) => Array.isArray(tbd) && tbd.find(x => x.createdBy) ? true : tbd
export const transform = async (data:CrudContract | any): Promise<Output> => {
  const valid = await validate(await loadJSON(`${await baseSchemaLocation()}crudContractSchema.json`), data)

  if (isValidationError(valid)) return valid

  const contractData: CrudContract = data

  const au = contractData.authentication

  const auth = {
    GET: isCrudAuth(au) ? au.get : (isCrudAuthSome(au) ? au.get : au),
    POST: isCrudAuth(au) ? au.post : transformForPost((isCrudAuthSome(au) ? au.modify : au)),
    PUT: isCrudAuth(au) ? au.put : (isCrudAuthSome(au) ? au.modify : au),
    PATCH: isCrudAuth(au) ? au.put : (isCrudAuthSome(au) ? au.modify : au),
    DELETE: isCrudAuth(au) ? au.delete : (isCrudAuthSome(au) ? au.delete || au.modify : au)
  }

  const createOutput = (method: HttpMethods, args: ObjectType,
    returns: ObjectType = contractData.dataType): OutputSuccess => ({
    method,
    name: contractData.name,
    authentication: auth[method],
    manageFields: contractData.manageFields || {},
    search: contractData.search,
    preferredImplementation: contractData.preferredImplementation,
    arguments: args,
    returns
  })

  const returnArray = { $array: contractData.dataType }
  const output: OutputSuccess[] = []

  const errorWithId = checkIdField(contractData)
  if (errorWithId) return errorWithId

  const errorWithManageFields = checkManageFields(contractData)
  if (errorWithManageFields) return errorWithManageFields

  const idType: any = contractData.dataType.id
  if (contractData.methods?.get !== false) {
    const search = contractData.search
    output.push(createOutput('GET',
      searchToType(idType, contractData.dataType, search), returnArray))
  }

  if (contractData.methods?.post !== false) {
    const post = { ...contractData.dataType, id: [idType, '?'] }
    output.push(createOutput('POST', removeManaged(post, contractData.manageFields)))
  }

  if (contractData.methods?.put !== false) {
    output.push(createOutput('PUT', removeManaged(contractData.dataType, contractData.manageFields)))
  }

  if (contractData.methods?.patch !== false) {
    const patch: {[s: string]: ValueType | ValueType[];} =
    { ...map(contractData.dataType, contractOptions), id: idType }
    output.push(createOutput('PATCH', removeManaged(patch, contractData.manageFields)))
  }

  if (contractData.methods?.delete !== false) {
    const deleteIds: {[s: string]: ValueType[];} = { id: [idType, { $array: idType }] }
    output.push(createOutput('DELETE', deleteIds, returnArray))
  }

  return { type: 'result', key: contractData.name, results: output }
}

export default transform
