import type { Session, Provider, Model } from "../types/session"
import type { Message } from "../types/message"

import { instances } from "./instances"
import { nativeSessionApi } from "../lib/lite-mode"
import { needsMigration, autoImportCachedSessions, markMigrated, cacheSDKSessions } from "./session-migration"
import { preferences, setAgentModelPreference, getAgentModelPreference } from "./preferences"
import { setSessionCompactionState } from "./session-compaction"
import {
  activeSessionId,
  agents,
  clearSessionDraftPrompt,
  getChildSessions,
  isBlankSession,
  messagesLoaded,
  pruneDraftPrompts,
  providers,
  setActiveSessionId,
  setAgents,
  setMessagesLoaded,
  setProviders,
  setSessionInfoByInstance,
  setSessions,
  sessions,
  loading,
  setLoading,
  cleanupBlankSessions,
} from "./session-state"
import { getInstanceConfig, ensureInstanceConfigLoaded } from "./instance-config"
import { DEFAULT_MODEL_OUTPUT_LIMIT, getDefaultModel, isModelValid } from "./session-models"
import { normalizeMessagePart } from "./message-v2/normalizers"
import { updateSessionInfo } from "./message-v2/session-info"
import { seedSessionMessagesV2 } from "./message-v2/bridge"
import { messageStoreBus } from "./message-v2/bus"
import { clearCacheForSession } from "../lib/global-cache"
import { getLogger } from "../lib/logger"
import { showToastNotification } from "../lib/notifications"
import { getUserScopedKey } from "../lib/user-storage"

const log = getLogger("api")

type ProviderMap = Map<string, Provider>
export const ANTIGRAVITY_MODEL_IDS = new Set([
  "gemini-3-pro-low",
  "gemini-3-pro-high",
  "gemini-3-flash",
  "claude-sonnet-4-5",
  "claude-sonnet-4-5-thinking-low",
  "claude-sonnet-4-5-thinking-medium",
  "claude-sonnet-4-5-thinking-high",
  "claude-opus-4-5-thinking-low",
  "claude-opus-4-5-thinking-medium",
  "claude-opus-4-5-thinking-high",
  "gpt-oss-120b-medium",
])
const ANTIGRAVITY_PROJECT_KEY = "antigravity_project_id"

async function fetchJson<T>(url: string): Promise<T | null> {
  try {
    const response = await fetch(url)
    if (!response.ok) return null
    return (await response.json()) as T
  } catch (error) {
    log.warn("Failed to fetch provider data", { url, error })
    return null
  }
}

function mergeProviders(base: Provider[], extras: Provider[]): Provider[] {
  if (extras.length === 0) return base
  const map: ProviderMap = new Map(base.map((provider) => [provider.id, { ...provider }]))

  for (const extra of extras) {
    const existing = map.get(extra.id)
    if (!existing) {
      map.set(extra.id, extra)
      continue
    }

    const modelMap = new Map<string, Model>(existing.models.map((model) => [model.id, model]))
    for (const model of extra.models) {
      if (!modelMap.has(model.id)) {
        modelMap.set(model.id, model)
      }
    }
    existing.models = Array.from(modelMap.values())
  }

  return Array.from(map.values())
}

const OLLAMA_TOAST_COOLDOWN_MS = 30000
let lastOllamaToastAt = 0
let lastOllamaToastKey = ""

function showOllamaToastOnce(key: string, payload: Parameters<typeof showToastNotification>[0]) {
  const now = Date.now()
  if (lastOllamaToastKey === key && now - lastOllamaToastAt < OLLAMA_TOAST_COOLDOWN_MS) {
    return
  }
  lastOllamaToastKey = key
  lastOllamaToastAt = now
  showToastNotification(payload)
}

async function fetchOllamaCloudProvider(): Promise<Provider | null> {
  try {
    const config = await fetchJson<{ config?: { enabled?: boolean } }>("/api/ollama/config")
    if (config && config.config?.enabled === false) {
      return null
    }

    const response = await fetch("/api/ollama/models")
    if (!response.ok) {
      const errorText = await response.text().catch(() => "")
      showOllamaToastOnce(`ollama-unavailable-${response.status}`, {
        title: "Ollama Cloud unavailable",
        message: errorText
          ? `Unable to load Ollama Cloud models (${response.status}). ${errorText}`
          : "Unable to load Ollama Cloud models. Check that the integration is enabled and the API key is valid.",
        variant: "warning",
        duration: 8000,
      })
      return null
    }
    const data = (await response.json()) as { models?: Array<{ name?: string } | string> }
    const models = Array.isArray(data?.models) ? data.models : []
    const modelNames = models
      .map((model) => (typeof model === "string" ? model : model?.name))
      .filter((name): name is string => Boolean(name))
    if (modelNames.length === 0) {
      showOllamaToastOnce("ollama-empty", {
        title: "Ollama Cloud models unavailable",
        message: "Ollama Cloud returned no models. Check your API key and endpoint.",
        variant: "warning",
        duration: 8000,
      })
      return null
    }

    return {
      id: "ollama-cloud",
      name: "Ollama Cloud",
      models: modelNames.map((name) => ({
        id: name,
        name,
        providerId: "ollama-cloud",
      })),
    }
  } catch (error) {
    log.warn("Failed to fetch Ollama Cloud models", { error })
    showOllamaToastOnce("ollama-fetch-error", {
      title: "Ollama Cloud unavailable",
      message: "Unable to load Ollama Cloud models. Check that the integration is enabled and the API key is valid.",
      variant: "warning",
      duration: 8000,
    })
    return null
  }
}

function getStoredQwenToken():
  | { access_token: string; expires_in: number; created_at: number }
  | null {
  if (typeof window === "undefined") return null
  try {
    const raw = window.localStorage.getItem(getUserScopedKey("qwen_oauth_token"))
    if (!raw) return null
    return JSON.parse(raw)
  } catch {
    return null
  }
}

function isQwenTokenValid(token: { expires_in: number; created_at: number } | null): boolean {
  if (!token) return false
  const createdAt = token.created_at > 1e12 ? Math.floor(token.created_at / 1000) : token.created_at
  const expiresAt = (createdAt + token.expires_in) * 1000 - 300000
  return Date.now() < expiresAt
}

async function fetchQwenOAuthProvider(): Promise<Provider | null> {
  const token = getStoredQwenToken()
  if (!isQwenTokenValid(token)) return null

  // Use actual Qwen model IDs that work with the DashScope API
  const qwenModels: Model[] = [
    {
      id: "qwen-coder-plus-latest",
      name: "Qwen Coder Plus (OAuth)",
      providerId: "qwen-oauth",
      limit: { context: 131072, output: 16384 },
    },
    {
      id: "qwen-turbo-latest",
      name: "Qwen Turbo (OAuth)",
      providerId: "qwen-oauth",
      limit: { context: 131072, output: 8192 },
    },
    {
      id: "qwen-plus-latest",
      name: "Qwen Plus (OAuth)",
      providerId: "qwen-oauth",
      limit: { context: 131072, output: 8192 },
    },
    {
      id: "qwen-max-latest",
      name: "Qwen Max (OAuth)",
      providerId: "qwen-oauth",
      limit: { context: 32768, output: 8192 },
    },
  ]

  return {
    id: "qwen-oauth",
    name: "Qwen OAuth",
    models: qwenModels,
    defaultModelId: "qwen-coder-plus-latest",
  }
}

async function fetchOpenCodeZenProvider(): Promise<Provider | null> {
  const data = await fetchJson<{ models?: Array<{ id: string; name: string; limit?: Model["limit"]; cost?: Model["cost"] }> }>(
    "/api/opencode-zen/models",
  )
  const models = Array.isArray(data?.models) ? data?.models ?? [] : []
  if (models.length === 0) return null

  return {
    id: "opencode-zen",
    name: "OpenCode Zen",
    models: models.map((model) => ({
      id: model.id,
      name: model.name,
      providerId: "opencode-zen",
      limit: model.limit,
      cost: model.cost,
    })),
  }
}

async function fetchZAIProvider(): Promise<Provider | null> {
  try {
    const config = await fetchJson<{ config?: { enabled?: boolean } }>("/api/zai/config")
    if (!config?.config?.enabled) return null

    const data = await fetchJson<{ models?: Array<{ name: string; provider: string }> }>("/api/zai/models")
    const models = Array.isArray(data?.models) ? data.models : []
    if (models.length === 0) return null

    return {
      id: "zai",
      name: "Z.AI Coding Plan",
      models: models.map((model) => ({
        id: model.name,
        name: model.name.toUpperCase(),
        providerId: "zai",
        limit: { context: 131072, output: 8192 },
      })),
      defaultModelId: "glm-4.7",
    }
  } catch (error) {
    log.warn("Failed to fetch Z.AI models", { error })
    return null
  }
}

export function getStoredAntigravityToken():
  | { access_token: string; expires_in: number; created_at: number }
  | null {
  if (typeof window === "undefined") return null
  try {
    const raw = window.localStorage.getItem(getUserScopedKey("antigravity_oauth_token"))
    if (!raw) return null
    return JSON.parse(raw)
  } catch {
    return null
  }
}

export function isAntigravityTokenValid(token: { expires_in: number; created_at: number } | null): boolean {
  if (!token) return false
  const createdAt = token.created_at > 1e12 ? Math.floor(token.created_at / 1000) : token.created_at
  const expiresAt = (createdAt + token.expires_in) * 1000 - 300000
  return Date.now() < expiresAt
}

export function getStoredAntigravityProjectId(): string | undefined {
  if (typeof window === "undefined") return undefined
  try {
    const value = window.localStorage.getItem(getUserScopedKey(ANTIGRAVITY_PROJECT_KEY))
    return value && value.trim().length > 0 ? value.trim() : undefined
  } catch {
    return undefined
  }
}

async function fetchAntigravityProvider(): Promise<Provider | null> {
  const token = getStoredAntigravityToken()
  const projectId = getStoredAntigravityProjectId()
  const headers: Record<string, string> = { "Content-Type": "application/json" }
  if (token?.access_token) {
    headers["Authorization"] = `Bearer ${token.access_token}`
  }
  if (projectId) {
    headers["X-Antigravity-Project"] = projectId
  }

  try {
    const response = await fetch("/api/antigravity/models", { headers })
    if (!response.ok) {
      // If server is down, return null to not show broken provider
      return null
    }

    const data = (await response.json()) as { models?: Array<{ id: string; name: string; limit?: Model["limit"] }> }
    const models = Array.isArray(data?.models) ? data.models : []

    // If no models returned from server (unlikely now with backend fix),
    // but we can still return the provider with 0 models if we want it to show up.
    // However, LiteModelSelector typically hides providers with empty models.
    if (models.length === 0) return null

    return {
      id: "antigravity",
      name: "Antigravity",
      models: models.map((model) => ({
        id: model.id,
        name: model.name,
        providerId: "antigravity",
        limit: model.limit,
      })),
      defaultModelId: "gemini-3-pro-high",
    }
  } catch (error) {
    log.error("Failed to fetch Antigravity models", error)
    return null
  }
}

async function fetchExtraProviders(): Promise<Provider[]> {
  const [ollama, zen, qwen, zai, antigravity] = await Promise.all([
    fetchOllamaCloudProvider(),
    fetchOpenCodeZenProvider(),
    fetchQwenOAuthProvider(),
    fetchZAIProvider(),
    fetchAntigravityProvider(),
  ])
  return [ollama, zen, qwen, zai, antigravity].filter((provider): provider is Provider => Boolean(provider))
}

function removeDuplicateProviders(base: Provider[], extras: Provider[]): Provider[] {
  // Collect all extra provider IDs and model IDs to prevent duplicates
  const extraProviderIds = new Set(extras.map((provider) => provider.id))
  const extraModelIds = new Set(extras.flatMap((provider) => provider.models.map((model) => model.id)))

  return base.filter((provider) => {
    // Remove base providers that have the same ID as an extra provider
    // This prevents qwen-oauth, zai, ollama-cloud, antigravity duplicates
    if (extraProviderIds.has(provider.id)) {
      return false
    }
    // Special case: remove opencode if opencode-zen is present and covers all models
    if (provider.id === "opencode" && extraProviderIds.has("opencode-zen") &&
      provider.models.every((model) => extraModelIds.has(model.id))) {
      return false
    }
    // Remove any qwen-related SDK providers when qwen-oauth is present
    if (extraProviderIds.has("qwen-oauth") &&
      (provider.id.toLowerCase().includes("qwen") ||
        provider.models.some((m) => m.id.toLowerCase().includes("qwen")))) {
      return false
    }
    return true
  })
}


interface SessionForkResponse {
  id: string
  title?: string
  parentID?: string | null
  agent?: string
  model?: {
    providerID?: string
    modelID?: string
  }
  time?: {
    created?: number
    updated?: number
  }
  revert?: {
    messageID?: string
    partID?: string
    snapshot?: string
    diff?: string
  }
}

async function fetchSessions(instanceId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"

  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  setLoading((prev) => {
    const next = { ...prev }
    next.fetchingSessions.set(instanceId, true)
    return next
  })

  try {
    log.info("session.list", { instanceId, isNative })

    let responseData: any[] = []

    if (isNative) {
      // Auto-sync SDK sessions from OpenCode's storage on native mode startup
      if (needsMigration(instanceId)) {
        try {
          // First try to sync directly from OpenCode's storage (most reliable)
          const folderPath = instance.folder
          if (folderPath) {
            log.info({ instanceId, folderPath }, "Syncing SDK sessions from OpenCode storage")
            const syncResult = await nativeSessionApi.syncFromSdk(instanceId, folderPath)
            if (syncResult.imported > 0) {
              log.info({ instanceId, syncResult }, "Synced SDK sessions from OpenCode storage")
            } else if (syncResult.message) {
              log.info({ instanceId, message: syncResult.message }, "SDK sync info")
            }
          }

          // Also try the localStorage cache as fallback
          const cacheResult = await autoImportCachedSessions(instanceId)
          if (cacheResult.imported > 0) {
            log.info({ instanceId, cacheResult }, "Auto-imported cached SDK sessions")
          }
        } catch (error) {
          log.error({ instanceId, error }, "Failed to sync SDK sessions")
          markMigrated(instanceId) // Mark as migrated to prevent repeated failures
        }
      }

      const nativeSessions = await nativeSessionApi.listSessions(instanceId)
      responseData = nativeSessions.map(s => ({
        id: s.id,
        title: s.title,
        parentID: s.parentId,
        version: "0",
        time: {
          created: s.createdAt,
          updated: s.updatedAt
        },
        model: s.model ? {
          providerID: s.model.providerId,
          modelID: s.model.modelId
        } : undefined,
        agent: s.agent
      }))
    } else {
      const response = await instance.client!.session.list()
      if (response.data && Array.isArray(response.data)) {
        responseData = response.data
      }
    }

    const sessionMap = new Map<string, Session>()

    if (responseData.length === 0 && !isNative) {
      // In SDK mode we still check response.data for empty
    }

    const existingSessions = sessions().get(instanceId)

    // Load session tasks from storage
    await ensureInstanceConfigLoaded(instanceId)
    const instanceData = getInstanceConfig(instanceId)
    const sessionTasks = instanceData.sessionTasks || {}
    const sessionSkills = instanceData.sessionSkills || {}

    for (const apiSession of responseData) {
      const existingSession = existingSessions?.get(apiSession.id)

      const existingModel = existingSession?.model ?? { providerId: "", modelId: "" }
      const hasUserSelectedModel = existingModel.providerId && existingModel.modelId
      const apiModel = apiSession.model?.providerID && apiSession.model?.modelID
        ? { providerId: apiSession.model.providerID, modelId: apiSession.model.modelID }
        : { providerId: "", modelId: "" }

      sessionMap.set(apiSession.id, {
        id: apiSession.id,
        instanceId,
        title: apiSession.title || "Untitled",
        parentId: apiSession.parentID || null,
        agent: existingSession?.agent ?? apiSession.agent ?? "",
        model: hasUserSelectedModel ? existingModel : apiModel,
        version: apiSession.version,
        time: {
          ...apiSession.time,
        },
        revert: apiSession.revert
          ? {
            messageID: apiSession.revert.messageID,
            partID: apiSession.revert.partID,
            snapshot: apiSession.revert.snapshot,
            diff: apiSession.revert.diff,
          }
          : undefined,
        tasks: sessionTasks[apiSession.id] || [],
        skills: sessionSkills[apiSession.id] || [],
      })
    }

    const updates: Promise<unknown>[] = []
    for (const [parentId, tasks] of Object.entries(sessionTasks)) {
      if (!Array.isArray(tasks)) continue
      for (const task of tasks as Array<{ taskSessionId?: string }>) {
        const childId = task?.taskSessionId
        if (!childId) continue
        const childSession = sessionMap.get(childId)
        if (!childSession) continue
        if (childSession.parentId === parentId) continue
        sessionMap.set(childId, {
          ...childSession,
          parentId,
        })
        if (isNative) {
          updates.push(nativeSessionApi.updateSession(instanceId, childId, { parentId }).catch(() => undefined))
        }
      }
    }
    if (updates.length > 0) {
      await Promise.allSettled(updates)
    }

    for (const [sessionId, tasks] of Object.entries(sessionTasks)) {
      if (sessionMap.has(sessionId)) continue
      if (!Array.isArray(tasks) || tasks.length === 0) continue
      const existingSession = existingSessions?.get(sessionId)
      sessionMap.set(sessionId, {
        id: sessionId,
        instanceId,
        title: existingSession?.title ?? "Untitled",
        parentId: existingSession?.parentId ?? null,
        agent: existingSession?.agent ?? "",
        model: existingSession?.model ?? { providerId: "", modelId: "" },
        skills: existingSession?.skills ?? [],
        version: existingSession?.version ?? "0",
        time: existingSession?.time ?? { created: Date.now(), updated: Date.now() },
        revert: existingSession?.revert,
        tasks: tasks as any[],
      })
    }

    const validSessionIds = new Set(sessionMap.keys())

    setSessions((prev) => {
      const next = new Map(prev)
      next.set(instanceId, sessionMap)
      return next
    })

    // Cache SDK sessions to localStorage for later migration to native mode
    if (!isNative && sessionMap.size > 0) {
      cacheSDKSessions(instanceId, Array.from(sessionMap.values()))
    }


    setMessagesLoaded((prev) => {
      const next = new Map(prev)
      const loadedSet = next.get(instanceId)
      if (loadedSet) {
        const filtered = new Set<string>()
        for (const id of loadedSet) {
          if (validSessionIds.has(id)) {
            filtered.add(id)
          }
        }
        next.set(instanceId, filtered)
      }
      return next
    })

    for (const session of sessionMap.values()) {
      const flag = (session.time as (Session["time"] & { compacting?: number | boolean }) | undefined)?.compacting
      const active = typeof flag === "number" ? flag > 0 : Boolean(flag)
      setSessionCompactionState(instanceId, session.id, active)
    }

    pruneDraftPrompts(instanceId, new Set(sessionMap.keys()))
  } catch (error) {
    log.error("Failed to fetch sessions:", error)
    throw error
  } finally {
    setLoading((prev) => {
      const next = { ...prev }
      next.fetchingSessions.set(instanceId, false)
      return next
    })
  }
}

async function createSession(
  instanceId: string,
  agent?: string,
  options?: { skipAutoCleanup?: boolean },
): Promise<Session> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  const instanceAgents = agents().get(instanceId) || []
  const nonSubagents = instanceAgents.filter((a) => a.mode !== "subagent")
  const selectedAgent = agent || (nonSubagents.length > 0 ? nonSubagents[0].name : "")

  const agentModelPreference = await getAgentModelPreference(instanceId, selectedAgent)
  const defaultModel = await getDefaultModel(instanceId, selectedAgent)
  const sessionModel = agentModelPreference || defaultModel

  if (selectedAgent && isModelValid(instanceId, sessionModel) && !agentModelPreference) {
    await setAgentModelPreference(instanceId, selectedAgent, sessionModel)
  }

  setLoading((prev) => {
    const next = { ...prev }
    next.creatingSession.set(instanceId, true)
    return next
  })

  try {
    log.info(`[HTTP] POST session create for instance ${instanceId}, isNative: ${isNative}`)

    let sessionData: any = null

    if (isNative) {
      const native = await nativeSessionApi.createSession(instanceId, {
        agent: selectedAgent,
        model: sessionModel
      })
      sessionData = {
        id: native.id,
        title: native.title || "New Session",
        parentID: native.parentId,
        version: "0",
        time: {
          created: native.createdAt,
          updated: native.updatedAt
        },
        agent: native.agent,
        model: native.model ? {
          providerID: native.model.providerId,
          modelID: native.model.modelId
        } : undefined
      }
    } else {
      const response = await instance.client!.session.create()
      sessionData = response.data
    }

    if (!sessionData) {
      throw new Error("Failed to create session: No data returned")
    }

    const session: Session = {
      id: sessionData.id,
      instanceId,
      title: sessionData.title || "New Session",
      parentId: null,
      agent: selectedAgent,
      model: sessionModel,
      skills: [],
      version: sessionData.version,
      time: {
        ...sessionData.time,
      },
      revert: sessionData.revert
        ? {
          messageID: sessionData.revert.messageID,
          partID: sessionData.revert.partID,
          snapshot: sessionData.revert.snapshot,
          diff: sessionData.revert.diff,
        }
        : undefined,
    }

    setSessions((prev) => {
      const next = new Map(prev)
      const instanceSessions = next.get(instanceId) || new Map()
      instanceSessions.set(session.id, session)
      next.set(instanceId, instanceSessions)
      return next
    })

    const instanceProviders = providers().get(instanceId) || []
    const initialProvider = instanceProviders.find((p) => p.id === session.model.providerId)
    const initialModel = initialProvider?.models.find((m) => m.id === session.model.modelId)
    const initialContextWindow = initialModel?.limit?.context ?? 0
    const initialSubscriptionModel = initialModel?.cost?.input === 0 && initialModel?.cost?.output === 0
    const initialOutputLimit =
      initialModel?.limit?.output && initialModel.limit.output > 0
        ? initialModel.limit.output
        : DEFAULT_MODEL_OUTPUT_LIMIT
    const initialContextAvailable = initialContextWindow > 0 ? initialContextWindow : null

    setSessionInfoByInstance((prev) => {
      const next = new Map(prev)
      const instanceInfo = new Map(prev.get(instanceId))
      instanceInfo.set(session.id, {
        cost: 0,
        contextWindow: initialContextWindow,
        isSubscriptionModel: Boolean(initialSubscriptionModel),
        inputTokens: 0,
        outputTokens: 0,
        reasoningTokens: 0,
        cacheReadTokens: 0,
        cacheWriteTokens: 0,
        actualUsageTokens: 0,
        modelOutputLimit: initialOutputLimit,
        contextAvailableTokens: initialContextAvailable,
      })
      next.set(instanceId, instanceInfo)
      return next
    })

    if (preferences().autoCleanupBlankSessions && !options?.skipAutoCleanup) {
      await cleanupBlankSessions(instanceId, session.id)
    }

    return session
  } catch (error) {
    log.error("Failed to create session:", error)
    throw error
  } finally {
    setLoading((prev) => {
      const next = { ...prev }
      next.creatingSession.set(instanceId, false)
      return next
    })
  }
}

async function forkSession(
  instanceId: string,
  sourceSessionId: string,
  options?: { messageId?: string },
): Promise<Session> {
  const instance = instances().get(instanceId)
  if (!instance || !instance.client) {
    throw new Error("Instance not ready")
  }

  const request: {
    path: { id: string }
    body?: { messageID: string }
  } = {
    path: { id: sourceSessionId },
  }

  if (options?.messageId) {
    request.body = { messageID: options.messageId }
  }

  log.info(`[HTTP] POST /session.fork for instance ${instanceId}`, request)
  const response = await instance.client.session.fork(request)

  if (!response.data) {
    throw new Error("Failed to fork session: No data returned")
  }

  const sourceSession = sessions().get(instanceId)?.get(sourceSessionId)
  const sourceModel = sourceSession?.model ?? { providerId: "", modelId: "" }
  const sourceSkills = sourceSession?.skills ?? []

  const info = response.data as SessionForkResponse
  const forkedSession = {
    id: info.id,
    instanceId,
    title: info.title || "Forked Session",
    parentId: info.parentID || sourceSessionId,  // Fallback to source session to ensure parent-child relationship
    agent: info.agent || sourceSession?.agent || "",
    model: sourceModel.providerId && sourceModel.modelId
      ? { providerId: sourceModel.providerId, modelId: sourceModel.modelId }
      : {
        providerId: info.model?.providerID || "",
        modelId: info.model?.modelID || "",
      },
    skills: sourceSkills,
    version: "0",
    time: info.time ? { ...info.time } : { created: Date.now(), updated: Date.now() },
    revert: info.revert
      ? {
        messageID: info.revert.messageID,
        partID: info.revert.partID,
        snapshot: info.revert.snapshot,
        diff: info.revert.diff,
      }
      : undefined,
  } as unknown as Session

  setSessions((prev) => {
    const next = new Map(prev)
    const instanceSessions = next.get(instanceId) || new Map()
    instanceSessions.set(forkedSession.id, forkedSession)
    next.set(instanceId, instanceSessions)
    return next
  })

  const instanceProviders = providers().get(instanceId) || []
  const forkProvider = instanceProviders.find((p) => p.id === forkedSession.model.providerId)
  const forkModel = forkProvider?.models.find((m) => m.id === forkedSession.model.modelId)
  const forkContextWindow = forkModel?.limit?.context ?? 0
  const forkSubscriptionModel = forkModel?.cost?.input === 0 && forkModel?.cost?.output === 0
  const forkOutputLimit =
    forkModel?.limit?.output && forkModel.limit.output > 0 ? forkModel.limit.output : DEFAULT_MODEL_OUTPUT_LIMIT
  const forkContextAvailable = forkContextWindow > 0 ? forkContextWindow : null

  setSessionInfoByInstance((prev) => {
    const next = new Map(prev)
    const instanceInfo = new Map(prev.get(instanceId))
    instanceInfo.set(forkedSession.id, {
      cost: 0,
      contextWindow: forkContextWindow,
      isSubscriptionModel: Boolean(forkSubscriptionModel),
      inputTokens: 0,
      outputTokens: 0,
      reasoningTokens: 0,
      cacheReadTokens: 0,
      cacheWriteTokens: 0,
      actualUsageTokens: 0,
      modelOutputLimit: forkOutputLimit,
      contextAvailableTokens: forkContextAvailable,
    })
    next.set(instanceId, instanceInfo)
    return next
  })

  return forkedSession
}

async function deleteSession(instanceId: string, sessionId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) return

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) return

  setLoading((prev) => {
    const next = { ...prev }
    const deleting = next.deletingSession.get(instanceId) || new Set()
    deleting.add(sessionId)
    next.deletingSession.set(instanceId, deleting)
    return next
  })

  try {
    log.info("session.delete", { instanceId, sessionId, isNative })

    if (isNative) {
      await nativeSessionApi.deleteSession(instanceId, sessionId)
    } else {
      await instance.client!.session.delete({ path: { id: sessionId } })
    }

    setSessions((prev) => {
      const next = new Map(prev)
      const instanceSessions = next.get(instanceId)
      if (instanceSessions) {
        instanceSessions.delete(sessionId)
      }
      return next
    })

    setSessionCompactionState(instanceId, sessionId, false)
    clearSessionDraftPrompt(instanceId, sessionId)

    // Drop normalized message state and caches for this session
    messageStoreBus.getOrCreate(instanceId).clearSession(sessionId)
    clearCacheForSession(instanceId, sessionId)

    setSessionInfoByInstance((prev) => {
      const next = new Map(prev)
      const instanceInfo = next.get(instanceId)
      if (instanceInfo) {
        const updatedInstanceInfo = new Map(instanceInfo)
        updatedInstanceInfo.delete(sessionId)
        if (updatedInstanceInfo.size === 0) {
          next.delete(instanceId)
        } else {
          next.set(instanceId, updatedInstanceInfo)
        }
      }
      return next
    })

    if (activeSessionId().get(instanceId) === sessionId) {
      setActiveSessionId((prev) => {
        const next = new Map(prev)
        next.delete(instanceId)
        return next
      })
    }
  } catch (error) {
    log.error("Failed to delete session:", error)
    throw error
  } finally {
    setLoading((prev) => {
      const next = { ...prev }
      const deleting = next.deletingSession.get(instanceId)
      if (deleting) {
        deleting.delete(sessionId)
      }
      return next
    })
  }
}

async function fetchAgents(instanceId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  try {
    log.info("agents.list", { instanceId, isNative })

    let agentList: any[] = []

    if (isNative) {
      // In native mode, we don't have agents from the SDK yet
      // We can return a default agent or common agents
      agentList = [{
        name: "Assistant",
        description: "Native assistant agent",
        mode: "native"
      }]
    } else {
      const response = await instance.client!.app.agents()
      agentList = (response.data || []).map((agent: any) => ({
        name: agent.name,
        description: agent.description || "",
        mode: agent.mode as "standard" | "subagent",
        model: agent.model
          ? {
            providerId: agent.model.providerID || "",
            modelId: agent.model.modelID,
          }
          : undefined,
      }))
    }

    const customAgents = getInstanceConfig(instanceId)?.customAgents ?? []
    const customList = customAgents.map((agent) => ({
      name: agent.name,
      description: agent.description || "",
      mode: "custom",
    }))

    setAgents((prev) => {
      const next = new Map(prev)
      next.set(instanceId, [...agentList, ...customList])
      return next
    })
  } catch (error) {
    log.error("Failed to fetch agents:", error)
  }
}

async function fetchProviders(instanceId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  try {
    log.info("config.providers", { instanceId, isNative })

    let providerList: any[] = []
    let defaultProviders: any = {}

    if (isNative) {
      // For native mode, we mainly rely on extra providers
      // but we could add "zen" (OpenCode Zen) if it's available via server API
      providerList = []
    } else {
      const response = await instance.client!.config.providers()
      if (response.data) {
        providerList = response.data.providers.map((provider) => ({
          id: provider.id,
          name: provider.name,
          defaultModelId: response.data?.default?.[provider.id],
          models: Object.entries(provider.models).map(([id, model]) => ({
            id,
            name: model.name,
            providerId: provider.id,
            limit: model.limit,
            cost: model.cost,
          })),
        }))
        defaultProviders = response.data.default || {}
      }
    }

    const normalizedProviders = providerList
      .map((provider) => {
        if (provider.id !== "google") return provider
        const filteredModels = provider.models.filter((model: Model) => ANTIGRAVITY_MODEL_IDS.has(model.id))
        if (filteredModels.length === 0) return null
        const defaultModelId = filteredModels.some((model: Model) => model.id === provider.defaultModelId)
          ? provider.defaultModelId
          : filteredModels[0]?.id
        return {
          ...provider,
          name: "Antigravity",
          defaultModelId,
          models: filteredModels,
        }
      })
      .filter(Boolean) as typeof providerList

    // Filter out Z.AI providers from SDK to use our custom routing with full message history
    const filteredBaseProviders = normalizedProviders.filter((provider) =>
      !provider.id.toLowerCase().includes("zai") &&
      !provider.id.toLowerCase().includes("z.ai") &&
      !provider.id.toLowerCase().includes("glm")
    )

    let extraProviders = await fetchExtraProviders()
    if (!isNative) {
      const hasSdkAntigravity = normalizedProviders.some((provider) => provider.id === "google")
      if (hasSdkAntigravity) {
        extraProviders = extraProviders.filter((provider) => provider.id !== "antigravity")
      }
    }
    const baseProviders = removeDuplicateProviders(filteredBaseProviders, extraProviders)
    const mergedProviders = mergeProviders(baseProviders, extraProviders)

    setProviders((prev) => {
      const next = new Map(prev)
      next.set(instanceId, mergedProviders)
      return next
    })
  } catch (error) {
    log.error("Failed to fetch providers:", error)
  }
}

async function loadMessages(instanceId: string, sessionId: string, force = false): Promise<void> {
  if (force) {
    setMessagesLoaded((prev) => {
      const next = new Map(prev)
      const loadedSet = next.get(instanceId)
      if (loadedSet) {
        loadedSet.delete(sessionId)
      }
      return next
    })
  }

  const alreadyLoaded = messagesLoaded().get(instanceId)?.has(sessionId)
  if (alreadyLoaded && !force) {
    return
  }

  const isLoading = loading().loadingMessages.get(instanceId)?.has(sessionId)
  if (isLoading) {
    return
  }

  const instance = instances().get(instanceId)
  if (!instance) {
    throw new Error("Instance not ready")
  }

  const isNative = instance.binaryPath === "__nomadarch_native__"
  if (!isNative && !instance.client) {
    throw new Error("Instance client not ready")
  }

  const instanceSessions = sessions().get(instanceId)
  const session = instanceSessions?.get(sessionId)
  if (!session) {
    throw new Error("Session not found")
  }

  setLoading((prev) => {
    const next = { ...prev }
    const loadingSet = next.loadingMessages.get(instanceId) || new Set()
    loadingSet.add(sessionId)
    next.loadingMessages.set(instanceId, loadingSet)
    return next
  })

  try {
    log.info("session.getMessages", { instanceId, sessionId, isNative })

    let apiMessages: any[] = []
    let apiMessagesInfo: any = {}

    if (isNative) {
      const nativeMessages = await nativeSessionApi.getMessages(instanceId, sessionId)
      apiMessages = nativeMessages.map(m => ({
        id: m.id,
        role: m.role,
        content: m.content || "",
        createdAt: m.createdAt,
        status: m.status,
        info: {
          id: m.id,
          role: m.role,
          time: { created: m.createdAt },
          // Add other native message properties to info if needed for later processing
        }
      }))
    } else {
      const response = await instance.client!.session.messages({ path: { id: sessionId } })
      if (!response.data || !Array.isArray(response.data)) {
        return
      }
      apiMessages = response.data || []
      apiMessagesInfo = (response as any).info || {} // Assuming 'info' might be on the response object itself for some cases
    }

    if (!isNative) {
      await ensureInstanceConfigLoaded(instanceId)
      const cachedMessages = getInstanceConfig(instanceId).sessionMessages?.[sessionId] ?? []
      if (cachedMessages.length > 0) {
        const existingIds = new Set<string>()
        for (const apiMessage of apiMessages) {
          const info = apiMessage.info || apiMessage
          if (info?.id) {
            existingIds.add(info.id)
          }
        }
        for (const cached of cachedMessages) {
          if (!cached?.id || existingIds.has(cached.id)) continue
          apiMessages.push({
            id: cached.id,
            role: cached.role,
            content: cached.content,
            createdAt: cached.createdAt,
            info: {
              id: cached.id,
              role: cached.role,
              time: { created: cached.createdAt ?? Date.now() },
            },
            parts: cached.content
              ? [{ id: `part-${cached.id}`, type: "text", text: cached.content }]
              : [],
          })
          existingIds.add(cached.id)
        }
        apiMessages.sort((a, b) => {
          const aInfo = a.info || a
          const bInfo = b.info || b
          const aTime = aInfo.time?.created ?? aInfo.createdAt ?? 0
          const bTime = bInfo.time?.created ?? bInfo.createdAt ?? 0
          return aTime - bTime
        })
      }
    }

    const messagesInfo = new Map<string, any>()
    const messages: Message[] = apiMessages.map((apiMessage: any) => {
      const info = apiMessage.info || apiMessage
      const role = info.role || "assistant"
      const messageId = info.id || String(Date.now())

      messagesInfo.set(messageId, info)

      let parts: any[] = (apiMessage.parts || []).map((part: any) => normalizeMessagePart(part))
      if (parts.length === 0 && typeof apiMessage.content === "string" && apiMessage.content.trim().length > 0) {
        parts = [normalizeMessagePart({ id: `part-${messageId}`, type: "text", text: apiMessage.content })]
      }

      const message: Message = {
        id: messageId,
        sessionId,
        type: role === "user" ? "user" : "assistant",
        parts,
        timestamp: info.time?.created || Date.now(),
        status: "complete" as const,
        version: 0,
      }

      return message
    })

    let agentName = ""
    let providerID = ""
    let modelID = ""

    for (let i = apiMessages.length - 1; i >= 0; i--) {
      const apiMessage = apiMessages[i]
      const info = apiMessage.info || apiMessage

      if (info.role === "assistant") {
        agentName = (info as any).mode || (info as any).agent || ""
        providerID = (info as any).providerID || ""
        modelID = (info as any).modelID || ""
        if (agentName && providerID && modelID) break
      }
    }


    if (!agentName && !providerID && !modelID) {
      const defaultModel = await getDefaultModel(instanceId, session.agent)
      agentName = session.agent
      providerID = defaultModel.providerId
      modelID = defaultModel.modelId
    }

    setSessions((prev) => {
      const next = new Map(prev)
      const nextInstanceSessions = next.get(instanceId)
      if (nextInstanceSessions) {
        const existingSession = nextInstanceSessions.get(sessionId)
        if (existingSession) {
          const currentModel = existingSession.model
          const hasUserSelectedModel = currentModel.providerId && currentModel.modelId

          const updatedSession = {
            ...existingSession,
            agent: agentName || existingSession.agent,
            model: hasUserSelectedModel ? currentModel : (providerID && modelID ? { providerId: providerID, modelId: modelID } : currentModel),
          }
          const updatedInstanceSessions = new Map(nextInstanceSessions)
          updatedInstanceSessions.set(sessionId, updatedSession)
          next.set(instanceId, updatedInstanceSessions)
        }
      }
      return next
    })

    setMessagesLoaded((prev) => {
      const next = new Map(prev)
      const loadedSet = next.get(instanceId) || new Set()
      loadedSet.add(sessionId)
      next.set(instanceId, loadedSet)
      return next
    })

    const sessionForV2 = sessions().get(instanceId)?.get(sessionId) ?? {
      id: sessionId,
      instanceId,
      parentId: session?.parentId ?? null,
      agent: "",
      model: { providerId: "", modelId: "" },
      version: "0",
      time: { created: Date.now(), updated: Date.now() },
      revert: session?.revert,
    }
    seedSessionMessagesV2(instanceId, sessionForV2, messages, messagesInfo)

  } catch (error) {
    log.error("Failed to load messages:", error)
    throw error
  } finally {
    setLoading((prev) => {
      const next = { ...prev }
      const loadingSet = next.loadingMessages.get(instanceId)
      if (loadingSet) {
        loadingSet.delete(sessionId)
      }
      return next
    })
  }

  updateSessionInfo(instanceId, sessionId)
}

async function syncSessionsFromSdk(instanceId: string): Promise<void> {
  const instance = instances().get(instanceId)
  if (!instance) throw new Error("Instance not ready")

  const folderPath = instance.folder
  if (!folderPath) throw new Error("No folder path for instance")

  log.info({ instanceId, folderPath }, "Manual SDK sync requested")

  try {
    const result = await nativeSessionApi.syncFromSdk(instanceId, folderPath)
    log.info({ instanceId, result }, "Manual SDK sync result")

    // Refresh sessions after sync
    await fetchSessions(instanceId)
  } catch (error) {
    log.error({ instanceId, error }, "Manual SDK sync failed")
    throw error
  }
}

export {
  createSession,
  deleteSession,
  fetchAgents,
  fetchProviders,

  fetchSessions,
  syncSessionsFromSdk,
  forkSession,
  loadMessages,
}
