import z from "zod"
import type { SQL } from "@/storage/db"
import { Database, eq, and, gte, isNull, desc, like, inArray, lt, or } from "@/storage/db"
import { ProjectTable } from "@/project/project.sql"
import { ProjectID } from "@/project/schema"
import { Filesystem } from "@/util/filesystem"
import { SessionTable } from "@/session/session.sql"
import * as Log from "@saeeol/core/util/log"
import type { LanguageModelUsage, ProviderMetadata } from "ai"
import type { Provider } from "@/provider/provider"

const log = Log.create({ service: "session.saeeol" })

function family(id: string): string[] {
  const row = Database.use((db) =>
    db
      .select({ worktree: ProjectTable.worktree })
      .from(ProjectTable)
      .where(eq(ProjectTable.id, ProjectID.make(id)))
      .get(),
  )
  const root = row?.worktree ? Filesystem.resolve(row.worktree) : undefined
  if (!root || root === "/") return [id]
  const ids = Database.use((db) =>
    db
      .select({ id: ProjectTable.id })
      .from(ProjectTable)
      .where(eq(ProjectTable.worktree, root))
      .all()
      .map((item) => item.id),
  )
  return ids.length ? ids : [id]
}

function filters(input: { projectID: ProjectID; directory?: string }): SQL[] {
  const dir = input.directory ? Filesystem.resolve(input.directory) : undefined
  if (!dir) return [eq(SessionTable.project_id, input.projectID)]
  return [
    or(eq(SessionTable.project_id, input.projectID), eq(SessionTable.directory, dir)),
    eq(SessionTable.directory, dir),
  ].filter((item): item is SQL => item !== undefined)
}

function providerCost(input: {
  metadata?: ProviderMetadata
  usage?: LanguageModelUsage
  provider?: Provider.Info
  providerID: string
}): number | undefined {
  const isSaeeol = (input.provider?.id ?? input.providerID) === "saeeol"

  const num = (value: unknown): number | undefined => {
    if (value === undefined || value === null) return undefined
    const n = typeof value === "string" ? Number(value) : (value as number)
    return Number.isFinite(n) ? n : undefined
  }

  const orUsage = input.metadata?.["openrouter"]?.["usage"] as
    | { cost?: number; costDetails?: { upstreamInferenceCost?: number } }
    | undefined
  if (orUsage) {
    const upstream = num(orUsage.costDetails?.upstreamInferenceCost)
    const regular = num(orUsage.cost)
    const cost = isSaeeol && upstream !== undefined ? upstream : regular
    if (cost !== undefined) return cost
  }

  const raw = input.usage?.raw as { cost_details?: { upstream_inference_cost?: number } } | undefined
  const upstream = num(raw?.cost_details?.upstream_inference_cost)
  if (upstream !== undefined) return upstream

  const gateway = input.metadata?.["gateway"] as { marketCost?: string | number } | undefined
  const marketCost = num(gateway?.marketCost)
  if (marketCost !== undefined) return marketCost

  return undefined
}

function runSyncSafe(run: () => void, context: { type: string; id: string; sessionID: string }): void {
  try {
    run()
  } catch (e: any) {
    if (e?.code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
      log.warn(`skipping ${context.type} for deleted session`, { id: context.id, sessionID: context.sessionID })
      return
    }
    throw e
  }
}

const ProjectInfo = z
  .object({
    id: ProjectID.zod,
    name: z.string().optional(),
    worktree: z.string(),
  })
  .meta({ ref: "ProjectSummary" })
type ProjectInfo = z.output<typeof ProjectInfo>

type SessionRow = typeof SessionTable.$inferSelect

function* listGlobal<T extends { time: { updated: number }; project?: ProjectInfo | null }>(input: {
  fromRow: (row: SessionRow) => Omit<T, "project">
  projectID?: string
  directory?: string
  directories?: string[]
  roots?: boolean
  start?: number
  cursor?: number
  search?: string
  limit?: number
  archived?: boolean
}) {
  const conditions: SQL[] = []

  if (input.projectID) {
    const ids = family(input.projectID)
    if (ids.length === 1 && ids[0] === input.projectID) {
      conditions.push(eq(SessionTable.project_id, ProjectID.make(input.projectID)))
    } else {
      conditions.push(
        inArray(
          SessionTable.project_id,
          ids.map((id) => ProjectID.make(id)),
        ),
      )
    }
  }

  if (input.directory) {
    conditions.push(eq(SessionTable.directory, Filesystem.resolve(input.directory)))
  }
  if (input.roots) {
    conditions.push(isNull(SessionTable.parent_id))
  }
  if (input.start) {
    conditions.push(gte(SessionTable.time_updated, input.start))
  }
  if (input.cursor) {
    conditions.push(lt(SessionTable.time_updated, input.cursor))
  }
  if (input.search) {
    conditions.push(like(SessionTable.title, `%${input.search}%`))
  }
  if (!input.archived) {
    conditions.push(isNull(SessionTable.time_archived))
  }

  const limit = input.limit ?? 100
  const dirs = [...new Set((input.directories ?? []).map((dir) => Filesystem.resolve(dir)))]

  const rows = Database.use((db) => {
    const query =
      conditions.length > 0
        ? db
            .select()
            .from(SessionTable)
            .where(and(...conditions))
        : db.select().from(SessionTable)
    const sorted = query.orderBy(desc(SessionTable.time_updated), desc(SessionTable.id))
    return dirs.length ? sorted.all() : sorted.limit(limit).all()
  })

  const list =
    dirs.length > 0
      ? rows.filter((row) => {
          const dir = Filesystem.resolve(row.directory)
          return dirs.some((root) => Filesystem.contains(root, dir))
        })
      : rows

  const ids = [...new Set(list.slice(0, limit).map((row) => row.project_id))]
  const projects = new Map<string, ProjectInfo>()

  if (ids.length > 0) {
    const items = Database.use((db) =>
      db
        .select({ id: ProjectTable.id, name: ProjectTable.name, worktree: ProjectTable.worktree })
        .from(ProjectTable)
        .where(inArray(ProjectTable.id, ids))
        .all(),
    )
    for (const item of items) {
      projects.set(item.id, {
        id: item.id,
        name: item.name ?? undefined,
        worktree: item.worktree,
      })
    }
  }

  for (const row of list.slice(0, limit)) {
    const project = projects.get(row.project_id) ?? null
    yield { ...input.fromRow(row), project } as T & { project: ProjectInfo | null }
  }
}

export { family, filters, providerCost, runSyncSafe, ProjectInfo, listGlobal, log }
export type { ProjectInfo as ProjectInfoType, SessionRow }
