/**
 * OpenCode Lattice Plugin — Passive Data Capture
 *
 * A lightweight plugin that passively captures chat interactions.
 * The agent is unaware — data flows silently to store.db via `lattice ingest`.
 *
 * Design Notes:
 * - Uses two hooks: chat.message for user messages, event for assistant parts
 * - Only captures completed parts (time.end exists) to avoid duplicates
 * - Compresses tool output at capture time (Layer 0)
 * - Tool outputs are NOT stored — only status and errors
 * - Requires: uv tool install -e . (from project) or pip install lattice-memory
 * - spawn is async (non-blocking) — capture never causes UI jank
 * - Silent failure: data loss is acceptable, agent interruption is not
 * - System 1 injection and System 2 search are handled by the MCP server
 *
 * Reference: RFC-002-R1 §4.2 Plugin Interface (OpenCode)
 */

import type { Plugin, Hooks } from "@opencode-ai/plugin"
import type { Part, Event, ToolStateCompleted, ToolStateError } from "@opencode-ai/sdk"
import { spawn, execSync } from "child_process"
import { appendFileSync } from "fs"
import { homedir } from "os"
import { join } from "path"

const DEBUG_LOG = join(homedir(), ".lattice-debug.log")

// Cached executable info for lattice command discovery
let _executable: { cmd: string; baseArgs: string[] } | null = null

/**
 * Discover the lattice executable.
 * First tries `lattice` directly, then falls back to `uvx lattice-memory`.
 * Result is cached to avoid repeated checks.
 */
function getLatticeExecutable(): { cmd: string; baseArgs: string[] } {
  if (_executable) return _executable

  // Try lattice directly first
  try {
    execSync("lattice --help", { stdio: "ignore" })
    _executable = { cmd: "lattice", baseArgs: [] }
    return _executable
  } catch {
    // Fall back to uvx lattice-memory (package provides lattice-memory entry point)
    _executable = { cmd: "uvx", baseArgs: ["lattice-memory"] }
    return _executable
  }
}

function debug(message: string, data: unknown) {
  const timestamp = new Date().toISOString()
  const line = `[${timestamp}] ${message}: ${JSON.stringify(data)}\n`
  try {
    appendFileSync(DEBUG_LOG, line)
  } catch {}
}

interface EventPayload {
  type: "user" | "assistant" | "reasoning" | "tool"
  content: string
  tool_input?: string
  tool_status?: string
  tool_error?: string
  message_id?: string
  parent_id?: string
}

// Track which messageIDs are from user messages (to filter out from event hook)
// Using LRU-like cleanup to prevent unbounded growth
const userMessageIDs = new Set<string>()
const MAX_USER_MESSAGE_IDS = 1000

/**
 * Summarize tool input for storage.
 * Extracts file paths, commands, or other key identifiers.
 */
function summarize_input(tool: string, input: unknown): string {
  if (!input) return ""
  if (typeof input === "string") return input.slice(0, 100)

  const inp = input as Record<string, unknown>

  // Known tool input fields
  if (tool === "read" || tool === "edit" || tool === "write") {
    return String(inp.file_path || inp.path || "").slice(0, 100)
  }
  if (tool === "bash") {
    return String(inp.command || "").slice(0, 100)
  }
  if (tool === "glob" || tool === "grep") {
    return String(inp.pattern || "").slice(0, 100)
  }

  // Generic fallback
  for (const key of ["file_path", "path", "command", "query", "pattern", "url"]) {
    if (inp[key]) return String(inp[key]).slice(0, 100)
  }

  return ""
}

function send_events(sessionID: string, events: EventPayload[]) {
  if (events.length === 0) return

  const payload = JSON.stringify({
    session_id: sessionID,
    events,
  })

  const exec = getLatticeExecutable()
  const child = spawn(exec.cmd, [...exec.baseArgs, "ingest", "--stdin"], {
    stdio: ["pipe", "ignore", "ignore"],
  })
  child.stdin?.write(payload)
  child.stdin?.end()
  // Silent failure - no await, no error handling
}

function send_cleanup(sessionID: string, messageID: string) {
  const payload = JSON.stringify({
    session_id: sessionID,
    delete_message: messageID,
  })

  debug("sending cleanup", { sessionID, messageID })

  const exec = getLatticeExecutable()
  const child = spawn(exec.cmd, [...exec.baseArgs, "ingest", "--cleanup", "--stdin"], {
    stdio: ["pipe", "ignore", "ignore"],
  })
  child.stdin?.write(payload)
  child.stdin?.end()
}

const plugin: Plugin = async () => {
  return {
    // Hook 1: Capture user messages
    "chat.message": async (_input, output) => {
      const sessionID = _input.sessionID
      const messageID = output.message.id

      debug("chat.message", {
        sessionID,
        messageID,
        role: output.message.role,
      })

      // Track this messageID as a user message (with cleanup)
      userMessageIDs.add(messageID)
      if (userMessageIDs.size > MAX_USER_MESSAGE_IDS) {
        // Clear oldest half (simple cleanup strategy)
        const toDelete = [...userMessageIDs].slice(0, MAX_USER_MESSAGE_IDS / 2)
        toDelete.forEach((id) => userMessageIDs.delete(id))
      }

      // Extract events from parts
      const events: EventPayload[] = (output.parts || [])
        .map((part: Part): EventPayload | null => {
          if (part.type === "text") {
            return {
              type: "user",
              content: (part as { text: string }).text || "",
              message_id: messageID,
            }
          }
          return null
        })
        .filter((e): e is EventPayload => e !== null && e.content.length > 0)

      send_events(sessionID, events)
    },

    // Hook 2: Capture all events for debugging + assistant parts
    event: async (input) => {
      const event = input.event

      // DEBUG: Log all event types to understand revoke behavior
      debug("event", {
        type: event.type,
        properties: event.type === "message.removed"
          ? event.properties
          : event.type === "message.updated"
            ? { info: (event.properties as { info?: { id?: string; parentID?: string } }).info }
            : event.type === "message.part.updated"
              ? { partType: (event.properties as { part?: { type?: string; id?: string } }).part?.type }
              : undefined,
      })

      // Handle message.removed (revoke)
      if (event.type === "message.removed") {
        const props = event.properties as { sessionID: string; messageID: string }
        send_cleanup(props.sessionID, props.messageID)
        return
      }

      // Handle message.updated - track assistant message info
      if (event.type === "message.updated") {
        const props = event.properties as { info?: { id?: string; parentID?: string; role?: string } }
        debug("message.updated details", {
          id: props.info?.id,
          parentID: props.info?.parentID,
          role: props.info?.role,
        })
        return
      }

      // Handle message.part.updated for assistant parts
      if (event.type !== "message.part.updated") return

      const part = event.properties.part as Part & {
        messageID: string
        sessionID: string
      }

      // Skip if this is from a user message
      if (userMessageIDs.has(part.messageID)) {
        return
      }

      const events: EventPayload[] = []

      if (part.type === "text") {
        const textPart = part as {
          text: string
          time?: { start: number; end?: number }
          messageID: string
        }
        // Only capture when complete (has end time)
        if (textPart.time?.end && textPart.text) {
          events.push({
            type: "assistant",
            content: textPart.text,
            message_id: textPart.messageID,
          })
        }
      } else if (part.type === "reasoning") {
        const reasoningPart = part as {
          text: string
          time?: { start: number; end?: number }
          messageID: string
        }
        // Only capture when complete (has end time)
        if (reasoningPart.time?.end && reasoningPart.text) {
          events.push({
            type: "reasoning",
            content: reasoningPart.text,
            message_id: reasoningPart.messageID,
          })
        }
      } else if (part.type === "tool") {
        const toolPart = part as {
          tool: string
          messageID: string
          state: ToolStateCompleted | ToolStateError | { status: string; input?: unknown }
        }
        const state = toolPart.state

        // Only capture completed or error tool calls
        if (state.status === "completed" || state.status === "error") {
          events.push({
            type: "tool",
            content: toolPart.tool,
            tool_input: summarize_input(toolPart.tool, state.input),
            tool_status: state.status,
            tool_error:
              state.status === "error"
                ? (state as ToolStateError).error?.slice(0, 500)
                : undefined,
            message_id: toolPart.messageID,
          })
        }
      }

      send_events(part.sessionID, events)
    },
  } as Hooks
}

export default plugin