/** LTM — filesystem-based vector store */

import path from "path"
import { mkdir, readFile, writeFile, readdir, rm, stat } from "fs/promises"
import * as Log from "@saeeol/core/util/log"
import { Global } from "@saeeol/core/global"
import type { Memory, MemoryType } from "./types"

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

function cosine(a: number[], b: number[]): number {
  let dot = 0
  let na = 0
  let nb = 0
  const len = Math.min(a.length, b.length)
  for (let i = 0; i < len; i++) {
    dot += a[i]! * b[i]!
    na += a[i]! * a[i]!
    nb += b[i]! * b[i]!
  }
  const denom = Math.sqrt(na) * Math.sqrt(nb)
  return denom === 0 ? 0 : dot / denom
}

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

function memoryPath(id: string): string {
  const safe = id.replace(/[:<>\"|?*]/g, "_")
  return path.join(storeDir(), `${safe}.json`)
}

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

async function ensure(): Promise<void> {
  await mkdir(storeDir(), { recursive: true })
}

interface Index {
  memories: Array<{ id: string; type: MemoryType; timestamp: number; source: string }>
}

async function readIndex(): Promise<Index> {
  try {
    const raw = await readFile(indexPath(), "utf-8")
    return JSON.parse(raw) as Index
  } catch {
    return { memories: [] }
  }
}

async function writeIndex(idx: Index): Promise<void> {
  await ensure()
  await writeFile(indexPath(), JSON.stringify(idx, null, 2))
}

export async function upsert(memory: Memory): Promise<void> {
  await ensure()
  await writeFile(memoryPath(memory.id), JSON.stringify(memory, null, 2))

  const idx = await readIndex()
  const existing = idx.memories.findIndex((m) => m.id === memory.id)
  const entry = { id: memory.id, type: memory.type, timestamp: memory.metadata.timestamp, source: memory.metadata.source }
  if (existing >= 0) {
    idx.memories[existing] = entry
  } else {
    idx.memories.push(entry)
  }
  await writeIndex(idx)
  log.info("upserted", { id: memory.id, type: memory.type })
}

export async function search(
  query: number[],
  opts?: { topK?: number; minScore?: number; type?: MemoryType },
): Promise<Memory[]> {
  const topK = opts?.topK ?? 5
  const minScore = opts?.minScore ?? 0.7
  const idx = await readIndex()

  const candidates: Memory[] = []
  for (const entry of idx.memories) {
    if (opts?.type && entry.type !== opts.type) continue
    try {
      const raw = await readFile(memoryPath(entry.id), "utf-8")
      const mem = JSON.parse(raw) as Memory
      const score = cosine(query, mem.vector)
      if (score >= minScore) {
        candidates.push({ ...mem, score })
      }
    } catch {
      // File deleted — remove from index too
    }
  }

  candidates.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
  return candidates.slice(0, topK)
}

export async function remove(ids: string[]): Promise<void> {
  for (const id of ids) {
    try {
      await rm(memoryPath(id))
    } catch { /* already removed */ }
  }

  const idx = await readIndex()
  const set = new Set(ids)
  idx.memories = idx.memories.filter((m) => !set.has(m.id))
  await writeIndex(idx)
}

export async function list(projectID?: string): Promise<Memory[]> {
  const idx = await readIndex()
  const result: Memory[] = []
  for (const entry of idx.memories) {
    try {
      const raw = await readFile(memoryPath(entry.id), "utf-8")
      const mem = JSON.parse(raw) as Memory
      if (!projectID || mem.metadata.projectID === projectID) {
        result.push(mem)
      }
    } catch { /* skip */ }
  }
  return result
}

export async function prune(olderThanMs: number): Promise<number> {
  const cutoff = Date.now() - olderThanMs
  const idx = await readIndex()
  const toRemove = idx.memories.filter((m) => m.timestamp < cutoff)
  await remove(toRemove.map((m) => m.id))
  log.info("pruned", { count: toRemove.length })
  return toRemove.length
}

export async function count(): Promise<number> {
  const idx = await readIndex()
  return idx.memories.length
}