import { isEqual } from 'lodash-es'
import { simplifyReferences, simplifySnak, type PropertyClaims, type PropertyStatements } from 'wikibase-sdk'
import { isMatchingClaimFactory } from '../claim/is_matching_claim.js'
import { isMatchingSnak } from '../claim/is_matching_snak.js'
import { newError } from '../error.js'
import { validateReconciliationObject, type Reconciliation } from './validate_reconciliation_object.js'
import type { CustomSimplifiedEditableClaim } from '../types/edit_entity.js'

// Ignoring MediaInfo statements weirdness here, as it doesn't rely on snaks datatypes
export function reconcileClaimFactory (reconciliation: Reconciliation, existingPropertyClaims: PropertyClaims | PropertyStatements) {
  return function reconcileClaim (claim: CustomSimplifiedEditableClaim): CustomSimplifiedEditableClaim | CustomSimplifiedEditableClaim[] | void {
    reconciliation = 'reconciliation' in claim ? claim.reconciliation : reconciliation
    if (!reconciliation) return claim
    validateReconciliationObject(reconciliation, claim)
    const { mode, matchingQualifiers, matchingReferences } = reconciliation
    if (mode === 'skip-on-any-value') {
      if (existingPropertyClaims.length > 0) {
        console.warn(`[wikibase-edit] skipping claim: a claim already exists for that property\n${JSON.stringify({ claim, existingPropertyClaims })}`)
        return
      }
    }

    const existingClaims = existingPropertyClaims.filter(isMatchingClaimFactory(claim, matchingQualifiers))

    if (claim.remove) {
      if (existingClaims.length > 0) {
        return existingClaims.map(({ id }) => ({ id, remove: true }))
      } else {
        throw newError("can't remove claim: claim not found", claim)
      }
    }

    if (existingClaims.length === 0) return claim

    if (mode === 'skip-on-value-match') {
      console.warn(`[wikibase-edit] skipping claim: a similar claim already exists\n${JSON.stringify({ claim, existingClaims })}`)
    } else if (mode === 'merge') {
      if (existingClaims.length > 1) {
        throw newError('too many matching claims found', { claim, existingClaims })
      }
      const existingClaim = existingClaims[0]

      if (claim.qualifiers != null) {
        existingClaim.qualifiers = existingClaim.qualifiers || {}
        addMissingQualifiers(existingClaim.qualifiers, claim.qualifiers)
      }
      if (claim.references != null) {
        existingClaim.references = existingClaim.references || []
        if (matchingReferences) {
          mergeReferences(existingClaim.references, claim.references, matchingReferences)
        } else {
          const currentReference = simplifyReferences(existingClaim.references)
          // @ts-expect-error
          const newReferenceReference = claim.references.filter(isNewReference(currentReference))
          existingClaim.references.push(...newReferenceReference)
        }
      }
      // @ts-expect-error
      return existingClaim
    } else {
      throw newError('unexpected reconciliation mode', 500, { reconciliation })
    }
  }
}

function addMissingQualifiers (existingQualifiers, newQualifiers) {
  for (const property in newQualifiers) {
    existingQualifiers[property] = existingQualifiers[property] || []
    existingQualifiers[property].push(...newQualifiers[property])
  }
}

const isNewReference = currentReference => reference => {
  const simplifiedReference = aggregateReferenceSnaks(reference.snaks, true)
  return !currentReference.some(currentReference => {
    return isEqual(currentReference, simplifiedReference)
  })
}

function mergeReferences (existingReferences, newReferences, matchingReferences) {
  const addedReferences = []
  for (const newReference of newReferences) {
    const newReferenceSnaks = aggregateReferenceSnaks(newReference.snaks, false)
    const matchingReference = existingReferences.find(isMatchingReference(newReferenceSnaks, matchingReferences))
    if (matchingReference) {
      mergeMatchingReference(matchingReference, newReferenceSnaks)
    } else {
      addedReferences.push(newReference)
    }
  }
  existingReferences.push(...addedReferences)
}

function mergeMatchingReference (matchingReference, newReferenceSnaks) {
  for (const property in newReferenceSnaks) {
    if (property in matchingReference.snaks) {
      const existingPropertySnaks = matchingReference.snaks[property]
      const newSnaks = newReferenceSnaks[property]
        .filter(snak => !hasSomeMatch(existingPropertySnaks, snak))
      matchingReference.snaks[property].push(...newSnaks)
    } else {
      matchingReference.snaks[property] = newReferenceSnaks[property]
    }
  }
}

const isMatchingReference = (newReferenceSnaks, matchingReferences) => existingReference => {
  const existingReferenceSnaks = existingReference.snaks
  for (const property of matchingReferences) {
    const [ pid, option = 'all' ] = property.split(':')
    const newPropertySnaks = newReferenceSnaks[pid]
    const existingPropertySnaks = existingReferenceSnaks[pid]
    if (newPropertySnaks == null && existingPropertySnaks == null) return false
    if (newPropertySnaks != null && existingPropertySnaks == null) return false
    if (newPropertySnaks == null && existingPropertySnaks != null) return false
    const methodName = methodNameByOption[option]
    const everyNewSnakHasAnExistingMatch = newPropertySnaks[methodName](snak => {
      return hasSomeMatch(existingPropertySnaks, snak)
    })
    if (!everyNewSnakHasAnExistingMatch) return false
  }
  return true
}

const methodNameByOption = {
  any: 'some',
  all: 'every',
}

function aggregateReferenceSnaks (snaksArray, simplify) {
  return snaksArray.reduce((aggregatedSnaks, snak) => {
    const { property } = snak
    aggregatedSnaks[property] = aggregatedSnaks[property] || []
    if (simplify) snak = simplifySnak(snak, {})
    aggregatedSnaks[property].push(snak)
    return aggregatedSnaks
  }, {})
}

function hasSomeMatch (existingPropertySnaks, snak) {
  return existingPropertySnaks.some(existingSnak => isMatchingSnak(existingSnak, snak))
}
