import * as Log from "@saeeol/core/util/log"
import path from "path"
import os from "os"
import { mergeDeep, unique } from "remeda"
import { Global } from "@saeeol/core/global"
import { NamedError } from "@saeeol/core/util/error"
import { Flag } from "@saeeol/core/flag/flag"
import { Auth } from "../auth"
import { Env } from "../env"
import { type InstanceContext } from "../project/instance"
import { InstanceStore } from "../project/instance-store"
import { InstallationLocal, InstallationVersion } from "@saeeol/core/installation/version"
import { existsSync } from "fs"
import { GlobalBus } from "@/bus/global"
import { Event } from "../server/event"
import { Account } from "@/account/account"
import { containsPath } from "../project/instance-context"
import type { ConsoleState } from "./console-state"
import { AppFileSystem } from "@saeeol/core/filesystem"
import { InstanceState } from "@/effect/instance-state"
import { Context, Duration, Effect, Exit, Fiber, Layer, Option, Schema } from "effect"
import { EffectFlock } from "@saeeol/core/util/effect-flock"
import { Npm } from "@saeeol/core/npm"
import { SaeeolConfig } from "../overlay/config/config"
import { SaeeolDefaultPlugins } from "@/saeeol/config/default-plugins"
import { makeRuntime } from "@/effect/run-service"
import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command"
import { ConfigManaged } from "./managed"
import { ConfigPaths } from "./paths"
import { ConfigPermission } from "./permission"
import { ConfigPlugin } from "./plugin"
import { ConfigParse } from "./parse"
import z from "zod"
import { Info, Warning, type Layout, Server, Indexing } from "./config-schema"
export { Info, Warning, Server, Indexing }
export type { Info as InfoType, Warning as WarningType, Layout as LayoutType }
import {
  mergeConfig,
  mergeConfigConcatArrays,
  globalConfigFile,
  patchJsonc,
  writable,
  writableGlobal,
  readConfigFile,
  loadConfig,
  loadFile,
  loadGlobal,
} from "./config-loader"

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

export const ConfigDirectoryTypoError = NamedError.create(
  "ConfigDirectoryTypoError",
  z.object({ path: z.string(), dir: z.string(), suggestion: z.string() }),
)

type State = {
  config: Info
  directories: string[]
  deps: Fiber.Fiber<void, never>[]
  warnings: Warning[]
  consoleState: ConsoleState
}

export interface Interface {
  readonly get: () => Effect.Effect<Info>
  readonly getGlobal: () => Effect.Effect<Info>
  readonly getConsoleState: () => Effect.Effect<ConsoleState>
  readonly update: (config: Info, options?: { dispose?: boolean }) => Effect.Effect<void>
  readonly updateGlobal: (config: Info, options?: { dispose?: boolean }) => Effect.Effect<Info>
  readonly invalidate: (wait?: boolean) => Effect.Effect<void>
  readonly directories: () => Effect.Effect<string[]>
  readonly waitForDependencies: () => Effect.Effect<void>
  readonly warnings: () => Effect.Effect<Warning[]>
}

export class Service extends Context.Service<Service, Interface>()("@saeeol/Config") {}

export const layer = Layer.effect(
  Service,
  Effect.gen(function* () {
    const fs = yield* AppFileSystem.Service
    const authSvc = yield* Auth.Service
    const accountSvc = yield* Account.Service
    const env = yield* Env.Service
    const npmSvc = yield* Npm.Service

    const $read = readConfigFile(fs)
    const $load = loadConfig(fs, Info)
    const $loadFile = loadFile(fs, Info)

    const [cachedGlobal, invalidateGlobal] = yield* Effect.cachedInvalidateWithTTL(
      loadGlobal(fs, Info)().pipe(
        Effect.tapError((error) =>
          Effect.sync(() => log.error("failed to load global config, using defaults", { error: String(error) })),
        ),
        Effect.orElseSucceed((): Info => ({})),
      ),
      Duration.infinity,
    )

    const getGlobal = Effect.fn("Config.getGlobal")(function* () {
      return yield* cachedGlobal
    })

    const ensureGitignore = Effect.fn("Config.ensureGitignore")(function* (dir: string) {
      const gitignore = path.join(dir, ".gitignore")
      const hasIgnore = yield* fs.existsSafe(gitignore)
      if (!hasIgnore) {
        yield* fs
          .writeFileString(
            gitignore,
            ["node_modules", "package.json", "package-lock.json", "pnpm-lock.yaml", "bun.lock", "yarn.lock", ".gitignore"].join("\n"),
          )
          .pipe(Effect.catchIf((e) => e.reason._tag === "PermissionDenied" || e.reason._tag === "NotFound", () => Effect.void))
      }
    })

    const loadInstanceState = Effect.fn("Config.loadInstanceState")(function* (ctx: InstanceContext) {
      const warnings: Warning[] = []
      const auth = yield* authSvc.all().pipe(Effect.orDie)
      let result: Info = {}

      const legacy = yield* Effect.promise(() =>
        SaeeolConfig.loadLegacyConfigs({ projectDir: ctx.directory, merge: mergeConfigConcatArrays }),
      )
      result = mergeConfigConcatArrays(result, legacy.config)
      warnings.push(...legacy.warnings)

      const orgModes = yield* Effect.promise(() => SaeeolConfig.loadOrganizationModes(auth))
      if (Object.keys(orgModes.agents).length > 0) {
        result = mergeConfigConcatArrays(result, { agent: orgModes.agents })
      }
      warnings.push(...orgModes.warnings)

      const consoleManagedProviders = new Set<string>()

      const pluginScopeForSource = Effect.fnUntraced(function* (source: string) {
        if (source.startsWith("http://") || source.startsWith("https://")) return "global"
        if (source === "SAEEOL_CONFIG_CONTENT") return "local"
        if (containsPath(source, ctx)) return "local"
        return "global"
      })

      const mergePluginOrigins = Effect.fnUntraced(function* (
        source: string, list: ConfigPlugin.Spec[] | undefined, kind?: ConfigPlugin.Scope,
      ) {
        if (!list?.length) return
        const hit = kind ?? (yield* pluginScopeForSource(source))
        const plugins = ConfigPlugin.deduplicatePluginOrigins([
          ...(result.plugin_origins ?? []),
          ...list.map((spec) => ({ spec, source, scope: hit })),
        ])
        result.plugin = plugins.map((item) => item.spec)
        result.plugin_origins = plugins
      })

      const merge = Effect.fnUntraced(function* (source: string, next: Info, kind?: ConfigPlugin.Scope) {
        const scope = kind ?? (yield* pluginScopeForSource(source))
        const scoped = SaeeolConfig.scopeIndexing(next, scope)
        result = mergeConfigConcatArrays(result, scoped)
        return yield* mergePluginOrigins(source, scoped.plugin, scope)
      })

      for (const [key, value] of Object.entries(auth)) {
        if (value.type === "wellknown") {
          const url = key.replace(/\/+$/, "")
          const source = `${url}/.well-known/saeeol`
          process.env[value.key] = value.token
          const next = yield* Effect.tryPromise({
            try: async () => {
              const response = await fetch(source)
              if (!response.ok) throw new Error(`failed to fetch remote config from ${url}: ${response.status}`)
              const wellknown = (await response.json()) as { config?: Record<string, unknown> }
              const remoteConfig = wellknown.config ?? {}
              if (!remoteConfig.$schema) remoteConfig.$schema = "https://app.saeeol.ai/config.json"
              return remoteConfig
            },
            catch: (err) => err,
          }).pipe(
            Effect.flatMap((remoteConfig) => $load(JSON.stringify(remoteConfig), { dir: path.dirname(source), source })),
            Effect.catch((err: unknown) => { caughtWarning(warnings, source, err); return Effect.succeed({} as Info) }),
            Effect.catchDefect((err: unknown) => { caughtWarning(warnings, source, err); return Effect.succeed({} as Info) }),
          )
          yield* merge(source, next, "global")
        }
      }

      const global = yield* getGlobal().pipe(
        Effect.catchDefect((err: unknown) => { caughtWarning(warnings, "global config", err); return Effect.succeed({} as Info) }),
      )
      yield* merge(Global.Path.config, global, "global")

      if (Flag.SAEEOL_CONFIG) {
        yield* merge(
          Flag.SAEEOL_CONFIG,
          yield* $loadFile(Flag.SAEEOL_CONFIG).pipe(
            Effect.catchDefect((err: unknown) => { caughtWarning(warnings, Flag.SAEEOL_CONFIG!, err); return Effect.succeed({} as Info) }),
          ),
        )
      }

      if (!Flag.SAEEOL_DISABLE_PROJECT_CONFIG) {
        for (const name of ["saeeol"] as const) {
          for (const file of yield* ConfigPaths.files(name, ctx.directory, ctx.worktree).pipe(Effect.orDie)) {
            yield* merge(file, yield* $loadFile(file).pipe(
              Effect.catchDefect((err: unknown) => { caughtWarning(warnings, file, err); return Effect.succeed({} as Info) }),
            ), "local")
          }
        }
      }

      result.agent = result.agent || {}
      result.mode = result.mode || {}
      result.plugin = result.plugin || []
      const directories = yield* ConfigPaths.directories(ctx.directory, ctx.worktree)

      const deps: Fiber.Fiber<void, never>[] = []
      for (const dir of unique(directories)) {
        if (SaeeolConfig.isConfigDir(dir, Flag.SAEEOL_CONFIG_DIR)) {
          for (const file of SaeeolConfig.ALL_CONFIG_FILES) {
            const source = path.join(dir, file)
            yield* merge(source, yield* $loadFile(source).pipe(
              Effect.catchDefect((err: unknown) => { caughtWarning(warnings, source, err); return Effect.succeed({} as Info) }),
            ))
            result.agent ??= {}
            result.mode ??= {}
            result.plugin ??= []
          }
        }
        yield* ensureGitignore(dir).pipe(Effect.orDie)
        const dep = yield* npmSvc.install(dir, {
          add: [{ name: "@saeeol/plugin", version: InstallationLocal ? undefined : InstallationVersion }],
        }).pipe(Effect.exit, Effect.tap((exit) =>
          Exit.isFailure(exit) ? Effect.sync(() => log.warn("background dependency install failed", { dir, error: String(exit.cause) })) : Effect.void,
        ), Effect.asVoid, Effect.forkDetach)
        deps.push(dep)
        result.command = mergeDeep(result.command ?? {}, yield* Effect.promise(() => ConfigCommand.load(dir, warnings)))
        result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.load(dir, warnings)))
        result.agent = mergeDeep(result.agent ?? {}, yield* Effect.promise(() => ConfigAgent.loadMode(dir, warnings)))
        const list = yield* Effect.promise(() => ConfigPlugin.load(dir))
        yield* mergePluginOrigins(dir, list)
      }

      if (process.env.SAEEOL_CONFIG_CONTENT) {
        const source = "SAEEOL_CONFIG_CONTENT"
        yield* merge(source, yield* $load(process.env.SAEEOL_CONFIG_CONTENT, { dir: ctx.directory, source }).pipe(
          Effect.tap(() => Effect.sync(() => log.debug("loaded custom config from SAEEOL_CONFIG_CONTENT"))),
          Effect.catchDefect((err: unknown) => { caughtWarning(warnings, source, err); return Effect.succeed({} as Info) }),
        ), "local")
      }

      const activeAccount = Option.getOrUndefined(
        yield* accountSvc.active().pipe(Effect.catch(() => Effect.succeed(Option.none()))),
      )
      if (activeAccount?.active_org_id) {
        yield* Effect.gen(function* () {
          const [configOpt, tokenOpt] = yield* Effect.all(
            [accountSvc.config(activeAccount.id, activeAccount.active_org_id!), accountSvc.token(activeAccount.id)],
            { concurrency: 2 },
          )
          if (Option.isSome(tokenOpt)) {
            process.env["SAEEOL_CONSOLE_TOKEN"] = tokenOpt.value
            yield* env.set("SAEEOL_CONSOLE_TOKEN", tokenOpt.value)
          }
          if (Option.isSome(configOpt)) {
            const source = `${activeAccount.url}/api/config`
            const next = yield* $load(JSON.stringify(configOpt.value), { dir: path.dirname(source), source })
            for (const providerID of Object.keys(next.provider ?? {})) consoleManagedProviders.add(providerID)
            yield* merge(source, next, "global")
          }
        }).pipe(Effect.withSpan("Config.loadActiveOrgConfig"), Effect.catch((err) => {
          log.debug("failed to fetch remote account config", { error: err instanceof Error ? err.message : String(err) })
          return Effect.void
        }))
      }

      const managedDir = ConfigManaged.managedConfigDir()
      if (existsSync(managedDir)) {
        for (const file of SaeeolConfig.ALL_CONFIG_FILES) {
          yield* merge(path.join(managedDir, file), yield* $loadFile(path.join(managedDir, file)), "global")
        }
      }
      const managed = yield* Effect.promise(() => ConfigManaged.readManagedPreferences())
      if (managed) {
        yield* merge(managed.source, yield* $load(managed.text, { dir: path.dirname(managed.source), source: managed.source }), "global")
      }

      for (const [name, mode] of Object.entries(result.mode ?? {})) {
        result.agent = mergeDeep(result.agent ?? {}, { [name]: { ...mode, mode: "primary" as const } })
      }
      if (Flag.SAEEOL_PERMISSION) {
        result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.SAEEOL_PERMISSION))
      }
      if (result.tools) {
        const perms: Record<string, ConfigPermission.Action> = {}
        for (const [tool, enabled] of Object.entries(result.tools)) {
          const action: ConfigPermission.Action = enabled ? "allow" : "deny"
          if (tool === "write" || tool === "edit" || tool === "patch") { perms.edit = action; continue }
          perms[tool] = action
        }
        result.permission = mergeDeep(perms, result.permission ?? {})
      }
      if (!result.username) result.username = os.userInfo().username
      if (result.autoshare === true && !result.share) result.share = "auto"
      if (Flag.SAEEOL_DISABLE_AUTOCOMPACT) result.compaction = { ...result.compaction, auto: false }
      if (Flag.SAEEOL_DISABLE_PRUNE) result.compaction = { ...result.compaction, prune: false }
      SaeeolDefaultPlugins.apply(result, { disabled: Flag.SAEEOL_DISABLE_DEFAULT_PLUGINS, log })

      return { config: result, directories, deps, warnings, consoleState: { consoleManagedProviders: Array.from(consoleManagedProviders), activeOrgName: undefined, switchableOrgCount: 0 } }
    })

    const state = yield* InstanceState.make<State>(
      Effect.fn("Config.state")(function* (ctx) {
        return yield* loadInstanceState(ctx).pipe(
          Effect.provideService(AppFileSystem.Service, fs),
          Effect.orDie,
        )
      }),
    )

    const get = Effect.fn("Config.get")(function* () { return yield* InstanceState.use(state, (s) => s.config) })
    const directories = Effect.fn("Config.directories")(function* () { return yield* InstanceState.use(state, (s) => s.directories) })
    const getConsoleState = Effect.fn("Config.getConsoleState")(function* () { return yield* InstanceState.use(state, (s) => s.consoleState) })
    const waitForDependencies = Effect.fn("Config.waitForDependencies")(function* () {
      yield* InstanceState.useEffect(state, (s) =>
        Effect.forEach(s.deps, Fiber.join, { concurrency: "unbounded" }).pipe(Effect.asVoid),
      )
    })
    const warnings = Effect.fn("Config.warnings")(function* () { return yield* InstanceState.use(state, (s) => s.warnings) })

    const update = Effect.fn("Config.update")(function* (config: Info, options?: { dispose?: boolean }) {
      const ctx = yield* InstanceState.context
      yield* SaeeolConfig.updateProjectConfig({
        fs, directory: ctx.directory, worktree: ctx.worktree, config,
        read: $read, parse: (input, file) => ConfigParse.schema(Info.zod, ConfigParse.jsonc(input, file), file),
        patch: (input, p) => patchJsonc(input, p), writable,
      })
      if (options?.dispose !== false) {
        const ctx = yield* InstanceState.context
        yield* Effect.promise(() => InstanceStore.disposeInstance(ctx))
      }
    })

    const invalidate = Effect.fn("Config.invalidate")(function* (wait?: boolean) {
      yield* invalidateGlobal
      const task = InstanceStore.disposeAllInstances()
        .catch(() => undefined)
        .finally(() => GlobalBus.emit("event", { directory: "global", payload: { type: Event.Disposed.type, properties: {} } }))
      if (wait) yield* Effect.promise(() => task)
      else void task
    })

    const updateGlobal = Effect.fn("Config.updateGlobal")(function* (config: Info, options?: { dispose?: boolean }) {
      const dispose = options?.dispose ?? true
      const file = globalConfigFile()
      const before = (yield* $read(file)) ?? "{}"
      const patch = writableGlobal(config)
      let next: Info
      let changed: boolean
      if (!file.endsWith(".jsonc")) {
        const existing = ConfigParse.effectSchema(Info, ConfigParse.jsonc(before, file), file)
        const merged = SaeeolConfig.mergeConfig(writable(existing), patch)
        const serialized = JSON.stringify(merged, null, 2)
        changed = serialized !== before
        if (changed) yield* fs.writeFileString(file, serialized).pipe(Effect.orDie)
        next = merged
      } else {
        const updated = patchJsonc(before, patch)
        next = ConfigParse.effectSchema(Info, ConfigParse.jsonc(updated, file), file)
        changed = updated !== before
        if (changed) yield* fs.writeFileString(file, updated).pipe(Effect.orDie)
      }
      if (!dispose) {
        yield* invalidateGlobal
        yield* InstanceState.invalidate(state)
        yield* Effect.sync(() => GlobalBus.emit("event", { directory: "global", payload: { type: Event.ConfigUpdated.type, properties: {} } }))
        return next
      }
      if (changed) yield* invalidate()
      return next
    })

    return Service.of({ get, getGlobal, getConsoleState, update, updateGlobal, invalidate, directories, waitForDependencies, warnings })
  }),
)

export const defaultLayer = layer.pipe(
  Layer.provide(EffectFlock.defaultLayer),
  Layer.provide(AppFileSystem.defaultLayer),
  Layer.provide(Env.defaultLayer),
  Layer.provide(Auth.defaultLayer),
  Layer.provide(Account.defaultLayer),
  Layer.provide(Npm.defaultLayer),
)

const { runPromise } = makeRuntime(Service, defaultLayer)

export async function get() { return runPromise((svc) => svc.get()) }
export async function getGlobal() { return runPromise((svc) => svc.getGlobal()) }
export async function getConsoleState() { return runPromise((svc) => svc.getConsoleState()) }
export async function update(config: Info, options?: { dispose?: boolean }) { return runPromise((svc) => svc.update(config, options)) }
export async function updateGlobal(config: Info, options?: { dispose?: boolean }) { return runPromise((svc) => svc.updateGlobal(config, options)) }
export async function invalidate(wait = false) { return runPromise((svc) => svc.invalidate(wait)) }
export async function directories() { return runPromise((svc) => svc.directories()) }
export async function waitForDependencies() { return runPromise((svc) => svc.waitForDependencies()) }
export async function warnings() { return runPromise((svc) => svc.warnings()) }

export * as Config from "./config"
