import path from "path"
import { existsSync } from "fs"
import { Effect, Schema } from "effect"
import { mergeDeep } from "remeda"
import * as Log from "@saeeol/core/util/log"
import type { AppFileSystem } from "@saeeol/core/filesystem"
import { isRecord } from "@/util/record"
import type { Config } from "../../config/config"

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

export const CommitMessageSchema = Schema.optional(
  Schema.Struct({
    prompt: Schema.optional(Schema.String).annotate({
      description:
        "Custom system prompt for AI commit message generation. When set, replaces the default conventional commits prompt entirely.",
    }),
  }),
).annotate({ description: "Configuration for AI-generated commit messages" })

export const SAEEOL_CONFIG_FILES = ["saeeol.jsonc", "saeeol.json"] as const

export const ALL_CONFIG_FILES = ["saeeol.jsonc", "saeeol.json"] as const

export const SAEEOL_DIR_SUFFIXES = [".saeeol"] as const

export const ALL_CONFIG_DIR_SUFFIXES = [".saeeol"] as const

export const AGENT_PATTERNS = ["/.saeeol/agent/", "/.saeeol/agents/"] as const

export const COMMAND_PATTERNS = [
  "/.saeeol/command/",
  "/.saeeol/commands/",
] as const

export const projectConfigUpdateTarget = Effect.fn("SaeeolConfig.projectConfigUpdateTarget")(function* (input: {
  fs: AppFileSystem.Interface
  directory: string
  worktree?: string
}) {
  const dirs = yield* input.fs
    .up({ targets: [...ALL_CONFIG_DIR_SUFFIXES], start: input.directory, stop: input.worktree })
    .pipe(Effect.orDie)
  const roots = yield* input.fs
    .up({ targets: [...ALL_CONFIG_FILES], start: input.directory, stop: input.worktree })
    .pipe(Effect.orDie)
  const files = [...dirs.flatMap((dir) => ALL_CONFIG_FILES.map((file) => path.join(dir, file))), ...roots]
  return files.find((file) => existsSync(file)) ?? path.join(input.directory, ".saeeol", "saeeol.json")
})

type UpdateProjectConfigInput = {
  fs: AppFileSystem.Interface
  directory: string
  worktree?: string
  config: Config.Info
  read: (file: string) => Effect.Effect<string | undefined>
  parse: (input: string, file: string) => Config.Info
  patch: (input: string, config: Config.Info) => string
  writable: (config: Config.Info) => Config.Info
}

export const updateProjectConfig: (input: UpdateProjectConfigInput) => Effect.Effect<void> = Effect.fn("SaeeolConfig.updateProjectConfig")(function* (input: UpdateProjectConfigInput) {
  const file = yield* projectConfigUpdateTarget(input)
  const source = yield* input.read(file)
  const before = source ?? "{}"
  const patch = input.writable(input.config)

  if (file.endsWith(".jsonc")) {
    const updated = input.patch(before, patch)
    yield* input.fs.writeWithDirs(file, updated).pipe(Effect.orDie)
    return
  }

  const existing = input.parse(before, file)
  const merged = mergeConfig(input.writable(existing), patch)
  if (source === undefined && Object.keys(merged).length === 0) return
  yield* input.fs.writeWithDirs(file, JSON.stringify(merged, null, 2)).pipe(Effect.orDie)
})

export function scopeIndexing(info: Config.Info, scope: "global" | "local"): Config.Info {
  if (scope !== "global") return info
  return stripGlobalIndexing(info)
}

function stripGlobalIndexing(info: Config.Info): Config.Info {
  if (info.indexing?.enabled === undefined) return info
  const indexing = Object.fromEntries(Object.entries(info.indexing).filter(([key]) => key !== "enabled"))
  if (Object.keys(indexing).length > 0) return { ...info, indexing }
  const copy = { ...info }
  delete copy.indexing
  return copy
}

export function stripNulls(obj: Record<string, unknown>): Record<string, unknown> {
  const result: Record<string, unknown> = {}
  for (const [key, value] of Object.entries(obj)) {
    if (value === null) continue
    if (isRecord(value)) {
      const stripped = stripNulls(value)
      if (Object.keys(stripped).length > 0) result[key] = stripped
    } else {
      result[key] = value
    }
  }
  return result
}

export function mergeConfig(existing: Config.Info, patch: Config.Info): Config.Info {
  const e = { ...existing } as Record<string, unknown>
  const p = patch as Record<string, unknown>

  const existingPerm = e.permission
  const patchPerm = p.permission
  if (isRecord(existingPerm) && isRecord(patchPerm)) {
    const cloned = { ...existingPerm }
    for (const [key, value] of Object.entries(patchPerm)) {
      const existing = cloned[key]
      if (typeof existing === "string" && isRecord(value)) {
        cloned[key] = { "*": existing }
      }
    }
    e.permission = cloned
  }

  return stripNulls(mergeDeep(e, p) as Record<string, unknown>) as Config.Info
}

export function isConfigDir(dir: string, flagDir?: string): boolean {
  return dir.endsWith(".saeeol") || dir === flagDir
}
