import * as Log from "@saeeol/core/util/log"
import path from "path"
import { pathToFileURL } from "url"
import os from "os"
import { mergeDeep } from "remeda"
import { Global } from "@saeeol/core/global"
import fsNode from "fs/promises"
import { Flag } from "@saeeol/core/flag/flag"
import { applyEdits, findNodeAtLocation, modify, parseTree } from "jsonc-parser"
import { existsSync } from "fs"
import { isRecord } from "@/util/record"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { Duration, Effect, Fiber, Option } from "effect"
import { containsPath } from "../project/instance-context"
import type { Info, Warning } from "./config-schema"
import { ConfigPlugin } from "./plugin"
import { ConfigParse } from "./parse"
import { ConfigPaths } from "./paths"
import { ConfigVariable } from "./variable"
import { SaeeolConfig } from "../overlay/config/config"

const log = Log.create({ service: "config" })
const { caught: caughtWarning } = SaeeolConfig

export function mergeConfig(target: Info, source: Info): Info {
  return mergeDeep(target, source) as Info
}

export function mergeConfigConcatArrays(target: Info, source: Info): Info {
  const merged = mergeConfig(target, source)
  if (target.instructions && source.instructions) {
    merged.instructions = Array.from(new Set([...target.instructions, ...source.instructions]))
  }
  return merged
}

export function normalizeLoadedConfig(data: unknown, source: string) {
  if (!isRecord(data)) return data
  const copy = { ...data }
  const hadLegacy = "theme" in copy || "keybinds" in copy || "tui" in copy
  if (!hadLegacy) return copy
  delete copy.theme
  delete copy.keybinds
  delete copy.tui
  log.warn("tui keys in saeeol config are deprecated; move them to tui.json", { path: source })
  return copy
}

async function resolveLoadedPlugins<T extends { plugin?: ConfigPlugin.Spec[] }>(config: T, filepath: string) {
  if (!config.plugin) return config
  for (let i = 0; i < config.plugin.length; i++) {
    config.plugin[i] = await ConfigPlugin.resolvePluginSpec(config.plugin[i], filepath)
  }
  return config
}

export function globalConfigFile() {
  const candidates = ["saeeol.jsonc", "saeeol.json", "config.json"].map((file) =>
    path.join(Global.Path.config, file),
  )
  for (const file of candidates) {
    if (existsSync(file)) return file
  }
  return candidates[0]
}

export function patchJsonc(input: string, patch: unknown, path: string[] = []): string {
  if (!isRecord(patch)) {
    const edits = modify(input, path, patch === null ? undefined : patch, {
      formattingOptions: { insertSpaces: true, tabSize: 2 },
    })
    return applyEdits(input, edits)
  }
  if (path.length > 0) {
    const tree = parseTree(input)
    const node = tree && findNodeAtLocation(tree, path)
    if (node && node.type !== "object") {
      const isPermissionKey = path[0] === "permission" && path.length === 2
      const replacement = isPermissionKey ? { "*": node.value, ...patch } : patch
      const edits = modify(input, path, replacement, {
        formattingOptions: { insertSpaces: true, tabSize: 2 },
      })
      return applyEdits(input, edits)
    }
  }
  return Object.entries(patch).reduce((result, [key, value]) => patchJsonc(result, value, [...path, key]), input)
}

export function writable(info: Info) {
  const { plugin_origins: _, ...next } = info
  return next
}

export function writableGlobal(info: Info) {
  const next = writable(info)
  if ("shell" in next && next.shell === "") return { ...next, shell: undefined }
  return next
}

export function readConfigFile(fs: AppFileSystem.Interface) {
  return Effect.fnUntraced(function* (filepath: string) {
    return yield* fs.readFileString(filepath).pipe(
      Effect.catchIf((e) => e.reason._tag === "NotFound", () => Effect.succeed(undefined)),
      Effect.orDie,
    )
  })
}

export function loadConfig(fs: AppFileSystem.Interface, Info: any) {
  return Effect.fnUntraced(function* (
    text: string,
    options: { path: string } | { dir: string; source: string },
  ) {
    const source = "path" in options ? options.path : options.source
    const expanded = yield* Effect.promise(() =>
      ConfigVariable.substitute(
        "path" in options ? { text, type: "path", path: options.path } : { text, type: "virtual", ...options },
      ),
    )
    const parsed = ConfigParse.jsonc(expanded, source)
    const data = ConfigParse.effectSchema(Info, normalizeLoadedConfig(parsed, source), source)
    if (!("path" in options)) return data
    yield* Effect.promise(() => resolveLoadedPlugins(data, options.path))
    if (!data.$schema) {
      data.$schema = "https://app.saeeol.ai/config.json"
      const updated = text.replace(/^\s*\{/, '{\n  "$schema": "https://app.saeeol.ai/config.json",')
      yield* fs.writeFileString(options.path, updated).pipe(Effect.catch(() => Effect.void))
    }
    return data
  })
}

export function loadFile(fs: AppFileSystem.Interface, Info: any) {
  return Effect.fnUntraced(function* (filepath: string) {
    log.info("loading", { path: filepath })
    const text = yield* readConfigFile(fs)(filepath)
    if (!text) return {} as Info
    return yield* loadConfig(fs, Info)(text, { path: filepath })
  })
}

export function loadGlobal(fs: AppFileSystem.Interface, Info: any) {
  return Effect.fnUntraced(function* () {
    yield* Effect.promise(() => SaeeolConfig.migrateBashPermission())
    let result: Info = {}
    result = mergeConfig(result, yield* loadFile(fs, Info)(path.join(Global.Path.config, "config.json")))
    result = mergeConfig(result, yield* loadFile(fs, Info)(path.join(Global.Path.config, "saeeol.json")))
    result = mergeConfig(result, yield* loadFile(fs, Info)(path.join(Global.Path.config, "saeeol.jsonc")))
    const legacy = path.join(Global.Path.config, "config")
    if (existsSync(legacy)) {
      yield* Effect.promise(() =>
        import(pathToFileURL(legacy).href, { with: { type: "toml" } })
          .then(async (mod) => {
            const { provider, model, ...rest } = mod.default
            if (provider && model) result.model = `${provider}/${model}`
            result["$schema"] = "https://app.saeeol.ai/config.json"
            result = mergeConfig(result, rest)
            await fsNode.writeFile(path.join(Global.Path.config, "config.json"), JSON.stringify(result, null, 2))
            await fsNode.unlink(legacy)
          })
          .catch(() => {}),
      )
    }
    return result
  })
}
