import type { SQLiteBunDatabase } from "drizzle-orm/bun-sqlite"
import type { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite"
import { Global } from "@opencode-ai/core/global"
import { Log } from "../util"
import { ProjectTable } from "../project/project.sql"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../session/session.sql"
import { SessionShareTable } from "../share/share.sql"
import path from "path"
import { existsSync } from "fs"
import { Filesystem } from "../util"
import { Glob } from "@opencode-ai/core/util/glob"

const log = Log.create({ service: "json-migration" })

export type Progress = {
  current: number
  total: number
  label: string
}

type Options = {
  progress?: (event: Progress) => void
}

export async function run(db: SQLiteBunDatabase<any, any> | NodeSQLiteDatabase<any, any>, options?: Options) {
  const storageDir = path.join(Global.Path.data, "storage")

  if (!existsSync(storageDir)) {
    log.info("storage directory does not exist, skipping migration")
    return {
      projects: 0,
      sessions: 0,
      messages: 0,
      parts: 0,
      todos: 0,
      permissions: 0,
      shares: 0,
      errors: [] as string[],
    }
  }

  log.info("starting json to sqlite migration", { storageDir })
  const start = performance.now()

  // const db = drizzle({ client: sqlite })

  // Optimize SQLite for bulk inserts
  db.run("PRAGMA journal_mode = WAL")
  db.run("PRAGMA synchronous = OFF")
  db.run("PRAGMA cache_size = 10000")
  db.run("PRAGMA temp_store = MEMORY")
  const stats = {
    projects: 0,
    sessions: 0,
    messages: 0,
    parts: 0,
    todos: 0,
    permissions: 0,
    shares: 0,
    errors: [] as string[],
  }
  const orphans = {
    sessions: 0,
    todos: 0,
    permissions: 0,
    shares: 0,
  }
  const errs = stats.errors

  const batchSize = 1000
  const now = Date.now()

  async function list(pattern: string) {
    return Glob.scan(pattern, { cwd: storageDir, absolute: true })
  }

  async function read(files: string[], start: number, end: number) {
    const count = end - start
    // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill
    const tasks = new Array(count)
    for (let i = 0; i < count; i++) {
      tasks[i] = Filesystem.readJson(files[start + i])
    }
    const results = await Promise.allSettled(tasks)
    // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill
    const items = new Array(count)
    for (let i = 0; i < results.length; i++) {
      const result = results[i]
      if (result.status === "fulfilled") {
        items[i] = result.value
        continue
      }
      errs.push(`failed to read ${files[start + i]}: ${result.reason}`)
    }
    return items
  }

  function insert(values: unknown[], table: Parameters<typeof db.insert>[0], label: string) {
    if (values.length === 0) return 0
    try {
      db.insert(table).values(values).onConflictDoNothing().run()
      return values.length
    } catch (e) {
      errs.push(`failed to migrate ${label} batch: ${e}`)
      return 0
    }
  }

  // Pre-scan all files upfront to avoid repeated glob operations
  log.info("scanning files...")
  const [projectFiles, sessionFiles, messageFiles, partFiles, todoFiles, permFiles, shareFiles] = await Promise.all([
    list("project/*.json"),
    list("session/*/*.json"),
    list("message/*/*.json"),
    list("part/*/*.json"),
    list("todo/*.json"),
    list("permission/*.json"),
    list("session_share/*.json"),
  ])

  log.info("file scan complete", {
    projects: projectFiles.length,
    sessions: sessionFiles.length,
    messages: messageFiles.length,
    parts: partFiles.length,
    todos: todoFiles.length,
    permissions: permFiles.length,
    shares: shareFiles.length,
  })

  const total = Math.max(
    1,
    projectFiles.length +
      sessionFiles.length +
      messageFiles.length +
      partFiles.length +
      todoFiles.length +
      permFiles.length +
      shareFiles.length,
  )
  const progress = options?.progress
  let current = 0
  const step = (label: string, count: number) => {
    current = Math.min(total, current + count)
    progress?.({ current, total, label })
  }

  progress?.({ current, total, label: "starting" })

  db.run("BEGIN TRANSACTION")

  // Migrate projects first (no FK deps)
  // Derive all IDs from file paths, not JSON content
  const projectIds = new Set<string>()
  const projectValues: unknown[] = []
  for (let i = 0; i < projectFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, projectFiles.length)
    const batch = await read(projectFiles, i, end)
    projectValues.length = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const id = path.basename(projectFiles[i + j], ".json")
      projectIds.add(id)
      projectValues.push({
        id,
        worktree: data.worktree ?? "/",
        vcs: data.vcs,
        name: data.name ?? undefined,
        icon_url: data.icon?.url,
        icon_url_override: data.icon?.override,
        icon_color: data.icon?.color,
        time_created: data.time?.created ?? now,
        time_updated: data.time?.updated ?? now,
        time_initialized: data.time?.initialized,
        sandboxes: data.sandboxes ?? [],
        commands: data.commands,
      })
    }
    stats.projects += insert(projectValues, ProjectTable, "project")
    step("projects", end - i)
  }
  log.info("migrated projects", { count: stats.projects, duration: Math.round(performance.now() - start) })

  // Migrate sessions (depends on projects)
  // Derive all IDs from directory/file paths, not JSON content, since earlier
  // migrations may have moved sessions to new directories without updating the JSON
  const sessionProjects = sessionFiles.map((file) => path.basename(path.dirname(file)))
  const sessionIds = new Set<string>()
  const sessionValues: unknown[] = []
  for (let i = 0; i < sessionFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, sessionFiles.length)
    const batch = await read(sessionFiles, i, end)
    sessionValues.length = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const id = path.basename(sessionFiles[i + j], ".json")
      const projectID = sessionProjects[i + j]
      if (!projectIds.has(projectID)) {
        orphans.sessions++
        continue
      }
      sessionIds.add(id)
      sessionValues.push({
        id,
        project_id: projectID,
        parent_id: data.parentID ?? null,
        slug: data.slug ?? "",
        directory: data.directory ?? "",
        title: data.title ?? "",
        version: data.version ?? "",
        share_url: data.share?.url ?? null,
        summary_additions: data.summary?.additions ?? null,
        summary_deletions: data.summary?.deletions ?? null,
        summary_files: data.summary?.files ?? null,
        summary_diffs: data.summary?.diffs ?? null,
        revert: data.revert ?? null,
        permission: data.permission ?? null,
        time_created: data.time?.created ?? now,
        time_updated: data.time?.updated ?? now,
        time_compacting: data.time?.compacting ?? null,
        time_archived: data.time?.archived ?? null,
      })
    }
    stats.sessions += insert(sessionValues, SessionTable, "session")
    step("sessions", end - i)
  }
  log.info("migrated sessions", { count: stats.sessions })
  if (orphans.sessions > 0) {
    log.warn("skipped orphaned sessions", { count: orphans.sessions })
  }

  // Migrate messages using pre-scanned file map
  const allMessageFiles = [] as string[]
  const allMessageSessions = [] as string[]
  const messageSessions = new Map<string, string>()
  for (const file of messageFiles) {
    const sessionID = path.basename(path.dirname(file))
    if (!sessionIds.has(sessionID)) continue
    allMessageFiles.push(file)
    allMessageSessions.push(sessionID)
  }

  for (let i = 0; i < allMessageFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, allMessageFiles.length)
    const batch = await read(allMessageFiles, i, end)
    // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill
    const values = new Array(batch.length)
    let count = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const file = allMessageFiles[i + j]
      const id = path.basename(file, ".json")
      const sessionID = allMessageSessions[i + j]
      messageSessions.set(id, sessionID)
      const rest = data
      delete rest.id
      delete rest.sessionID
      values[count++] = {
        id,
        session_id: sessionID,
        time_created: data.time?.created ?? now,
        time_updated: data.time?.updated ?? now,
        data: rest,
      }
    }
    values.length = count
    stats.messages += insert(values, MessageTable, "message")
    step("messages", end - i)
  }
  log.info("migrated messages", { count: stats.messages })

  // Migrate parts using pre-scanned file map
  for (let i = 0; i < partFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, partFiles.length)
    const batch = await read(partFiles, i, end)
    // oxlint-disable-next-line unicorn/no-new-array -- pre-allocated for index-based batch fill
    const values = new Array(batch.length)
    let count = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const file = partFiles[i + j]
      const id = path.basename(file, ".json")
      const messageID = path.basename(path.dirname(file))
      const sessionID = messageSessions.get(messageID)
      if (!sessionID) {
        errs.push(`part missing message session: ${file}`)
        continue
      }
      if (!sessionIds.has(sessionID)) continue
      const rest = data
      delete rest.id
      delete rest.messageID
      delete rest.sessionID
      values[count++] = {
        id,
        message_id: messageID,
        session_id: sessionID,
        time_created: data.time?.created ?? now,
        time_updated: data.time?.updated ?? now,
        data: rest,
      }
    }
    values.length = count
    stats.parts += insert(values, PartTable, "part")
    step("parts", end - i)
  }
  log.info("migrated parts", { count: stats.parts })

  // Migrate todos
  const todoSessions = todoFiles.map((file) => path.basename(file, ".json"))
  for (let i = 0; i < todoFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, todoFiles.length)
    const batch = await read(todoFiles, i, end)
    const values: unknown[] = []
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const sessionID = todoSessions[i + j]
      if (!sessionIds.has(sessionID)) {
        orphans.todos++
        continue
      }
      if (!Array.isArray(data)) {
        errs.push(`todo not an array: ${todoFiles[i + j]}`)
        continue
      }
      for (let position = 0; position < data.length; position++) {
        const todo = data[position]
        if (!todo?.content || !todo?.status || !todo?.priority) continue
        values.push({
          session_id: sessionID,
          content: todo.content,
          status: todo.status,
          priority: todo.priority,
          position,
          time_created: now,
          time_updated: now,
        })
      }
    }
    stats.todos += insert(values, TodoTable, "todo")
    step("todos", end - i)
  }
  log.info("migrated todos", { count: stats.todos })
  if (orphans.todos > 0) {
    log.warn("skipped orphaned todos", { count: orphans.todos })
  }

  // Migrate permissions
  const permProjects = permFiles.map((file) => path.basename(file, ".json"))
  const permValues: unknown[] = []
  for (let i = 0; i < permFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, permFiles.length)
    const batch = await read(permFiles, i, end)
    permValues.length = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const projectID = permProjects[i + j]
      if (!projectIds.has(projectID)) {
        orphans.permissions++
        continue
      }
      permValues.push({ project_id: projectID, data })
    }
    stats.permissions += insert(permValues, PermissionTable, "permission")
    step("permissions", end - i)
  }
  log.info("migrated permissions", { count: stats.permissions })
  if (orphans.permissions > 0) {
    log.warn("skipped orphaned permissions", { count: orphans.permissions })
  }

  // Migrate session shares
  const shareSessions = shareFiles.map((file) => path.basename(file, ".json"))
  const shareValues: unknown[] = []
  for (let i = 0; i < shareFiles.length; i += batchSize) {
    const end = Math.min(i + batchSize, shareFiles.length)
    const batch = await read(shareFiles, i, end)
    shareValues.length = 0
    for (let j = 0; j < batch.length; j++) {
      const data = batch[j]
      if (!data) continue
      const sessionID = shareSessions[i + j]
      if (!sessionIds.has(sessionID)) {
        orphans.shares++
        continue
      }
      if (!data?.id || !data?.secret || !data?.url) {
        errs.push(`session_share missing id/secret/url: ${shareFiles[i + j]}`)
        continue
      }
      shareValues.push({ session_id: sessionID, id: data.id, secret: data.secret, url: data.url })
    }
    stats.shares += insert(shareValues, SessionShareTable, "session_share")
    step("shares", end - i)
  }
  log.info("migrated session shares", { count: stats.shares })
  if (orphans.shares > 0) {
    log.warn("skipped orphaned session shares", { count: orphans.shares })
  }

  db.run("COMMIT")

  log.info("json migration complete", {
    projects: stats.projects,
    sessions: stats.sessions,
    messages: stats.messages,
    parts: stats.parts,
    todos: stats.todos,
    permissions: stats.permissions,
    shares: stats.shares,
    errorCount: stats.errors.length,
    duration: Math.round(performance.now() - start),
  })

  if (stats.errors.length > 0) {
    log.warn("migration errors", { errors: stats.errors.slice(0, 20) })
  }

  progress?.({ current: total, total, label: "complete" })

  return stats
}
