import { omit } from 'lodash-es'
import { isEntityId, type EntityType, type Item, type Lexeme, type MediaInfo, type Property, datatypes, type Claims, type Statements } from 'wikibase-sdk'
import { newError } from '../error.js'
import { getEntityClaims } from '../get_entity.js'
import { arrayIncludes, forceArray, objectEntries } from '../utils.js'
import { formatClaims, formatSitelinks, formatTermsObject } from './format.js'
import { isIdAliasPattern, resolveIdAlias } from './id_alias.js'
import type { CreateEntityResponse } from './create.js'
import type { Reconciliation } from './validate_reconciliation_object.js'
import type { PropertiesDatatypes } from '../properties/fetch_properties_datatypes.js'
import type { AbsoluteUrl, BaseRevId } from '../types/common.js'
import type { SerializedConfig } from '../types/config.js'
import type { RawEditableEntity, RawEditableItem, RawEditableLexeme, RawEditableMediaInfo, RawEditableProperty, SimplifiedEditableClaims, SimplifiedEditableEntity } from '../types/edit_entity.js'

interface EditEntityParamsBase {
  clear?: boolean
  create?: boolean
  reconciliation?: Reconciliation
  summary?: string
  baserevid?: BaseRevId
}

export type EditEntityRawModeParams = EditEntityParamsBase & Partial<RawEditableEntity> & { rawMode: true }
export type EditEntitySimplifiedModeParams = EditEntityParamsBase & Partial<SimplifiedEditableEntity>
export type EditEntityParams = EditEntityRawModeParams | EditEntitySimplifiedModeParams

const editableTypes = [
  // 'form',
  'item',
  'lexeme',
  'mediainfo',
  'property',
  // 'sense',
] as const

interface WbeditentityDataBase <T extends RawEditableEntity> {
  type: T['type']
  new?: T['type']
  clear?: boolean
  id?: T['id']
  data: Partial<T>
}

type WbeditentityItemData = WbeditentityDataBase<RawEditableItem>
type WbeditentityPropertyData = WbeditentityDataBase<RawEditableProperty>
type WbeditentityLexemeData = WbeditentityDataBase<RawEditableLexeme>
type WbeditentityMediaInfoData = WbeditentityDataBase<RawEditableMediaInfo>
type WbeditentityData = WbeditentityItemData | WbeditentityPropertyData | WbeditentityLexemeData | WbeditentityMediaInfoData

export async function editEntity (inputParams: EditEntitySimplifiedModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
  return _editEntity(inputParams, properties, instance, config)
}

export async function _rawEditEntity (inputParams: EditEntityRawModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
  return _editEntity(inputParams, properties, instance, config)
}

async function _editEntity (inputParams: EditEntitySimplifiedModeParams | EditEntityRawModeParams, properties: PropertiesDatatypes, instance: AbsoluteUrl, config: SerializedConfig) {
  validateParameters(inputParams)

  let { id } = inputParams
  const { create, type = 'item', clear, reconciliation } = inputParams
  const datatype = 'datatype' in inputParams ? inputParams.datatype : undefined
  const rawMode = 'rawMode' in inputParams ? inputParams.rawMode : false

  if (!arrayIncludes(editableTypes, type)) {
    throw newError('invalid entity type', { type })
  }

  let params: Partial<WbeditentityData> = { type, data: {} }
  let existingClaims: Claims | Statements

  if (create) {
    if (type === 'property') {
      if (!datatype) throw newError('missing property datatype', { datatype })
      if (!datatypesSet.has(datatype)) {
        throw newError('invalid property datatype', { datatype, knownDatatypes: datatypes })
      }
      params = {
        ...params,
        new: 'property',
        data: {
          datatype,
        },
      } as WbeditentityPropertyData
    } else {
      if (datatype) {
        throw newError("an item can't have a datatype", { datatype })
      }
      params.new = 'item'
    }
  } else if (isEntityId(id) || isIdAliasPattern(id)) {
    if (isIdAliasPattern(id)) {
      id = await resolveIdAlias(id, instance)
    }
    params.id = id
    // @ts-expect-error
    if (hasReconciliationSettings(reconciliation, inputParams.claims || inputParams.statements)) {
      existingClaims = await getEntityClaims(id, config)
    }
  } else {
    throw newError('invalid entity id', { id })
  }

  for (const [ attribute, types ] of objectEntries(attributesPerEntityType)) {
    if (arrayIncludes(types, params.type)) {
      if (attribute in inputParams && inputParams[attribute] != null) {
        const inputParam = inputParams[attribute]
        if (rawMode) {
          params.data[attribute] = inputParam
        } else {
          if (attribute === 'claims' || attribute === 'statements') {
            params.data[attribute] = formatClaims(inputParam, properties, instance, reconciliation, existingClaims)
          } else {
            params.data[attribute] = simplifiedInputFormatters[attribute](inputParam)
          }
        }
      }
    }
  }

  if (clear === true) params.clear = true

  if (!clear && Object.keys(params.data).length === 0) {
    throw newError('no data was passed', { id })
  }

  return {
    action: 'wbeditentity',
    data: {
      ...omit(params, 'type', 'data'),
      // Stringify as it will be passed as form data
      data: JSON.stringify(params.data),
    },
  }
}

// TODO: add support for lemmas, forms and senses
const attributesPerEntityType = {
  aliases: [ 'item', 'property' ],
  claims: [ 'item', 'property', 'lexeme' ],
  descriptions: [ 'item', 'property', 'mediainfo' ],
  labels: [ 'item', 'property', 'mediainfo' ],
  statements: [ 'mediainfo' ],
  sitelinks: [ 'item' ],
} satisfies Partial<Record<keyof Item | keyof Property | keyof Lexeme | keyof MediaInfo, EntityType[]>>

type HasSimplifiedInputFormatters = Exclude<keyof typeof attributesPerEntityType, 'claims' | 'statements'>
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const simplifiedInputFormatters: Record<HasSimplifiedInputFormatters, Function> = {
  aliases: formatTermsObject.bind(null, 'alias'),
  descriptions: formatTermsObject.bind(null, 'description'),
  labels: formatTermsObject.bind(null, 'label'),
  sitelinks: formatSitelinks,
}

const datatypesSet = new Set(datatypes)

const allowedParameters = new Set([
  'id', 'create', 'type', 'datatype', 'clear', 'rawMode', 'summary', 'baserevid',
  'labels', 'aliases', 'descriptions', 'claims', 'statements', 'sitelinks', 'reconciliation',
  'origin',
])

function validateParameters (params: EditEntityParams) {
  for (const parameter in params) {
    if (!allowedParameters.has(parameter)) {
      throw newError(`invalid parameter: ${parameter}`, 400, { parameter, allowedParameters, params })
    }
  }
}

function hasReconciliationSettings (reconciliation: Reconciliation, claims: SimplifiedEditableClaims) {
  if (reconciliation != null) return true
  for (const property in claims) {
    for (const claim of forceArray(claims[property])) {
      if (claim.reconciliation != null) return true
    }
  }
  return false
}

export type EditEntityResponse = CreateEntityResponse
