import { createContext, createMemo, createSignal, onMount, useContext } from "solid-js"
import type { Accessor, ParentComponent } from "solid-js"
import { storage, type ConfigData } from "../lib/storage"
import {
  ensureInstanceConfigLoaded,
  getInstanceConfig,
  updateInstanceConfig as updateInstanceData,
} from "./instance-config"

type DeepReadonly<T> = T extends (...args: any[]) => unknown
  ? T
  : T extends Array<infer U>
    ? ReadonlyArray<DeepReadonly<U>>
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T

export interface ModelPreference {
  providerId: string
  modelId: string
}

export interface AgentModelSelections {
  [instanceId: string]: Record<string, ModelPreference>
}

export type DiffViewMode = "split" | "unified"
export type ExpansionPreference = "expanded" | "collapsed"

export interface Preferences {
  showThinkingBlocks: boolean
  lastUsedBinary?: string
  environmentVariables: Record<string, string>
  modelRecents: ModelPreference[]
  diffViewMode: DiffViewMode
  toolOutputExpansion: ExpansionPreference
  diagnosticsExpansion: ExpansionPreference
}

export interface OpenCodeBinary {
  path: string
  version?: string
  lastUsed: number
}

export interface RecentFolder {
  path: string
  lastAccessed: number
}

export type ThemePreference = NonNullable<ConfigData["theme"]>

const MAX_RECENT_FOLDERS = 20
const MAX_RECENT_MODELS = 5

const defaultPreferences: Preferences = {
  showThinkingBlocks: false,
  environmentVariables: {},
  modelRecents: [],
  diffViewMode: "split",
  toolOutputExpansion: "expanded",
  diagnosticsExpansion: "expanded",
}

function deepEqual(a: unknown, b: unknown): boolean {
  if (a === b) return true
  if (typeof a === "object" && a !== null && typeof b === "object" && b !== null) {
    try {
      return JSON.stringify(a) === JSON.stringify(b)
    } catch (error) {
      console.warn("Failed to compare preference values", error)
    }
  }
  return false
}

function normalizePreferences(pref?: Partial<Preferences> & { agentModelSelections?: unknown }): Preferences {
  const sanitized = pref ?? {}
  const environmentVariables = {
    ...defaultPreferences.environmentVariables,
    ...(sanitized.environmentVariables ?? {}),
  }

  const sourceModelRecents = sanitized.modelRecents ?? defaultPreferences.modelRecents
  const modelRecents = sourceModelRecents.map((item) => ({ ...item }))

  return {
    showThinkingBlocks: sanitized.showThinkingBlocks ?? defaultPreferences.showThinkingBlocks,
    lastUsedBinary: sanitized.lastUsedBinary ?? defaultPreferences.lastUsedBinary,
    environmentVariables,
    modelRecents,
    diffViewMode: sanitized.diffViewMode ?? defaultPreferences.diffViewMode,
    toolOutputExpansion: sanitized.toolOutputExpansion ?? defaultPreferences.toolOutputExpansion,
    diagnosticsExpansion: sanitized.diagnosticsExpansion ?? defaultPreferences.diagnosticsExpansion,
  }
}

const [internalConfig, setInternalConfig] = createSignal<ConfigData>(buildFallbackConfig())
const config = createMemo<DeepReadonly<ConfigData>>(() => internalConfig())
const [isConfigLoaded, setIsConfigLoaded] = createSignal(false)
const preferences = createMemo<Preferences>(() => internalConfig().preferences)
const recentFolders = createMemo<RecentFolder[]>(() => internalConfig().recentFolders ?? [])
const opencodeBinaries = createMemo<OpenCodeBinary[]>(() => internalConfig().opencodeBinaries ?? [])
const themePreference = createMemo<ThemePreference>(() => internalConfig().theme ?? "dark")
let loadPromise: Promise<void> | null = null

function normalizeConfig(config?: ConfigData | null): ConfigData {
  return {
    preferences: normalizePreferences(config?.preferences),
    recentFolders: (config?.recentFolders ?? []).map((folder) => ({ ...folder })),
    opencodeBinaries: (config?.opencodeBinaries ?? []).map((binary) => ({ ...binary })),
    theme: config?.theme ?? "dark",
  }
}

function buildFallbackConfig(): ConfigData {
  return normalizeConfig()
}

function removeLegacyAgentSelections(config?: ConfigData | null): { cleaned: ConfigData; migrated: boolean } {
  const migrated = Boolean((config?.preferences as { agentModelSelections?: unknown } | undefined)?.agentModelSelections)
  const cleanedConfig = normalizeConfig(config)
  return { cleaned: cleanedConfig, migrated }
}

async function syncConfig(source?: ConfigData): Promise<void> {
  try {
    const loaded = source ?? (await storage.loadConfig())
    const { cleaned, migrated } = removeLegacyAgentSelections(loaded)
    applyConfig(cleaned)
    if (migrated) {
      void storage.updateConfig(cleaned).catch((error: unknown) => {
        console.error("Failed to persist legacy config cleanup:", error)
      })
    }
  } catch (error) {
    console.error("Failed to load config:", error)
    applyConfig(buildFallbackConfig())
  }
}

function applyConfig(next: ConfigData) {
  setInternalConfig(normalizeConfig(next))
  setIsConfigLoaded(true)
}

function cloneConfigForUpdate(): ConfigData {
  return normalizeConfig(internalConfig())
}

function logConfigDiff(previous: ConfigData, next: ConfigData) {
  if (deepEqual(previous, next)) {
    return
  }
  const changes = diffObjects(previous, next)
  if (changes.length > 0) {
    console.debug("[Config] Changes", changes)
  }
}

function diffObjects(previous: unknown, next: unknown, path: string[] = []): string[] {
  if (previous === next) {
    return []
  }

  if (typeof previous !== "object" || previous === null || typeof next !== "object" || next === null) {
    return [path.join(".")]
  }

  const prevKeys = Object.keys(previous as Record<string, unknown>)
  const nextKeys = Object.keys(next as Record<string, unknown>)
  const allKeys = new Set([...prevKeys, ...nextKeys])
  const changes: string[] = []

  for (const key of allKeys) {
    const childPath = [...path, key]
    const prevValue = (previous as Record<string, unknown>)[key]
    const nextValue = (next as Record<string, unknown>)[key]
    changes.push(...diffObjects(prevValue, nextValue, childPath))
  }

  return changes
}

function updateConfig(mutator: (draft: ConfigData) => void): void {
  const previous = internalConfig()
  const draft = cloneConfigForUpdate()
  mutator(draft)
  logConfigDiff(previous, draft)
  applyConfig(draft)
  void persistFullConfig(draft)
}

async function persistFullConfig(next: ConfigData): Promise<void> {
  try {
    await ensureConfigLoaded()
    await storage.updateConfig(next)
  } catch (error) {
    console.error("Failed to save config:", error)
    void syncConfig().catch((syncError: unknown) => {
      console.error("Failed to refresh config:", syncError)
    })
  }
}

function setThemePreference(preference: ThemePreference): void {
  if (themePreference() === preference) {
    return
  }
  updateConfig((draft) => {
    draft.theme = preference
  })
}

async function ensureConfigLoaded(): Promise<void> {
  if (isConfigLoaded()) return
  if (!loadPromise) {
    loadPromise = syncConfig().finally(() => {
      loadPromise = null
    })
  }
  await loadPromise
}

function buildRecentFolderList(path: string, source: RecentFolder[]): RecentFolder[] {
  const folders = source.filter((f) => f.path !== path)
  folders.unshift({ path, lastAccessed: Date.now() })
  return folders.slice(0, MAX_RECENT_FOLDERS)
}

function buildBinaryList(path: string, version: string | undefined, source: OpenCodeBinary[]): OpenCodeBinary[] {
  const timestamp = Date.now()
  const existing = source.find((b) => b.path === path)
  if (existing) {
    const updatedEntry: OpenCodeBinary = { ...existing, lastUsed: timestamp }
    const remaining = source.filter((b) => b.path !== path)
    return [updatedEntry, ...remaining]
  }
  const nextEntry: OpenCodeBinary = version ? { path, version, lastUsed: timestamp } : { path, lastUsed: timestamp }
  return [nextEntry, ...source].slice(0, 10)
}

function updatePreferences(updates: Partial<Preferences>): void {
  const current = internalConfig().preferences
  const merged = normalizePreferences({ ...current, ...updates })
  if (deepEqual(current, merged)) {
    return
  }
  updateConfig((draft) => {
    draft.preferences = merged
  })
}

function setDiffViewMode(mode: DiffViewMode): void {
  if (preferences().diffViewMode === mode) return
  updatePreferences({ diffViewMode: mode })
}

function setToolOutputExpansion(mode: ExpansionPreference): void {
  if (preferences().toolOutputExpansion === mode) return
  updatePreferences({ toolOutputExpansion: mode })
}

function setDiagnosticsExpansion(mode: ExpansionPreference): void {
  if (preferences().diagnosticsExpansion === mode) return
  updatePreferences({ diagnosticsExpansion: mode })
}

function toggleShowThinkingBlocks(): void {
  updatePreferences({ showThinkingBlocks: !preferences().showThinkingBlocks })
}

function addRecentFolder(path: string): void {
  updateConfig((draft) => {
    draft.recentFolders = buildRecentFolderList(path, draft.recentFolders)
  })
}

function removeRecentFolder(path: string): void {
  updateConfig((draft) => {
    draft.recentFolders = draft.recentFolders.filter((f) => f.path !== path)
  })
}

function addOpenCodeBinary(path: string, version?: string): void {
  updateConfig((draft) => {
    draft.opencodeBinaries = buildBinaryList(path, version, draft.opencodeBinaries)
  })
}

function removeOpenCodeBinary(path: string): void {
  updateConfig((draft) => {
    draft.opencodeBinaries = draft.opencodeBinaries.filter((b) => b.path !== path)
  })
}

function updateLastUsedBinary(path: string): void {
  const target = path || preferences().lastUsedBinary || "opencode"
  updateConfig((draft) => {
    draft.preferences = normalizePreferences({ ...draft.preferences, lastUsedBinary: target })
    draft.opencodeBinaries = buildBinaryList(target, undefined, draft.opencodeBinaries)
  })
}

function recordWorkspaceLaunch(folderPath: string, binaryPath?: string): void {
  updateConfig((draft) => {
    const targetBinary = binaryPath && binaryPath.trim().length > 0 ? binaryPath : draft.preferences.lastUsedBinary || "opencode"
    draft.recentFolders = buildRecentFolderList(folderPath, draft.recentFolders)
    draft.preferences = normalizePreferences({ ...draft.preferences, lastUsedBinary: targetBinary })
    draft.opencodeBinaries = buildBinaryList(targetBinary, undefined, draft.opencodeBinaries)
  })
}

function updateEnvironmentVariables(envVars: Record<string, string>): void {
  updatePreferences({ environmentVariables: envVars })
}

function addEnvironmentVariable(key: string, value: string): void {
  const current = preferences().environmentVariables || {}
  const updated = { ...current, [key]: value }
  updateEnvironmentVariables(updated)
}

function removeEnvironmentVariable(key: string): void {
  const current = preferences().environmentVariables || {}
  const { [key]: removed, ...rest } = current
  updateEnvironmentVariables(rest)
}

function addRecentModelPreference(model: ModelPreference): void {
  if (!model.providerId || !model.modelId) return
  const recents = preferences().modelRecents ?? []
  const filtered = recents.filter((item) => item.providerId !== model.providerId || item.modelId !== model.modelId)
  const updated = [model, ...filtered].slice(0, MAX_RECENT_MODELS)
  updatePreferences({ modelRecents: updated })
}

async function setAgentModelPreference(instanceId: string, agent: string, model: ModelPreference): Promise<void> {
  if (!instanceId || !agent || !model.providerId || !model.modelId) return
  await ensureInstanceConfigLoaded(instanceId)
  await updateInstanceData(instanceId, (draft) => {
    const selections = { ...(draft.agentModelSelections ?? {}) }
    const existing = selections[agent]
    if (existing && existing.providerId === model.providerId && existing.modelId === model.modelId) {
      return
    }
    selections[agent] = model
    draft.agentModelSelections = selections
  })
}

async function getAgentModelPreference(instanceId: string, agent: string): Promise<ModelPreference | undefined> {
  if (!instanceId || !agent) return undefined
  await ensureInstanceConfigLoaded(instanceId)
  const selections = getInstanceConfig(instanceId).agentModelSelections ?? {}
  return selections[agent]
}

void ensureConfigLoaded().catch((error: unknown) => {
  console.error("Failed to initialize config:", error)
})

interface ConfigContextValue {
  isLoaded: Accessor<boolean>
  config: typeof config
  preferences: typeof preferences
  recentFolders: typeof recentFolders
  opencodeBinaries: typeof opencodeBinaries
  themePreference: typeof themePreference
  setThemePreference: typeof setThemePreference
  updateConfig: typeof updateConfig
  toggleShowThinkingBlocks: typeof toggleShowThinkingBlocks
  setDiffViewMode: typeof setDiffViewMode
  setToolOutputExpansion: typeof setToolOutputExpansion
  setDiagnosticsExpansion: typeof setDiagnosticsExpansion
  addRecentFolder: typeof addRecentFolder
  removeRecentFolder: typeof removeRecentFolder
  addOpenCodeBinary: typeof addOpenCodeBinary
  removeOpenCodeBinary: typeof removeOpenCodeBinary
  updateLastUsedBinary: typeof updateLastUsedBinary
  recordWorkspaceLaunch: typeof recordWorkspaceLaunch
  updatePreferences: typeof updatePreferences
  updateEnvironmentVariables: typeof updateEnvironmentVariables
  addEnvironmentVariable: typeof addEnvironmentVariable
  removeEnvironmentVariable: typeof removeEnvironmentVariable
  addRecentModelPreference: typeof addRecentModelPreference
  setAgentModelPreference: typeof setAgentModelPreference
  getAgentModelPreference: typeof getAgentModelPreference
}

const ConfigContext = createContext<ConfigContextValue>()

const configContextValue: ConfigContextValue = {
  isLoaded: isConfigLoaded,
  config,
  preferences,
  recentFolders,
  opencodeBinaries,
  themePreference,
  setThemePreference,
  updateConfig,
  toggleShowThinkingBlocks,
  setDiffViewMode,
  setToolOutputExpansion,
  setDiagnosticsExpansion,
  addRecentFolder,
  removeRecentFolder,
  addOpenCodeBinary,
  removeOpenCodeBinary,
  updateLastUsedBinary,
  recordWorkspaceLaunch,
  updatePreferences,
  updateEnvironmentVariables,
  addEnvironmentVariable,
  removeEnvironmentVariable,
  addRecentModelPreference,
  setAgentModelPreference,
  getAgentModelPreference,
}

const ConfigProvider: ParentComponent = (props) => {
  onMount(() => {
    ensureConfigLoaded().catch((error: unknown) => {
      console.error("Failed to initialize config:", error)
    })

    const unsubscribe = storage.onConfigChanged((config) => {
      syncConfig(config).catch((error: unknown) => {
        console.error("Failed to refresh config:", error)
      })
    })

    return () => {
      unsubscribe()
    }
  })

  return <ConfigContext.Provider value={configContextValue}>{props.children}</ConfigContext.Provider>
}

function useConfig(): ConfigContextValue {
  const context = useContext(ConfigContext)
  if (!context) {
    throw new Error("useConfig must be used within ConfigProvider")
  }
  return context
}

export {
  ConfigProvider,
  useConfig,
  config,
  preferences,
  updateConfig,
  updatePreferences,
  toggleShowThinkingBlocks,
  recentFolders,
  addRecentFolder,
  removeRecentFolder,
  opencodeBinaries,
  addOpenCodeBinary,
  removeOpenCodeBinary,
  updateLastUsedBinary,
  updateEnvironmentVariables,
  addEnvironmentVariable,
  removeEnvironmentVariable,
  addRecentModelPreference,
  setAgentModelPreference,
  getAgentModelPreference,
  setDiffViewMode,
  setToolOutputExpansion,
  setDiagnosticsExpansion,
  themePreference,
  setThemePreference,
  recordWorkspaceLaunch,
}
