import { validateId } from './validate'

type Obj = Record<string, any>

function assertSingleProperty(obj: Obj) {
  if (typeof obj !== 'object') {
    throw new Error(`Not an object: ${JSON.stringify(obj)}`)
  }
  if (Object.keys(obj).length !== 1) {
    throw new Error(
      `Object must have one single property: ${JSON.stringify(obj)}`
    )
  }
  return [Object.keys(obj)[0], Object.values(obj)[0]]
}

function nary(op: string, expressions: string[]) {
  op = op.toUpperCase()
  if (!Array.isArray(expressions)) {
    throw new Error(`${op} requires an array: ${expressions}`)
  }
  for (const expression of expressions) {
    if (typeof expression !== 'string' || expression.trim().length === 0) {
      throw new Error(`${op} requires string expressions: ${expression}`)
    }
  }
  const many = 1 < expressions.length
  return `${many ? '(' : ''}${
    expressions.map((e) => e.trim()).join(` ${op} `)
  }${many ? ')' : ''}`
}

function OR(expressions: string[]) {
  return nary('or', expressions)
}

function NOT(expression: string) {
  if (typeof expression !== 'string' || expression.trim().length === 0) {
    throw new Error(`NOT requires a string expression: ${expression}`)
  }
  return `(NOT ${expression})`
}

const textElements: Record<string, string[]> = {
  title: ['ResourceName'],
  alttitle: ['AlternateResourceName'],
  anytitle: ['ResourceName', 'AlternateResourceName'],
  coo: ['CountryOfOrigin'],
  struct: ['StructuralType'],
  reftype: ['ReferentType'],
  lang: ['OriginalLanguage'],
  aoname: ['AssociatedOrg/DisplayName'],
  aoaltname: ['Associatedorg/AlternateName'],
  aoanyname: ['AssociatedOrg/DisplayName', 'AssociatedOrg/AlternateName'],
  aoid: ['AssociatedOrg@organizationID'],
  actor: ['Credits/Actor/DisplayName'],
  director: ['Credits/Director/DisplayName'],
  contributor: ['Credits/Director/DisplayName', 'Credits/Actor/DisplayName'],
  altid: ['AlternateID'],
  altidtype: ['AlternateID@type'],
  altiddomain: ['AlternateID@domain'],
}

function textQuery(key: string, obj: Obj) {
  const [op, list] = assertSingleProperty(obj)
  if (typeof list !== 'string') {
    throw new Error(`Invalid text query word list: ${list}`)
  }
  const words = list.split(' ').filter((s) => 0 < s.length)
  if (words.length === 0) {
    throw new Error(`Empty text query word list: ${list}`)
  }
  const te = textElements[key].map((p) => `/FullMetadata/BaseObjectData/${p}`)
  switch (op) {
  case 'words':
    return OR(te.map((p) => words.map((w) => `(${p} ${w})`)).flat())
  case 'contains':
    return OR(te.map((p) => `(${p} "${words.join(' ')}")`))
  case 'exact':
    return OR(te.map((p) => `(${p} IS "${words.join(' ')}")`))
  default:
    throw new Error(`Invalid text query operation: ${op}`)
  }
}

function idQuery(obj: Obj) {
  const [op, list] = assertSingleProperty(obj)
  if (typeof list !== 'string') {
    throw new Error(`Invalid ID list: ${list}`)
  }
  const ids = list.split(' ').filter((s) => 0 < s.length)
  if (ids.length === 0) {
    throw new Error(`Empty ID list: ${list}`)
  }
  for (const id of ids) {
    if (!validateId(id)) {
      throw new Error(`Invalid ID: ${id}`)
    }
  }
  switch (op) {
  case 'words':
    return OR(ids.map((id) => `/FullMetadata/BaseObjectData/ID ${id}`))
  case 'exact':
    if (ids.length !== 1) {
      throw new Error(`Excat ID expect single ID, but found ${ids.length}`)
    }
    return `(/FullMetadata/BaseObjectData/ID ${ids[0]})`
  default:
    throw new Error(`Invalid ID query operation: ${op}`)
  }
}

function dateQuery(obj: Obj) {
  const [op, date] = assertSingleProperty(obj)
  if (typeof date !== 'string' || date.trim().length === 0) {
    throw new Error(`Invalid date: ${date}`)
  }
  switch (op) {
  case 'date':
    return `(/FullMetadata/BaseObjectData/ReleaseDate ${date})`
  case 'before':
    return `(/FullMetadata/BaseObjectData/ReleaseDate <= ${date})`
  case 'after':
    return `(/FullMetadata/BaseObjectData/ReleaseDate >= ${date})`
  default:
    throw new Error(`Invalid date query operation: ${op}`)
  }
}

function lengthQuery(obj: Obj) {
  const [op, length] = assertSingleProperty(obj)
  if (typeof length !== 'string' || length.trim().length === 0) {
    throw new Error(`Invalid length: ${length}`)
  }
  switch (op) {
  case 'length':
    return `(/FullMetadata/BaseObjectData/ApproximateLength ${length})`
  case 'maxlength':
    return `(/FullMetadata/BaseObjectData/ApproximateLength <= ${length})`
  case 'minlength':
    return `(/FullMetadata/BaseObjectData/ApproximateLength >= ${length})`
  default:
    throw new Error(`Invalid length query operation: ${op}`)
  }
}

function existsQuery(element: string) {
  if (element === 'date') {
    return '(/FullMetadata/BaseObjectData/ReleaseDate EXISTS)'
  }
  if (element === 'length') {
    return '(/FullMetadata/BaseObjectData/ApproximateLength EXISTS)'
  }
  if (!(element in textElements)) {
    throw new Error(`Invalid element: ${element}`)
  }
  if (textElements[element].length !== 1) {
    throw new Error(`Invalid element for EXISTS query: ${element}`)
  }
  return `(/FullMetadata/BaseObjectData/${textElements[element][0]} EXISTS)`
}

function isRootQuery(value :boolean) {
  if (typeof value !== 'boolean') {
    throw new Error(`isroot value must be boolean: ${value}`)
  }
  const notRootQuery = OR([
    '(/FullMetadata/ExtraObjectMetadata/SeasonInfo EXISTS)',
    '(/FullMetadata/ExtraObjectMetadata/ClipInfo EXISTS)',
    '(/FullMetadata/ExtraObjectMetadata/ManifestationInfo EXISTS)',
    '(/FullMetadata/ExtraObjectMetadata/EpisodeInfo EXISTS)',
    '(/FullMetadata/ExtraObjectMetadata/EditInfo EXISTS)',
  ])
  return value ? NOT(notRootQuery) : notRootQuery
}

function parentQuery(id: string) {
  if (!validateId(id)) {
    throw new Error(`Invalida parent ID: ${id}`)
  }
  return OR([
    `(/FullMetadata/ExtraObjectMetadata/SeasonInfo/Parent ${id})`,
    `(/FullMetadata/ExtraObjectMetadata/ClipInfo/Parent ${id})`,
    `(/FullMetadata/ExtraObjectMetadata/ManifestationInfo/Parent ${id})`,
    `(/FullMetadata/ExtraObjectMetadata/EpisodeInfo/Parent ${id})`,
    `(/FullMetadata/ExtraObjectMetadata/EditInfo/Parent ${id})`,
  ])
}

export function buildJsonQuery(obj: Obj): string {
  const [element, value] = assertSingleProperty(obj)
  if (element === 'and' || element === 'or') {
    return nary(element, value.map(buildJsonQuery))
  }
  if (element === 'not') {
    return NOT(buildJsonQuery(value))
  }
  if (element in textElements) {
    return textQuery(element, value)
  }
  if (element === 'id') {
    return idQuery(value)
  }
  if (element === 'date') {
    return dateQuery(value)
  }
  if (element === 'length') {
    return lengthQuery(value)
  }
  if (element === 'exists') {
    return existsQuery(value)
  }
  if (element === 'isroot') {
    return isRootQuery(value)
  }
  if (element === 'parent') {
    return parentQuery(value)
  }
  throw new Error(`Invalid element: ${element}`)
}

// const qs = query(event.req.body)
// console.log(qs)
// await app.getConnector('eidr').query(qs)
// return event.res.send(qs)

// Tests:

// { title: { words: 'star wars' }, length: 3 }
// Error: Object must have one single property

// { title: { words: 'star wars' } }
// (
//   (/FullMetadata/BaseObjectData/ResourceName star) OR
//   (/FullMetadata/BaseObjectData/ResourceName wars)
// )

// { alltitles: { words: 'star wars' } }
// (
//   (/FullMetadata/BaseObjectData/ResourceName star) OR
//   (/FullMetadata/BaseObjectData/ResourceName wars) OR
//   (/FullMetadata/BaseObjectData/AlternateResourceName star) OR
//   (/FullMetadata/BaseObjectData/AlternateResourceName wars)
// )

// { and: [{ title: { contains: 'star wars' } }, { coo: { exact: 'us' } }] }
// (
//   (/FullMetadata/BaseObjectData/ResourceName "star wars") AND
//   (/FullMetadata/BaseObjectData/CountryOfOrigin IS "us")
// )

// { or: [{ title: { contains: 'star wars' } }, { coo: { exact: 'us' } }] }
// (
//   (/FullMetadata/BaseObjectData/ResourceName "star wars") OR
//   (/FullMetadata/BaseObjectData/CountryOfOrigin IS "us")
// )

// { not: { title: { contains: 'star wars' } } }
// (NOT (/FullMetadata/BaseObjectData/ResourceName "star wars"))

// { date: { date: '2000' } }
// (/FullMetadata/BaseObjectData/ReleaseDate 2000)

// { date: { before: '2000' } }
// (/FullMetadata/BaseObjectData/ReleaseDate <= 2000)

// { date: { after: '2000' } }
// (/FullMetadata/BaseObjectData/ReleaseDate >= 2000)

// { length: { length: 'PT23M' } }
// (/FullMetadata/BaseObjectData/ApproximateLength PT23M)

// { length: { maxlength: 'PT23M' } }
// (/FullMetadata/BaseObjectData/ApproximateLength <= PT23M)

// { length: { minlength: 'PT23M' } }
// (/FullMetadata/BaseObjectData/ApproximateLength >= PT23M)

// { exists: 'actor' }
// (/FullMetadata/BaseObjectData/Credits/Actor/DisplayName EXISTS)

// { isroot: true }
// (NOT (
//   (/FullMetadata/ExtraObjectMetadata/SeasonInfo EXISTS) OR
//   (/FullMetadata/ExtraObjectMetadata/ClipInfo EXISTS) OR
//   (/FullMetadata/ExtraObjectMetadata/ManifestationInfo EXISTS) OR
//   (/FullMetadata/ExtraObjectMetadata/EpisodeInfo EXISTS) OR
//   (/FullMetadata/ExtraObjectMetadata/EditInfo EXISTS))
// )

// { parent: '10.5240/75C0-4663-9D6D-C864-1D9B-I' }
// (
//   (
//     /FullMetadata/ExtraObjectMetadata/SeasonInfo/Parent
//     10.5240/75C0-4663-9D6D-C864-1D9B-I
//   ) OR (
//     /FullMetadata/ExtraObjectMetadata/ClipInfo/Parent
//     10.5240/75C0-4663-9D6D-C864-1D9B-I
//   ) OR (
//     /FullMetadata/ExtraObjectMetadata/ManifestationInfo/Parent
//     10.5240/75C0-4663-9D6D-C864-1D9B-I
//   ) OR (
//     /FullMetadata/ExtraObjectMetadata/EpisodeInfo/Parent
//     10.5240/75C0-4663-9D6D-C864-1D9B-I
//   ) OR (
//     /FullMetadata/ExtraObjectMetadata/EditInfo/Parent
//     10.5240/75C0-4663-9D6D-C864-1D9B-I
//   )
// )
