/** LTM — hardware auto-detection + deterministic LLM parameter calculation */

import os from "os"
import { Effect } from "effect"
import * as Log from "@saeeol/core/util/log"
import { Global } from "@saeeol/core/global"
import * as GPU from "@/provider/local/gpu"
import * as RAG from "@/provider/local/rag"
import type { HardwareProfile, LLMBakeParams, LTMConfig } from "./types"

const log = Log.create({ service: "ltm/config" })

export const DEFAULT_LTM_CONFIG: LTMConfig = {
  enabled: false,
  embeddingModel: "auto",
  vectorStore: "filesystem",
  maxMemories: 10000,
  episodic: { enabled: true, summaryInterval: 10, retainDays: 90 },
  semantic: { enabled: true, indexOnFileChange: true },
  procedural: { enabled: true, trackPreferences: true },
  retrieval: { topK: 5, minScore: 0.7, maxTokens: 2000 },
}

/** Collect system hardware information */
export async function profileHardware(): Promise<HardwareProfile> {
  const gpu = await Effect.runPromise(GPU.profile)
  const totalRAMMB = Math.round(os.totalmem() / (1024 * 1024))
  const cpuCores = os.cpus().length

  return {
    gpuCount: gpu.gpus.length,
    totalVRAMMB: gpu.totalVRAMMB,
    availableVRAMMB: gpu.availableVRAMMB,
    totalRAMMB,
    cpuCores,
    cudaAvailable: gpu.cudaAvailable,
  }
}

/** Hardware hash for change detection */
export function hardwareHash(hw: HardwareProfile): string {
  const raw = `${hw.gpuCount}|${hw.totalVRAMMB}|${hw.totalRAMMB}|${hw.cpuCores}|${hw.cudaAvailable}`
  let h = 0
  for (let i = 0; i < raw.length; i++) {
    h = ((h << 5) - h + raw.charCodeAt(i)) | 0
  }
  return h.toString(36)
}

/** Auto-select embedding model based on available VRAM */
export function selectEmbeddingModel(hw: HardwareProfile) {
  const vram = hw.availableVRAMMB
  // Prefer multilingual (Korean-capable) models
  if (vram >= 3000 || hw.totalRAMMB >= 16000) return RAG.EMBEDDING_MODELS[3] // bge-m3, 1024d, 2.2GB
  if (vram >= 1500 || hw.totalRAMMB >= 8000) return RAG.EMBEDDING_MODELS[4] // nomic-embed, 768d, 550MB
  if (vram >= 500) return RAG.EMBEDDING_MODELS[0] // bge-small-en, 384d, 130MB
  return RAG.EMBEDDING_MODELS[5] // all-minilm-l6, 384d, 80MB
}

 /**
  * Compute hardware-based LLM parameters and persist to disk.
  * Once computed, the same values are reused as long as hardware doesn't change.
  */
export async function bake(hw: HardwareProfile): Promise<LLMBakeParams> {
  const existing = await readBake()
  const hash = hardwareHash(hw)

  // If hash matches, reuse existing params (deterministic)
  if (existing && existing.hardwareHash === hash) {
    log.info("bake: hardware unchanged, reusing existing params", { hash })
    return existing
  }

  const model = selectEmbeddingModel(hw)
  const params: LLMBakeParams = {
    embeddingModel: model.id,
    embeddingDimensions: model.dimensions,
    embeddingVRAMMB: Math.ceil(model.sizeBytes / (1024 * 1024)),
    contextLength: hw.cudaAvailable ? 8192 : 4096,
    batchSize: hw.cudaAvailable ? 64 : 16,
    numThread: Math.max(1, Math.floor(hw.cpuCores * 0.75)),
    numGPU: hw.cudaAvailable ? -1 : 0,
    hardwareHash: hash,
  }

  await writeBake(params)
  log.info("bake: computed new params", { hash, model: model.id })
  return params
}

import path from "path"
import { mkdir, readFile, writeFile } from "fs/promises"

function bakeDir(): string {
  return path.join(Global.Path.data, "ltm")
}

function bakePath(): string {
  return path.join(bakeDir(), "bake.json")
}

async function readBake(): Promise<LLMBakeParams | undefined> {
  try {
    const raw = await readFile(bakePath(), "utf-8")
    return JSON.parse(raw) as LLMBakeParams
  } catch {
    return undefined
  }
}

async function writeBake(params: LLMBakeParams): Promise<void> {
  await mkdir(bakeDir(), { recursive: true })
  await writeFile(bakePath(), JSON.stringify(params, null, 2))
}