import z from "zod"
import { Context, Effect, Schema } from "effect"
import { zod, ZodOverride } from "@/util/effect-zod"
import { NonNegativeInt, PositiveInt, withStatics, type DeepMutable } from "@/util/schema"
import { ConfigAgent } from "./agent"
import { ConfigCommand } from "./command"
import { ConfigFormatter } from "./formatter"
import { ConfigLayout } from "./layout"
import { ConfigLSP } from "./lsp"
import { ConfigMCP } from "./mcp"
import { ConfigModelID } from "./model-id"
import { ConfigPermission } from "./permission"
import { ConfigPlugin } from "./plugin"
import { ConfigProvider } from "./provider"
import { ConfigServer } from "./server"
import { ConfigSkills } from "./skills"
import { SaeeolConfig } from "../overlay/config/config"
import { IndexingConfig as SaeeolIndexingConfig } from "@saeeol/indexing/config"

export const Warning = z.object({
  path: z.string(),
  message: z.string(),
  detail: z.string().optional(),
})
export type Warning = z.infer<typeof Warning>

export const Server = ConfigServer.Server.zod
export const Layout = ConfigLayout.Layout.zod
export type Layout = ConfigLayout.Layout
export const Indexing = SaeeolIndexingConfig
export type Indexing = z.infer<typeof Indexing>

const LogLevelRef = Schema.Literals(["DEBUG", "INFO", "WARN", "ERROR"]).annotate({
  identifier: "LogLevel",
  description: "Log level",
})
const Percent = Schema.Number.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(100))
const IndexingRef = Schema.Any.annotate({ [ZodOverride]: SaeeolIndexingConfig })

export const Info = Schema.Struct({
  $schema: Schema.optional(Schema.String).annotate({
    description: "JSON schema reference for configuration validation",
  }),
  shell: Schema.optional(Schema.String).annotate({
    description: "Default shell to use for terminal and bash tool",
  }),
  logLevel: Schema.optional(LogLevelRef).annotate({ description: "Log level" }),
  server: Schema.optional(ConfigServer.Server).annotate({
    description: "Server configuration for saeeol serve and web commands",
  }),
  command: Schema.optional(Schema.Record(Schema.String, ConfigCommand.Info)).annotate({
    description: "Command configuration, see https://saeeol.ai/docs/commands",
  }),
  skills: Schema.optional(ConfigSkills.Info).annotate({ description: "Additional skill folder paths" }),
  watcher: Schema.optional(
    Schema.Struct({
      ignore: Schema.optional(Schema.mutable(Schema.Array(Schema.String))),
    }),
  ),
  snapshot: Schema.optional(Schema.Boolean).annotate({
    description:
      "Enable or disable snapshot tracking. When false, filesystem snapshots are not recorded and undoing or reverting will not undo/redo file changes. Defaults to true.",
  }),
  plugin: Schema.optional(Schema.mutable(Schema.Array(ConfigPlugin.Spec))),
  share: Schema.optional(Schema.Literals(["manual", "auto", "disabled"])).annotate({
    description:
      "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing",
  }),
  autoshare: Schema.optional(Schema.Boolean).annotate({
    description: "@deprecated Use 'share' field instead. Share newly created sessions automatically",
  }),
  autoupdate: Schema.optional(Schema.Union([Schema.Boolean, Schema.Literal("notify")])).annotate({
    description:
      "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications",
  }),
  disabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
    description: "Disable providers that are loaded automatically",
  }),
  enabled_providers: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
    description: "When set, ONLY these providers will be enabled. All other providers will be ignored",
  }),
  remote_control: Schema.optional(Schema.Boolean).annotate({
    description: "Enable remote control of sessions via Saeeol Cloud. Equivalent to running /remote on startup.",
  }),
  auto_collapse_reasoning: Schema.optional(Schema.Boolean).annotate({
    description: "Automatically collapse reasoning blocks after the agent finishes writing them",
  }),
  indexing: Schema.optional(IndexingRef).annotate({ description: "Codebase indexing configuration" }),
  terminal_command_display: Schema.optional(Schema.Literals(["expanded", "collapsed"])).annotate({
    description: "Controls whether terminal command blocks are expanded or collapsed by default in the VS Code chat UI",
  }),
  model: Schema.optional(Schema.NullOr(ConfigModelID)).annotate({
    description: "Model to use in the format of provider/model, eg anthropic/claude-2",
  }),
  small_model: Schema.optional(Schema.NullOr(ConfigModelID)).annotate({
    description: "Small model to use for tasks like title generation in the format of provider/model",
  }),
  default_agent: Schema.optional(Schema.NullOr(Schema.String)).annotate({
    description:
      "Default agent to use when none is specified. Must be a primary agent. Falls back to 'code' if not set or if the specified agent is invalid.",
  }),
  username: Schema.optional(Schema.String).annotate({
    description: "Custom username to display in conversations instead of system username",
  }),
  mode: Schema.optional(
    Schema.StructWithRest(
      Schema.Struct({
        build: Schema.optional(ConfigAgent.Info),
        plan: Schema.optional(ConfigAgent.Info),
      }),
      [Schema.Record(Schema.String, ConfigAgent.Info)],
    ),
  ).annotate({ description: "@deprecated Use `agent` field instead." }),
  agent: Schema.optional(
    Schema.StructWithRest(
      Schema.Struct({
        plan: Schema.optional(ConfigAgent.Info),
        build: Schema.optional(ConfigAgent.Info),
        debug: Schema.optional(ConfigAgent.Info),
        orchestrator: Schema.optional(ConfigAgent.Info),
        ask: Schema.optional(ConfigAgent.Info),
        general: Schema.optional(ConfigAgent.Info),
        explore: Schema.optional(ConfigAgent.Info),
        title: Schema.optional(ConfigAgent.Info),
        summary: Schema.optional(ConfigAgent.Info),
        compaction: Schema.optional(ConfigAgent.Info),
      }),
      [Schema.Record(Schema.String, ConfigAgent.Info)],
    ),
  ).annotate({ description: "Agent configuration, see https://saeeol.ai/docs/agents" }),
  provider: Schema.optional(Schema.Record(Schema.String, Schema.NullOr(ConfigProvider.Info))).annotate({
    description: "Custom provider configurations and model overrides",
  }),
  mcp: Schema.optional(
    Schema.Record(
      Schema.String,
      Schema.Union([
        ConfigMCP.Info,
        Schema.Struct({ enabled: Schema.Boolean }),
      ]),
    ),
  ).annotate({ description: "MCP (Model Context Protocol) server configurations" }),
  formatter: Schema.optional(ConfigFormatter.Info),
  lsp: Schema.optional(ConfigLSP.Info),
  instructions: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
    description: "Additional instruction files or patterns to include",
  }),
  layout: Schema.optional(ConfigLayout.Layout).annotate({ description: "@deprecated Always uses stretch layout." }),
  permission: Schema.optional(ConfigPermission.Info),
  tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)),
  enterprise: Schema.optional(
    Schema.Struct({
      url: Schema.optional(Schema.String).annotate({ description: "Enterprise URL" }),
    }),
  ),
  commit_message: SaeeolConfig.CommitMessageSchema,
  tool_output: Schema.optional(
    Schema.Struct({
      max_lines: Schema.optional(PositiveInt).annotate({
        description: "Maximum lines of tool output before it is truncated and saved to disk (default: 2000)",
      }),
      max_bytes: Schema.optional(PositiveInt).annotate({
        description: "Maximum bytes of tool output before it is truncated and saved to disk (default: 51200)",
      }),
    }),
  ).annotate({
    description:
      "Thresholds for truncating tool output. When output exceeds either limit, the full text is written to the truncation directory and a preview is returned.",
  }),
  compaction: Schema.optional(
    Schema.Struct({
      auto: Schema.optional(Schema.Boolean).annotate({
        description: "Enable automatic compaction when context is full (default: true)",
      }),
      threshold_percent: Schema.optional(Schema.NullOr(Percent)).annotate({
        description:
          "Percentage of the model input/context window that triggers automatic compaction. The reserved safety buffer still applies if it would compact sooner.",
      }),
      prune: Schema.optional(Schema.Boolean).annotate({
        description: "Enable pruning of old tool outputs (default: true)",
      }),
      tail_turns: Schema.optional(NonNegativeInt).annotate({
        description:
          "Number of recent user turns, including their following assistant/tool responses, to keep verbatim during compaction (default: 2)",
      }),
      preserve_recent_tokens: Schema.optional(NonNegativeInt).annotate({
        description: "Maximum number of tokens from recent turns to preserve verbatim after compaction",
      }),
      reserved: Schema.optional(NonNegativeInt).annotate({
        description: "Token buffer for compaction. Leaves enough window to avoid overflow during compaction.",
      }),
      batch_size: Schema.optional(NonNegativeInt).annotate({
        description: "Number of messages per batch during /compact (default: 10)",
      }),
      auto_batches: Schema.optional(NonNegativeInt).annotate({
        description: "Number of oldest batches to summarize during auto-compaction (default: 5)",
      }),
      keep_head: Schema.optional(NonNegativeInt).annotate({
        description: "Messages to preserve at the start of each batch during /compact (default: 2)",
      }),
      keep_tail: Schema.optional(NonNegativeInt).annotate({
        description: "Messages to preserve at the end of each batch during /compact (default: 2)",
      }),
    }),
  ),
  experimental: Schema.optional(
    Schema.Struct({
      disable_paste_summary: Schema.optional(Schema.Boolean),
      batch_tool: Schema.optional(Schema.Boolean).annotate({ description: "Enable the batch tool" }),
      codebase_search: Schema.optional(Schema.Boolean).annotate({ description: "Enable AI-powered codebase search" }),
      semantic_indexing: Schema.optional(Schema.Boolean).annotate({
        description: "Enable semantic codebase indexing and the semantic_search tool",
      }),
      agent_manager_tool: Schema.optional(Schema.Boolean).annotate({
        description: "Enable the VS Code Agent Manager orchestration tool",
      }),
      openTelemetry: Schema.Boolean.pipe(Schema.optional, Schema.withDecodingDefault(Effect.succeed(true))).annotate({
        description: "Enable telemetry. Set to false to opt-out.",
      }),
      primary_tools: Schema.optional(Schema.mutable(Schema.Array(Schema.String))).annotate({
        description: "Tools that should only be available to primary agents.",
      }),
      continue_loop_on_deny: Schema.optional(Schema.Boolean).annotate({
        description: "Continue the agent loop when a tool call is denied",
      }),
      mcp_timeout: Schema.optional(PositiveInt).annotate({
        description: "Timeout in milliseconds for model context protocol (MCP) requests",
      }),
    }),
  ),
  ltm: Schema.optional(
    Schema.Struct({
      enabled: Schema.optional(Schema.Boolean).annotate({
        description: "Enable long-term memory with local embedding (default: false)",
      }),
      embedding_model: Schema.optional(Schema.String).annotate({
        description: "Embedding model ID or 'auto' for VRAM-based selection (default: auto)",
      }),
      vector_store: Schema.optional(Schema.Literals(["filesystem", "qdrant", "chroma"])).annotate({
        description: "Vector store backend (default: filesystem)",
      }),
      max_memories: Schema.optional(PositiveInt).annotate({
        description: "Maximum number of memories to retain (default: 10000)",
      }),
      episodic: Schema.optional(
        Schema.Struct({
          enabled: Schema.optional(Schema.Boolean).annotate({ description: "Enable episodic (conversation) memory" }),
          summary_interval: Schema.optional(PositiveInt).annotate({ description: "Summarize every N turns (default: 10)" }),
          retain_days: Schema.optional(PositiveInt).annotate({ description: "Days to retain episodic memories (default: 90)" }),
        }),
      ),
      semantic: Schema.optional(
        Schema.Struct({
          enabled: Schema.optional(Schema.Boolean).annotate({ description: "Enable semantic (code) memory" }),
          index_on_file_change: Schema.optional(Schema.Boolean).annotate({ description: "Auto-index on file change (default: true)" }),
        }),
      ),
      procedural: Schema.optional(
        Schema.Struct({
          enabled: Schema.optional(Schema.Boolean).annotate({ description: "Enable procedural (preference) memory" }),
          track_preferences: Schema.optional(Schema.Boolean).annotate({ description: "Track coding style preferences (default: true)" }),
        }),
      ),
      retrieval: Schema.optional(
        Schema.Struct({
          top_k: Schema.optional(PositiveInt).annotate({ description: "Number of memories to retrieve (default: 5)" }),
          min_score: Schema.optional(Schema.Number).annotate({ description: "Minimum similarity score 0-1 (default: 0.7)" }),
          max_tokens: Schema.optional(PositiveInt).annotate({ description: "Max tokens to inject into prompt (default: 2000)" }),
        }),
      ),
    }),
  ).annotate({
    description: "Long-term memory configuration with local embedding model",
  }),
})
  .annotate({ identifier: "Config" })
  .pipe(
    withStatics((s) => ({
      zod: (zod(s) as unknown as z.ZodObject<any>).strict().meta({ ref: "Config" }) as unknown as z.ZodType<
        DeepMutable<Schema.Schema.Type<typeof s>>
      >,
    })),
  )

export type Info = DeepMutable<Schema.Schema.Type<typeof Info>> & {
  plugin_origins?: ConfigPlugin.Origin[]
}
