import {
  App,
  Notice,
  Platform,
  Plugin,
  type PluginManifest,
  TFile,
} from 'obsidian'
import { LocatorVaultModal } from './components/modals'
import { Database } from './database'
import {
  eventBus,
  EventNames,
  indexingStep,
  IndexingStepType,
  type TextExtractorApi,
} from './globals'
import { NotesIndexer } from './notes-indexer'
import { DocumentsRepository } from './repositories/documents-repository'
import { EmbedsRepository } from './repositories/embeds-repository'
import { SearchEngine } from './search/search-engine'
import { SearchHistory } from './search/search-history'
import { getDefaultSettings, loadSettings, SettingsTab } from './settings'
import type { LocatorSettings } from './settings/utils'
import { isCacheEnabled, saveSettings, showExcerpt } from './settings/utils'
import { notifyOnIndexed, registerAPI } from './tools/api'
import { TextProcessor } from './tools/text-processing'
import { logVerbose } from './tools/utils'

export default class LocatorPlugin extends Plugin {
  // FIXME: fix the type
  public apiHttpServer: null | any = null
  public settings: LocatorSettings = getDefaultSettings(this.app)

  public readonly documentsRepository: DocumentsRepository
  public readonly embedsRepository = new EmbedsRepository(this)
  public readonly database = new Database(this)

  public readonly notesIndexer = new NotesIndexer(this)
  public readonly textProcessor = new TextProcessor(this)
  public readonly searchEngine = new SearchEngine(this)
  public readonly searchHistory = new SearchHistory(this)

  private ribbonButton?: HTMLElement
  private refreshIndexCallback?: (ev: FocusEvent) => any

  constructor(app: App, manifest: PluginManifest) {
    super(app, manifest)
    this.documentsRepository = new DocumentsRepository(this)
  }

  async onload(): Promise<void> {
    this.settings = await loadSettings(this)
    this.addSettingTab(new SettingsTab(this))

    if (!Platform.isMobile) {
      import('./tools/api-server').then(
        m => (this.apiHttpServer = m.getServer(this))
      )
    }

    await cleanOldCacheFiles(this.app)
    await this.database.clearOldDatabases()

    registerAPI(this)

    const settings = this.settings
    if (settings.ribbonIcon) {
      this.addRibbonButton()
    }

    eventBus.disable('vault')
    eventBus.disable('infile')
    eventBus.on('global', EventNames.ToggleExcerpts, () => {
      showExcerpt.set(!settings.showExcerpt)
    })

    // Commands to display Locator modals
    this.addCommand({
      id: 'show-modal',
      name: 'Vault search',
      callback: () => {
        new LocatorVaultModal(this).open()
      },
    })

    const searchEngine = this.searchEngine

    this.app.workspace.onLayoutReady(async () => {
      // Listeners to keep the search index up-to-date
      this.registerEvent(
        this.app.vault.on('create', file => {
          if (!(file instanceof TFile)) return
          if (this.notesIndexer.isFileIndexable(file.path)) {
            logVerbose('Indexing new file', file.path)
            searchEngine.addFromPaths([file.path])
            this.embedsRepository.refreshEmbedsForNote(file.path)
          }
        })
      )
      this.registerEvent(
        this.app.vault.on('delete', file => {
          if (!(file instanceof TFile)) return
          logVerbose('Removing file', file.path)
          this.documentsRepository.removeDocument(file.path)
          searchEngine.removeFromPaths([file.path])
          this.embedsRepository.removeFile(file.path)
        })
      )
      this.registerEvent(
        this.app.vault.on('modify', async file => {
          if (!(file instanceof TFile)) return
          if (this.notesIndexer.isFileIndexable(file.path)) {
            this.notesIndexer.flagNoteForReindex(file)
          }
          this.embedsRepository.refreshEmbedsForNote(file.path)
        })
      )
      this.registerEvent(
        this.app.vault.on('rename', async (file, oldPath) => {
          if (!(file instanceof TFile)) return
          if (this.notesIndexer.isFileIndexable(file.path)) {
            logVerbose('Renaming file', file.path)
            this.documentsRepository.removeDocument(oldPath)
            await this.documentsRepository.addDocument(file.path)

            searchEngine.removeFromPaths([oldPath])
            await searchEngine.addFromPaths([file.path])

            this.embedsRepository.renameFile(oldPath, file.path)
          }
        })
      )

      this.refreshIndexCallback = this.notesIndexer.refreshIndex.bind(
        this.notesIndexer
      )
      addEventListener('blur', this.refreshIndexCallback!)
      removeEventListener

      await this.executeFirstLaunchTasks()
      await this.populateIndex()

      if (this.apiHttpServer && settings.httpApiEnabled) {
        this.apiHttpServer.listen(settings.httpApiPort)
      }
    })
  }

  async executeFirstLaunchTasks(): Promise<void> {
    const code = '1.21.0'
    // if (settings.welcomeMessage !== code && getTextExtractor()) {
    //   const welcome = new DocumentFragment()
    //   welcome.createSpan({}, span => {
    //     span.innerHTML = `🔎 Locator can now index .docx and .xlsx documents. Don't forget to update Text Extractor and enable the toggle in Locator settings.`
    //   })
    //   new Notice(welcome, 20_000)
    // }
    this.settings.welcomeMessage = code
    await this.saveData(this.settings)
  }

  async onunload(): Promise<void> {
    // @ts-ignore
    delete globalThis['locator']

    if (this.refreshIndexCallback) {
      removeEventListener('blur', this.refreshIndexCallback)
    }

    // Clear cache when disabling Locator
    if (process.env.NODE_ENV === 'production') {
      await this.database.clearCache()
    }
    this.apiHttpServer.close()
  }

  addRibbonButton(): void {
    this.ribbonButton = this.addRibbonIcon('search', 'Locator', _evt => {
      new LocatorVaultModal(this).open()
    })
  }

  removeRibbonButton(): void {
    if (this.ribbonButton) {
      this.ribbonButton.parentNode?.removeChild(this.ribbonButton)
    }
  }

  /**
   * Plugin dependency - Chs Patch for Chinese word segmentation
   * @returns
   */
  public getChsSegmenter(): any | undefined {
    return (this.app as any).plugins.plugins['cm-chs-patch']
  }

  /**
   * Plugin dependency - Text Extractor
   * @returns
   */
  public getTextExtractor(): TextExtractorApi | undefined {
    return (this.app as any).plugins?.plugins?.['text-extractor']?.api
  }

  private async populateIndex(): Promise<void> {
    console.time('Indexing total time')
    indexingStep.set(IndexingStepType.ReadingFiles)
    const files = this.app.vault
      .getFiles()
      .filter(f => this.notesIndexer.isFileIndexable(f.path))
    console.debug(`${files.length} files total`)
    console.debug(`Cache is ${isCacheEnabled() ? 'enabled' : 'disabled'}`)
    // Map documents in the background
    // Promise.all(files.map(f => cacheManager.addToLiveCache(f.path)))

    const searchEngine = this.searchEngine
    if (isCacheEnabled()) {
      console.time('Loading index from cache')
      indexingStep.set(IndexingStepType.LoadingCache)
      const hasCache = await searchEngine.loadCache()
      if (hasCache) {
        console.timeEnd('Loading index from cache')
      }
    }

    const diff = searchEngine.getDocumentsToReindex(
      files.map(f => ({ path: f.path, mtime: f.stat.mtime }))
    )

    if (isCacheEnabled()) {
      if (diff.toAdd.length) {
        console.debug(
          'Total number of files to add/update: ' + diff.toAdd.length
        )
      }
      if (diff.toRemove.length) {
        console.debug(
          'Total number of files to remove: ' + diff.toRemove.length
        )
      }
    }

    if (diff.toAdd.length >= 1000 && isCacheEnabled()) {
      new Notice(
        `${diff.toAdd.length} files need to be indexed. Obsidian may experience stutters and freezes during the process`,
        10_000
      )
    }

    indexingStep.set(IndexingStepType.IndexingFiles)
    searchEngine.removeFromPaths(diff.toRemove.map(o => o.path))
    await searchEngine.addFromPaths(diff.toAdd.map(o => o.path))

    if ((diff.toRemove.length || diff.toAdd.length) && isCacheEnabled()) {
      indexingStep.set(IndexingStepType.WritingCache)

      // Disable settings.useCache while writing the cache, in case it freezes
      const cacheEnabled = this.settings.useCache
      if (cacheEnabled) {
        this.settings.useCache = false
        await saveSettings(this)
      }

      // Write the cache
      await this.database.writeMinisearchCache()
      await this.embedsRepository.writeToCache()

      // Re-enable settings.caching
      if (cacheEnabled) {
        this.settings.useCache = true
        await saveSettings(this)
      }
    }

    console.timeEnd('Indexing total time')
    console.log(`Indexed ${diff.toAdd.length} files, removed ${diff.toRemove.length} files`)
    if (diff.toAdd.length >= 1000 && isCacheEnabled()) {
      new Notice(`Your files have been indexed.`)
    }
    indexingStep.set(IndexingStepType.Done)
    notifyOnIndexed()
  }
}

/**
 * Read the files and feed them to Minisearch
 */

async function cleanOldCacheFiles(app: App) {
  const toDelete = [
    `${app.vault.configDir}/plugins/locator/searchIndex.json`,
    `${app.vault.configDir}/plugins/locator/notesCache.json`,
    `${app.vault.configDir}/plugins/locator/notesCache.data`,
    `${app.vault.configDir}/plugins/locator/searchIndex.data`,
    `${app.vault.configDir}/plugins/locator/historyCache.json`,
    `${app.vault.configDir}/plugins/locator/pdfCache.data`,
  ]
  for (const item of toDelete) {
    if (await app.vault.adapter.exists(item)) {
      try {
        await app.vault.adapter.remove(item)
      } catch (e) {}
    }
  }
}
