/** LTM — background collection pipeline */

import { Effect } from "effect"
import * as Log from "@saeeol/core/util/log"
import * as Bus from "@/bus"
import * as Store from "@/ltm/store"
import * as Embedder from "@/provider/local/embedder"
import * as Episodic from "@/ltm/memory/episodic"
import * as Semantic from "@/ltm/memory/semantic"
import * as Procedural from "@/ltm/memory/procedural"
import * as Scheduler from "@/ltm/scheduler"
import type { LTMConfig, LLMBakeParams } from "@/ltm/types"
import { LTMEvent } from "@/ltm/events"
import { Event as SessionEvent } from "@/session/core/session-types"

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

let running = false
let config: LTMConfig | undefined
let unsubscribers: Array<() => void> = []

/** Start the pipeline */
export async function start(cfg: LTMConfig, bake: LLMBakeParams): Promise<void> {
  if (running) return
  if (!cfg.enabled) return

  config = cfg
  running = true

  log.info("pipeline starting", { model: bake.embeddingModel })

  const server = await Embedder.start(bake)
  if (server.status !== "running") {
    log.error("embedding server failed to start, pipeline disabled")
    running = false
    return
  }
  if (cfg.episodic.enabled) {    const pruned = await Store.prune(cfg.episodic.retainDays * 24 * 60 * 60 * 1000)
    if (pruned > 0) {
      log.info("pruned old episodic memories", { count: pruned })
      void Bus.publish(LTMEvent.MemoryPruned, { count: pruned, type: "episodic" })
    }
  }
  const count = await Store.count()
  if (count > cfg.maxMemories) {
    const excess = count - cfg.maxMemories
    const memories = await Store.list()
    const sorted = memories.sort((a, b) => a.metadata.timestamp - b.metadata.timestamp)
    const toRemove = sorted.slice(0, excess).map((m) => m.id)
    await Store.remove(toRemove)
    log.info("trimmed memories to max", { removed: toRemove.length })
  }
  bindSubscriptions()

  log.info("pipeline started", { memoryCount: await Store.count() })
}

/** Stop the pipeline */
export async function stop(): Promise<void> {
  if (!running) return
  running = false
  for (const unsub of unsubscribers) {
    try { unsub() } catch { /* ignore */ }
  }
  unsubscribers = []

  await Embedder.stop()
  config = undefined
  log.info("pipeline stopped")
}

/** Current state */
export function isActive(): boolean {
  return running
}

function bindSubscriptions() {
  if (!config) return

  // 1. Session diff → episodic memory (after code change + AI response)
  const unsubDiff = Bus.subscribe(SessionEvent.Diff, async (event) => {
    if (!running || !config?.episodic.enabled) return

    const sessionID = event.properties.sessionID
    // diff events fire when session state is updated after an assistant response
    // Contains both user message and assistant response context
    const diffs = event.properties.diff
    if (!diffs || diffs.length === 0) return

    const summary = `code changes: ${diffs.map((d) => `${d.file} (${d.status})`).join(", ")}`
    try {
      const vector = await Embedder.embedOne(summary)
      const memory = {
        id: `epi:${sessionID}:${Date.now()}`,
        type: "episodic" as const,
        content: summary.slice(0, 500),
        summary,
        vector,
        metadata: {
          source: `session:${sessionID}`,
          timestamp: Date.now(),
          sessionID,
          tags: ["code-change"],
        },
      }
      await Store.upsert(memory)
      void Bus.publish(LTMEvent.MemoryStored, {
        id: memory.id,
        type: memory.type,
        source: memory.metadata.source,
      })
    } catch (e) {
      log.error("failed to store session diff memory", { error: e })
    }
  })
  unsubscribers.push(unsubDiff)

  // 2. Session error → episodic memory (problem/resolution tracking)
  const unsubError = Bus.subscribe(SessionEvent.Error, async (event) => {
    if (!running || !config?.episodic.enabled) return
    const sessionID = event.properties.sessionID
    if (!sessionID) return

    const err = event.properties.error
    if (!err) return

    const summary = `error in session: ${typeof err === "object" ? JSON.stringify(err).slice(0, 200) : String(err).slice(0, 200)}`
    try {
      const vector = await Embedder.embedOne(summary)
      const memory = {
        id: `epi:err:${sessionID}:${Date.now()}`,
        type: "episodic" as const,
        content: summary.slice(0, 500),
        summary,
        vector,
        metadata: {
          source: `session:${sessionID}`,
          timestamp: Date.now(),
          sessionID,
          tags: ["error"],
        },
      }
      await Store.upsert(memory)
      void Bus.publish(LTMEvent.MemoryStored, {
        id: memory.id,
        type: memory.type,
        source: memory.metadata.source,
      })
    } catch (e) {
      log.error("failed to store error memory", { error: e })
    }
  })
  unsubscribers.push(unsubError)

  log.info("bus subscriptions bound", { count: unsubscribers.length })
}

/** Conversation message → episodic memory */
export async function onMessageCompleted(
  sessionID: string,
  projectID: string | undefined,
  userMsg: string,
  assistantMsg: string,
): Promise<void> {
  if (!running || !config?.episodic.enabled) return

  const memory = await Episodic.fromConversation(sessionID, projectID, userMsg, assistantMsg)
  if (!memory) return

  await Store.upsert(memory)
  void Bus.publish(LTMEvent.MemoryStored, {
    id: memory.id,
    type: memory.type,
    source: memory.metadata.source,
  })
}

/** File change → semantic memory */
export async function onFileChanged(
  projectID: string,
  filePath: string,
  content: string,
): Promise<void> {
  if (!running || !config?.semantic.enabled || !config.semantic.indexOnFileChange) return

  // Check VRAM scheduler — can we run embedding?
  const canRun = await Scheduler.canRunEmbedding()
  if (!canRun) {
    log.info("skipping file indexing — VRAM budget exceeded", { filePath })
    return
  }

  // Split file into chunks (simple line-based)
  const lines = content.split("\n")
  const chunkSize = 50
  const chunks: Array<{ text: string; start: number; end: number }> = []

  for (let i = 0; i < lines.length; i += chunkSize) {
    const chunk = lines.slice(i, i + chunkSize).join("\n")
    if (chunk.trim().length > 20) {
      chunks.push({ text: chunk, start: i + 1, end: Math.min(i + chunkSize, lines.length) })
    }
  }

  for (const chunk of chunks) {
    const memory = await Semantic.fromCodeChunk(
      projectID,
      filePath,
      chunk.text,
      chunk.start,
      chunk.end,
    )
    if (memory) {
      await Store.upsert(memory)
      void Bus.publish(LTMEvent.MemoryStored, {
        id: memory.id,
        type: memory.type,
        source: memory.metadata.source,
      })
    }
  }

  log.info("indexed file", { filePath, chunks: chunks.length })
}

/** Code edit → procedural memory */
export async function onCodeEdit(
  projectID: string | undefined,
  filePath: string,
  content: string,
): Promise<void> {
  if (!running || !config?.procedural.enabled || !config.procedural.trackPreferences) return

  const signals = Procedural.extractStyleSignals(filePath, content)
  if (signals.length === 0) return

  const memories = await Procedural.fromStyleSignals(projectID, signals)
  for (const memory of memories) {
    await Store.upsert(memory)
  }
}