/** toModelMessages + metadata stripping + DB hydration — extracted from message-v2.ts */

import { convertToModelMessages, type UIMessage } from "ai"
import { Effect } from "effect"
import { and, desc, eq, inArray, lt, or } from "drizzle-orm"
import { Database } from "@/storage/db"
import { MessageTable, PartTable } from "../core/session.sql"
import { Snapshot } from "@/snapshot"
import { iife } from "@/util/iife"
import { isMedia } from "@/util/media"
import { MessageID, SessionID } from "../core/schema"
import * as EffectLogger from "@saeeol/core/effect/logger"
import type { Provider } from "@/provider/provider"
export const SYNTHETIC_ATTACHMENT_PROMPT = "Attached image(s) from tool result:"
import type { Info, User, WithParts } from "./message-types"
import type { Part, CompactionPart, ToolState, ToolStateCompleted } from "./message-parts"
import { truncateToolOutput } from "./message-parts"
import { AbortedError } from "./message-errors"

export { isMedia }

function stripPatch(value: unknown) {
  if (typeof value !== "string") return undefined
  if (Buffer.byteLength(value) > Snapshot.MAX_DIFF_SIZE) return undefined
  return value
}
function withPatch(value: unknown) { const kept = stripPatch(value); return kept ? { patch: kept } : {} }

export function stripPartMetadata(part: Part): Part {
  if (part.type !== "tool") return part
  const { state } = part
  if (state.status !== "completed" && state.status !== "running") return part
  const meta = state.metadata
  if (!meta) return part
  let changed = false
  let next = meta
  if (meta.diff !== undefined) { const { diff, ...rest } = next; next = rest; changed = true }
  if (meta.filediff) { const { before, after, patch, ...rest } = meta.filediff; next = { ...next, filediff: { ...rest, ...withPatch(patch) } }; changed = true }
  if (Array.isArray(meta.files) && meta.files.length > 0) { next = { ...next, files: meta.files.map((f: Record<string, unknown>) => { const { before, after, patch, diff, ...rest } = f; const kept = stripPatch(patch) ?? stripPatch(diff); return { ...rest, ...(kept ? { patch: kept } : {}) } }) }; changed = true }
  if (Array.isArray(meta.results) && meta.results.length > 0) { next = { ...next, results: meta.results.map((r: Record<string, unknown>) => { const { diff, ...rest } = r; if (!r.filediff || typeof r.filediff !== "object") return rest; const fd = r.filediff as Record<string, unknown>; const { before, after, patch, ...file } = fd; return { ...rest, filediff: { ...file, ...withPatch(patch) } } }) }; changed = true }
  if (!changed) return part
  return { ...part, state: { ...state, metadata: next } } as Part
}

export function stripMessageMetadata(info: Info): Info {
  if (info.role !== "user") return info
  const user = info as User
  if (!user.summary?.diffs?.length) return info
  const oversized = (d: Snapshot.FileDiff) => d.patch && Buffer.byteLength(d.patch) > Snapshot.MAX_DIFF_SIZE
  if (!user.summary.diffs.some(oversized)) return info
  return { ...user, summary: { ...user.summary, diffs: user.summary.diffs.map((d: Snapshot.FileDiff) => (oversized(d) ? { ...d, patch: "" } : d)) } } as Info
}

export const toInfo = (row: typeof MessageTable.$inferSelect) => stripMessageMetadata({ ...row.data, id: row.id, sessionID: row.session_id } as Info)
const toPart = (row: typeof PartTable.$inferSelect) => stripPartMetadata({ ...row.data, id: row.id, sessionID: row.session_id, messageID: row.message_id } as Part)

export const older = (row: { id: MessageID; time: number }) =>
  or(lt(MessageTable.time_created, row.time), and(eq(MessageTable.time_created, row.time), lt(MessageTable.id, row.id)))

export function hydrate(rows: (typeof MessageTable.$inferSelect)[]) {
  const ids = rows.map((row) => row.id)
  const partByMessage = new Map<string, Part[]>()
  if (ids.length > 0) {
    const partRows = Database.use((db) => db.select().from(PartTable).where(inArray(PartTable.message_id, ids)).orderBy(PartTable.message_id, PartTable.id).all())
    for (const row of partRows) { const next = toPart(row); const list = partByMessage.get(row.message_id); if (list) list.push(next); else partByMessage.set(row.message_id, [next]) }
  }
  return rows.map((row) => ({ info: toInfo(row), parts: partByMessage.get(row.id) ?? [] }))
}

function providerMeta(metadata: Record<string, any> | undefined) {
  if (!metadata) return undefined
  const { providerExecuted: _, ...rest } = metadata
  return Object.keys(rest).length > 0 ? rest : undefined
}

export const toModelMessagesEffect = Effect.fnUntraced(function* (input: WithParts[], model: Provider.Model, options?: { stripMedia?: boolean; toolOutputMaxChars?: number }) {
  const result: UIMessage[] = []
  const toolNames = new Set<string>()
  const supportsMediaInToolResults = (() => {
    if (model.api.npm === "@ai-sdk/anthropic") return true
    if (model.api.npm === "@ai-sdk/openai") return true
    if (model.api.npm === "@ai-sdk/amazon-bedrock") return true
    if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
    if (model.api.npm === "@ai-sdk/google") { const id = model.api.id.toLowerCase(); return id.includes("gemini-3") && !id.includes("gemini-2") }
    return false
  })()
  const toModelOutput = (opts: { toolCallId: string; input: unknown; output: unknown }) => {
    const output = opts.output
    if (typeof output === "string") return { type: "text", value: output }
    if (typeof output === "object") {
      const o = output as { text: string; attachments?: Array<{ mime: string; url: string }> }
      const attachments = (o.attachments ?? []).filter((a) => a.url.startsWith("data:") && a.url.includes(","))
      return { type: "content", value: [...(o.text ? [{ type: "text", text: o.text }] : []), ...attachments.map((a) => ({ type: "media", mediaType: a.mime, data: iife(() => { const ci = a.url.indexOf(","); return ci === -1 ? a.url : a.url.slice(ci + 1) }) }))] }
    }
    return { type: "json", value: output as never }
  }
  for (const msg of input) {
    if (msg.parts.length === 0) continue
    if (msg.info.role === "user") {
      const userMessage: UIMessage = { id: msg.info.id, role: "user", parts: [] }
      result.push(userMessage)
      for (const part of msg.parts) {
        if (part.type === "text" && !part.ignored) userMessage.parts.push({ type: "text", text: part.text })
        if (part.type === "file" && part.mime !== "text/plain" && part.mime !== "application/x-directory") {
          if (options?.stripMedia && isMedia(part.mime)) userMessage.parts.push({ type: "text", text: `[Attached ${part.mime}: ${part.filename ?? "file"}]` })
          else userMessage.parts.push({ type: "file", url: part.url, mediaType: part.mime, filename: part.filename })
        }
        if (part.type === "compaction") userMessage.parts.push({ type: "text", text: "What did we do so far?" })
        if (part.type === "subtask") userMessage.parts.push({ type: "text", text: "The following tool was executed by the user" })
      }
    }
    if (msg.info.role === "assistant") {
      const differentModel = `${model.providerID}/${model.id}` !== `${msg.info.providerID}/${msg.info.modelID}`
      const media: Array<{ mime: string; url: string }> = []
      if (msg.info.error && !(AbortedError.isInstance(msg.info.error) && msg.parts.some((p) => p.type !== "step-start" && p.type !== "reasoning"))) continue
      const assistantMessage: UIMessage = { id: msg.info.id, role: "assistant", parts: [] }
      for (const part of msg.parts) {
        if (part.type === "text" && !part.ignored) assistantMessage.parts.push({ type: "text", text: part.text, ...(differentModel ? {} : { providerMetadata: part.metadata }) })
        if (part.type === "step-start") assistantMessage.parts.push({ type: "step-start" })
        if (part.type === "tool") {
          toolNames.add(part.tool)
          if (part.state.status === "completed") {
            const outputText = part.state.time.compacted ? "[Old tool result content cleared]" : truncateToolOutput(part.state.output, options?.toolOutputMaxChars)
            const attachments = part.state.time.compacted || options?.stripMedia ? [] : (part.state.attachments ?? [])
            const mediaAttachments = attachments.filter((a) => isMedia(a.mime))
            const nonMediaAttachments = attachments.filter((a) => !isMedia(a.mime))
            if (!supportsMediaInToolResults && mediaAttachments.length > 0) media.push(...mediaAttachments)
            const finalAttachments = supportsMediaInToolResults ? attachments : nonMediaAttachments
            const output = finalAttachments.length > 0 ? { text: outputText, attachments: finalAttachments } : outputText
            assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-available", toolCallId: part.callID, input: part.state.input, output, ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }) })
          }
          if (part.state.status === "error") {
            const output = part.state.metadata?.interrupted === true ? part.state.metadata.output : undefined
            if (typeof output === "string") assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-available", toolCallId: part.callID, input: part.state.input, output, ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }) })
            else assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-error", toolCallId: part.callID, input: part.state.input, errorText: part.state.error, ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }) })
          }
          if (part.state.status === "pending" || part.state.status === "running") assistantMessage.parts.push({ type: ("tool-" + part.tool) as `tool-${string}`, state: "output-error", toolCallId: part.callID, input: part.state.input, errorText: "[Tool execution was interrupted]", ...(part.metadata?.providerExecuted ? { providerExecuted: true } : {}), ...(differentModel ? {} : { callProviderMetadata: providerMeta(part.metadata) }) })
        }
        if (part.type === "reasoning") {
          if (differentModel) { if (part.text.trim().length > 0) assistantMessage.parts.push({ type: "text", text: part.text }); continue }
          assistantMessage.parts.push({ type: "reasoning", text: part.text, providerMetadata: part.metadata })
        }
      }
      if (assistantMessage.parts.length > 0) {
        result.push(assistantMessage)
        if (media.length > 0) result.push({ id: MessageID.ascending(), role: "user", parts: [{ type: "text" as const, text: SYNTHETIC_ATTACHMENT_PROMPT }, ...media.map((a) => ({ type: "file" as const, url: a.url, mediaType: a.mime }))] })
      }
    }
  }
  const tools = Object.fromEntries(Array.from(toolNames).map((n) => [n, { toModelOutput }]))
  return yield* Effect.promise(() => convertToModelMessages(result.filter((m) => m.parts.some((p) => p.type !== "step-start")), { tools } as any))
})

export function toModelMessages(input: WithParts[], model: Provider.Model, options?: { stripMedia?: boolean; toolOutputMaxChars?: number }): Promise<import("ai").ModelMessage[]> {
  return Effect.runPromise(toModelMessagesEffect(input, model, options).pipe(Effect.provide(EffectLogger.layer)))
}
