import path from "path"
import { hasIndexingPlugin } from "@saeeol/indexing/detect"
import { toIndexingConfigInput } from "@saeeol/indexing/config"
import { CodeIndexManager } from "@saeeol/indexing/engine"
import {
  normalizeIndexingStatus,
  disabledIndexingStatus,
} from "@saeeol/indexing/status"
import { Instance } from "@/project/instance"
import { Bus } from "@/bus"
import { Config } from "@/config/config"
import { registerDisposer } from "@/effect/instance-registry"
import { Global } from "@saeeol/core/global"
import { LanceDBRuntime } from "./lancedb"
import { indexingWithSaeeolDefault } from "./indexing-auth"
import {
  log,
  missing,
  noWorkspace,
  worktreeDisabled,
  isWorktreePath,
  failed,
  pending,
  saeeolAuth,
  enrichSaeeol,
  model,
  trackTelemetry,
} from "./indexing-helpers"
import { Event, type Status, type Entry, type Cache } from "./indexing-types"

const cache = new Map<string, Cache>()

const inert = async (current: () => Status): Promise<Entry> => {
  const publish = async () => {
    await Bus.publish(Event, { status: current() })
  }

  await publish()
  return {
    current,
    publish,
    dispose() {},
  }
}

function track(hit: Cache, entry: Entry) {
  if (!hit.entry) hit.resolve(entry)
  hit.entry = entry
  if (hit.disposed) entry.dispose()
  return entry
}

const boot = async (hit: Cache): Promise<Entry> => {
  const dir = Instance.directory
  const cfg = await Config.get()
  if (process.env["SAEEOL_DISABLE_CODEBASE_INDEXING"] === "vscode-no-workspace") {
    return track(hit, await inert(() => noWorkspace()))
  }
  if (!hasIndexingPlugin(cfg.plugin)) return track(hit, await inert(() => missing()))

  if (cfg.experimental?.semantic_indexing !== true) {
    return track(
      hit,
      await inert(() =>
        disabledIndexingStatus("Semantic indexing is disabled. Enable it in the Experimental settings."),
      ),
    )
  }

  if (isWorktreePath(dir)) {
    return track(hit, await inert(() => worktreeDisabled()))
  }

  log.info("initializing project indexing", { workspacePath: dir })
  const root = path.join(Global.Path.state, "indexing")
  const manager = new CodeIndexManager(dir, root)
  const auth = await saeeolAuth(cfg)
  const globalConfig = await Config.getGlobal()
  const merged = indexingWithSaeeolDefault(
    { ...cfg, indexing: { ...globalConfig.indexing, ...cfg.indexing } },
    auth,
  ) as Config.Indexing | undefined
  const cfgInput = await model(
    enrichSaeeol(
      toIndexingConfigInput({
        ...merged,
        enabled: merged?.enabled === true || globalConfig.indexing?.enabled === true,
      }),
      auth,
    ),
    auth,
  )
  const box = { status: pending() as Status | undefined }
  const current = () => box.status ?? normalizeIndexingStatus(manager)
  let disposed = false

  const publish = async () => {
    await Bus.publish(Event, { status: current() })
  }
  const report = async () => {
    try {
      return await publish()
    } catch (err) {
      log.error("failed to publish indexing status", { err })
    }
  }

  const unsub = manager.onProgressUpdate.on(() => {
    void report()
  })
  const telemetrySub = manager.onTelemetry.on((event) => {
    trackTelemetry(event)
  })

  const base: Entry = {
    current,
    publish,
    dispose() {
      if (disposed) return
      disposed = true
      unsub.dispose()
      telemetrySub.dispose()
      manager.dispose()
    },
  }
  track(hit, base)
  await report()

  if (hit.disposed) return base
  const err = await LanceDBRuntime.ensure(cfgInput.vectorStoreProvider)
    .then(() => manager.initialize(cfgInput))
    .then(
      () => undefined,
      (err) => err,
    )
  if (hit.disposed) return base

  if (err) {
    box.status = failed(err)
    log.error("project indexing initialization failed", {
      err,
      workspacePath: dir,
    })
    await report()
    return base
  }
  box.status = undefined
  base.manager = manager

  log.info("project indexing initialized", {
    workspacePath: dir,
    featureEnabled: manager.isFeatureEnabled,
    featureConfigured: manager.isFeatureConfigured,
    state: manager.getCurrentStatus().systemStatus,
  })
  await report()

  return base
}

const hitFn = () => {
  const dir = Instance.directory
  const existing = cache.get(dir)
  if (existing) return existing

  const gate = Promise.withResolvers<Entry>()
  const next = {
    ready: gate.promise,
    resolve: gate.resolve,
    reject: gate.reject,
  } as Cache
  next.promise = boot(next)
    .then((entry) => {
      if (next.disposed) {
        entry.dispose()
        return entry
      }
      next.entry = entry
      return entry
    })
    .catch((err) => {
      next.reject(err)
      if (cache.get(dir) === next) cache.delete(dir)
      throw err
    })
  cache.set(dir, next)
  return next
}

registerDisposer(async (dir) => {
  const h = cache.get(dir)
  cache.delete(dir)
  if (h) h.disposed = true
  if (h?.entry) {
    h.entry.dispose()
    return
  }
})

export { cache, hitFn }
