import {
  docKey,
  type Branding,
  type Class,
  type Doc,
  type DocIndexState,
  type Hierarchy,
  type Ref,
  type RefTo,
  type SearchResultDoc
} from '@hcengineering/core'
import { getResource, type Resource } from '@hcengineering/platform'

import plugin from './plugin'
import {
  type ClassSearchConfigProps,
  type IndexedDoc,
  type SearchPresenter,
  type SearchPresenterFunc,
  type SearchScoring
} from './types'

interface IndexedReader {
  get: (attribute: string) => any
  getDoc: (attribute: string) => IndexedReader | undefined
}

// TODO: Rework to use mongo
function createIndexedReader (
  _class: Ref<Class<Doc>>,
  hierarchy: Hierarchy,
  doc: DocIndexState,
  otherDocs?: Record<string, DocIndexState | undefined>,
  refAttribute?: string
): IndexedReader {
  return {
    get: (attr: string) => {
      const realAttr = hierarchy.findAttribute(_class, attr)
      if (realAttr !== undefined) {
        return doc.attributes[docKey(attr, { _class: realAttr.attributeOf })] ?? (doc as any)[attr]
      }
      return undefined
    },
    getDoc: (attr: string) => {
      const realAttr = hierarchy.findAttribute(_class, attr)
      if (realAttr !== undefined) {
        const anotherDoc = otherDocs?.[attr]
        if (anotherDoc !== undefined) {
          const refAtrr = realAttr.type as RefTo<Doc>
          return createIndexedReader(refAtrr.to, hierarchy, anotherDoc, otherDocs, docKey(attr, { _class }))
        }
      }
      return undefined
    }
  }
}

async function readAndMapProps (
  reader: IndexedReader,
  props: ClassSearchConfigProps[],
  searchProvider?: {
    hierarchy: Hierarchy
    providers: SearchPresenterFunc
  }
): Promise<Record<string, any>> {
  const res: Record<string, any> = {}
  for (const prop of props) {
    if (typeof prop === 'string') {
      res[prop] = reader.get(prop)
    } else {
      for (const [propName, rest] of Object.entries(prop)) {
        if (rest.length > 1) {
          const val = reader.getDoc(rest[0])?.get(rest[1])
          const v = Array.isArray(val) ? val[0] : val
          if (searchProvider !== undefined) {
            const func =
              searchProvider.providers !== undefined && Object.keys(searchProvider.providers).includes(propName)
                ? ((await getResource(searchProvider.providers[propName])) as any)
                : undefined
            if (func !== undefined) {
              res[propName] = func(searchProvider.hierarchy, { _class: res?._class, [propName]: v })
              continue
            }
          }
          res[propName] = v
        }
      }
    }
  }
  return res
}

function findSearchPresenter (hierarchy: Hierarchy, _class: Ref<Class<Doc>>): SearchPresenter | undefined {
  const ancestors = hierarchy.getAncestors(_class).reverse()
  for (const _class of ancestors) {
    const searchMixin = hierarchy.classHierarchyMixin(_class, plugin.mixin.SearchPresenter)
    if (searchMixin !== undefined) {
      return searchMixin
    }
  }
  return undefined
}

/**
 * @public
 */
export async function updateDocWithPresenter (
  hierarchy: Hierarchy,
  doc: DocIndexState,
  elasticDoc: IndexedDoc,
  refDocs: {
    parentDoc: DocIndexState | undefined
    spaceDoc: DocIndexState | undefined
  },
  branding: Branding | null
): Promise<void> {
  const searchPresenter = findSearchPresenter(hierarchy, doc.objectClass)
  if (searchPresenter === undefined) {
    return
  }

  const reader = createIndexedReader(doc.objectClass, hierarchy, doc, {
    space: refDocs.spaceDoc,
    attachedTo: refDocs.parentDoc
  })

  const props = [
    {
      name: 'searchTitle',
      config: searchPresenter.searchConfig.title,
      provider: searchPresenter.getSearchTitle
    }
  ] as any[]

  if (searchPresenter.searchConfig.shortTitle !== undefined) {
    props.push({
      name: 'searchShortTitle',
      config: searchPresenter.searchConfig.shortTitle,
      provider: searchPresenter.getSearchShortTitle,
      lastNameFirst: branding?.lastNameFirst
    })
  }

  if (searchPresenter.searchConfig.iconConfig !== undefined) {
    props.push({
      name: 'searchIcon',
      config: searchPresenter.searchConfig.iconConfig
    })
  }

  for (const prop of props) {
    let value
    if (prop.config.tmpl !== undefined) {
      const tmpl = prop.config.tmpl
      const renderProps = await readAndMapProps(reader, prop.config.props, { hierarchy, providers: prop.provider })
      value = fillTemplate(tmpl, renderProps)
    } else if (typeof prop.config === 'string') {
      value = reader.get(prop.config)
    } else if (prop.provider !== undefined) {
      const func = await getResource(Object.values(prop.provider)[0] as Resource<any>)
      const renderProps = await readAndMapProps(reader, prop.config.props)
      value = func(hierarchy, { _class: doc.objectClass, ...renderProps })
    } else if (prop.name === 'searchIcon') {
      value = await readAndMapProps(reader, prop.config.props)
    }
    elasticDoc[prop.name] = value
  }
}

export function getScoringConfig (hierarchy: Hierarchy, classes: Ref<Class<Doc>>[]): SearchScoring[] {
  let results: SearchScoring[] = []
  for (const _class of classes) {
    const searchPresenter = findSearchPresenter(hierarchy, _class)
    if (searchPresenter?.searchConfig.scoring !== undefined) {
      results = results.concat(searchPresenter?.searchConfig.scoring)
    }
  }
  return results
}

/**
 * @public
 */
export function mapSearchResultDoc (hierarchy: Hierarchy, raw: IndexedDoc): SearchResultDoc {
  const doc: SearchResultDoc = {
    id: raw.id,
    title: raw.searchTitle,
    shortTitle: raw.searchShortTitle,
    iconProps: raw.searchIcon,
    doc: {
      _id: raw.id,
      _class: raw._class[0]
    },
    score: raw._score
  }

  const searchPresenter = findSearchPresenter(hierarchy, doc.doc._class)
  if (searchPresenter?.searchConfig.icon !== undefined) {
    doc.icon = searchPresenter.searchConfig.icon
  }
  if (searchPresenter?.searchConfig.iconConfig !== undefined) {
    doc.iconComponent = searchPresenter.searchConfig.iconConfig.component
  }

  return doc
}

function fillTemplate (tmpl: string, props: Record<string, any>): string {
  return tmpl.replace(/{(.*?)}/g, (_, key: string) => props[key])
}