export type JsonRecord = Record<string, unknown>

export interface CoreConfigForm {
  workspace: string
  restrictToWorkspace: boolean
  splitOnMarker: boolean
  toolFeedbackEnabled: boolean
  toolFeedbackMaxArgsLength: string
  toolFeedbackSeparateMessages: boolean
  execEnabled: boolean
  allowRemote: boolean
  enableDenyPatterns: boolean
  customDenyPatternsText: string
  customAllowPatternsText: string
  execTimeoutSeconds: string
  allowCommand: boolean
  cronExecTimeoutMinutes: string
  maxTokens: string
  contextWindow: string
  maxToolIterations: string
  summarizeMessageThreshold: string
  summarizeTokenPercent: string
  dmScope: string
  heartbeatEnabled: boolean
  heartbeatInterval: string
  devicesEnabled: boolean
  monitorUSB: boolean
  mcpEnabled: boolean
  mcpDiscoveryEnabled: boolean
  mcpDiscoveryTTL: string
  mcpDiscoveryMaxSearchResults: string
  mcpDiscoveryUseBM25: boolean
  mcpDiscoveryUseRegex: boolean
  mcpServers: MCPServerForm[]
  evolutionEnabled: boolean
  evolutionMode: string
  evolutionStateDir: string
  evolutionMinTaskCount: string
  evolutionMinSuccessRatio: string
  evolutionColdPathTrigger: string
  evolutionColdPathTimesText: string
}

export type MCPServerType = "http" | "sse" | "stdio"

export interface MCPServerForm {
  id: string
  name: string
  enabled: boolean
  deferredOverride: boolean | null
  type: MCPServerType
  url: string
  command: string
  argsText: string
  envText: string
  envFile: string
  headersText: string
}

export interface LauncherForm {
  port: string
  publicAccess: boolean
  allowedCIDRsText: string
  dashboardPassword: string
  dashboardPasswordConfirm: string
}

export const DM_SCOPE_OPTIONS = [
  {
    value: "per-channel-peer",
    labelKey: "pages.config.session_scope_per_channel_peer",
    labelDefault: "Per Channel + Peer",
    descKey: "pages.config.session_scope_per_channel_peer_desc",
    descDefault: "Separate context for each user in each channel.",
  },
  {
    value: "per-channel",
    labelKey: "pages.config.session_scope_per_channel",
    labelDefault: "Per Channel",
    descKey: "pages.config.session_scope_per_channel_desc",
    descDefault: "One shared context per channel.",
  },
  {
    value: "per-peer",
    labelKey: "pages.config.session_scope_per_peer",
    labelDefault: "Per Peer",
    descKey: "pages.config.session_scope_per_peer_desc",
    descDefault: "One context per user across channels.",
  },
  {
    value: "global",
    labelKey: "pages.config.session_scope_global",
    labelDefault: "Global",
    descKey: "pages.config.session_scope_global_desc",
    descDefault: "All messages share one global context.",
  },
] as const

export const EMPTY_FORM: CoreConfigForm = {
  workspace: "",
  restrictToWorkspace: true,
  splitOnMarker: false,
  toolFeedbackEnabled: false,
  toolFeedbackMaxArgsLength: "300",
  toolFeedbackSeparateMessages: false,
  execEnabled: true,
  allowRemote: true,
  enableDenyPatterns: true,
  customDenyPatternsText: "",
  customAllowPatternsText: "",
  execTimeoutSeconds: "0",
  allowCommand: true,
  cronExecTimeoutMinutes: "5",
  maxTokens: "32768",
  contextWindow: "",
  maxToolIterations: "50",
  summarizeMessageThreshold: "20",
  summarizeTokenPercent: "75",
  dmScope: "per-channel-peer",
  heartbeatEnabled: true,
  heartbeatInterval: "30",
  devicesEnabled: false,
  monitorUSB: true,
  mcpEnabled: false,
  mcpDiscoveryEnabled: false,
  mcpDiscoveryTTL: "5",
  mcpDiscoveryMaxSearchResults: "5",
  mcpDiscoveryUseBM25: true,
  mcpDiscoveryUseRegex: false,
  mcpServers: [],
  evolutionEnabled: false,
  evolutionMode: "observe",
  evolutionStateDir: "",
  evolutionMinTaskCount: "2",
  evolutionMinSuccessRatio: "0.7",
  evolutionColdPathTrigger: "after_turn",
  evolutionColdPathTimesText: "",
}

export const EMPTY_LAUNCHER_FORM: LauncherForm = {
  port: "18800",
  publicAccess: false,
  allowedCIDRsText: "",
  dashboardPassword: "",
  dashboardPasswordConfirm: "",
}

function asRecord(value: unknown): JsonRecord {
  if (value && typeof value === "object" && !Array.isArray(value)) {
    return value as JsonRecord
  }
  return {}
}

function asString(value: unknown): string {
  return typeof value === "string" ? value : ""
}

function asBool(value: unknown): boolean {
  return value === true
}

function asOptionalBool(value: unknown): boolean | null {
  return typeof value === "boolean" ? value : null
}

function asNumberString(value: unknown, fallback: string): string {
  if (typeof value === "number" && Number.isFinite(value)) {
    return String(value)
  }
  if (typeof value === "string" && value.trim() !== "") {
    return value
  }
  return fallback
}

function toMCPServerType(value: unknown): MCPServerType {
  if (value === "http" || value === "sse") {
    return value
  }
  return "stdio"
}

function makeMCPServerID(name: string): string {
  const encoded = encodeURIComponent(name)
  if (encoded.length > 0) {
    return `mcp-${encoded}`
  }
  return `mcp-${Math.random().toString(36).slice(2, 10)}`
}

function mapMCPServers(value: unknown): MCPServerForm[] {
  const servers = asRecord(value)
  return Object.entries(servers).map(([name, rawConfig]) => {
    const cfg = asRecord(rawConfig)
    const argsList = Array.isArray(cfg.args)
      ? cfg.args.filter((item): item is string => typeof item === "string")
      : []
    const url = asString(cfg.url)
    const type =
      cfg.type === undefined
        ? url
          ? "sse"
          : "stdio"
        : toMCPServerType(cfg.type)
    const env = asRecord(cfg.env)
    const headers = asRecord(cfg.headers)

    return {
      id: makeMCPServerID(name),
      name,
      enabled: cfg.enabled !== false,
      deferredOverride: asOptionalBool(cfg.deferred),
      type,
      url,
      command: asString(cfg.command),
      argsText: argsList.join("\n"),
      envText: JSON.stringify(env, null, 2),
      envFile: asString(cfg.env_file),
      headersText: JSON.stringify(headers, null, 2),
    }
  })
}

export function buildFormFromConfig(config: unknown): CoreConfigForm {
  const root = asRecord(config)
  const agents = asRecord(root.agents)
  const defaults = asRecord(agents.defaults)
  const session = asRecord(root.session)
  const heartbeat = asRecord(root.heartbeat)
  const devices = asRecord(root.devices)
  const evolution = asRecord(root.evolution)
  const tools = asRecord(root.tools)
  const mcp = asRecord(tools.mcp)
  const mcpDiscovery = asRecord(mcp.discovery)
  const cron = asRecord(tools.cron)
  const exec = asRecord(tools.exec)
  const toolFeedback = asRecord(defaults.tool_feedback)

  return {
    workspace: asString(defaults.workspace) || EMPTY_FORM.workspace,
    restrictToWorkspace:
      defaults.restrict_to_workspace === undefined
        ? EMPTY_FORM.restrictToWorkspace
        : asBool(defaults.restrict_to_workspace),
    splitOnMarker:
      defaults.split_on_marker === undefined
        ? EMPTY_FORM.splitOnMarker
        : asBool(defaults.split_on_marker),
    toolFeedbackEnabled:
      toolFeedback.enabled === undefined
        ? EMPTY_FORM.toolFeedbackEnabled
        : asBool(toolFeedback.enabled),
    toolFeedbackMaxArgsLength: asNumberString(
      toolFeedback.max_args_length,
      EMPTY_FORM.toolFeedbackMaxArgsLength,
    ),
    toolFeedbackSeparateMessages:
      toolFeedback.separate_messages === undefined
        ? EMPTY_FORM.toolFeedbackSeparateMessages
        : asBool(toolFeedback.separate_messages),
    execEnabled:
      exec.enabled === undefined
        ? EMPTY_FORM.execEnabled
        : asBool(exec.enabled),
    allowRemote:
      exec.allow_remote === undefined
        ? EMPTY_FORM.allowRemote
        : asBool(exec.allow_remote),
    enableDenyPatterns:
      exec.enable_deny_patterns === undefined
        ? EMPTY_FORM.enableDenyPatterns
        : asBool(exec.enable_deny_patterns),
    customDenyPatternsText: Array.isArray(exec.custom_deny_patterns)
      ? exec.custom_deny_patterns
          .filter((value): value is string => typeof value === "string")
          .join("\n")
      : EMPTY_FORM.customDenyPatternsText,
    customAllowPatternsText: Array.isArray(exec.custom_allow_patterns)
      ? exec.custom_allow_patterns
          .filter((value): value is string => typeof value === "string")
          .join("\n")
      : EMPTY_FORM.customAllowPatternsText,
    execTimeoutSeconds: asNumberString(
      exec.timeout_seconds,
      EMPTY_FORM.execTimeoutSeconds,
    ),
    allowCommand:
      cron.allow_command === undefined
        ? EMPTY_FORM.allowCommand
        : asBool(cron.allow_command),
    cronExecTimeoutMinutes: asNumberString(
      cron.exec_timeout_minutes,
      EMPTY_FORM.cronExecTimeoutMinutes,
    ),
    maxTokens: asNumberString(defaults.max_tokens, EMPTY_FORM.maxTokens),
    contextWindow: asNumberString(
      defaults.context_window,
      EMPTY_FORM.contextWindow,
    ),
    maxToolIterations: asNumberString(
      defaults.max_tool_iterations,
      EMPTY_FORM.maxToolIterations,
    ),
    summarizeMessageThreshold: asNumberString(
      defaults.summarize_message_threshold,
      EMPTY_FORM.summarizeMessageThreshold,
    ),
    summarizeTokenPercent: asNumberString(
      defaults.summarize_token_percent,
      EMPTY_FORM.summarizeTokenPercent,
    ),
    dmScope: asString(session.dm_scope) || EMPTY_FORM.dmScope,
    heartbeatEnabled:
      heartbeat.enabled === undefined
        ? EMPTY_FORM.heartbeatEnabled
        : asBool(heartbeat.enabled),
    heartbeatInterval: asNumberString(
      heartbeat.interval,
      EMPTY_FORM.heartbeatInterval,
    ),
    devicesEnabled:
      devices.enabled === undefined
        ? EMPTY_FORM.devicesEnabled
        : asBool(devices.enabled),
    monitorUSB:
      devices.monitor_usb === undefined
        ? EMPTY_FORM.monitorUSB
        : asBool(devices.monitor_usb),
    mcpEnabled:
      mcp.enabled === undefined ? EMPTY_FORM.mcpEnabled : asBool(mcp.enabled),
    mcpDiscoveryEnabled:
      mcpDiscovery.enabled === undefined
        ? EMPTY_FORM.mcpDiscoveryEnabled
        : asBool(mcpDiscovery.enabled),
    mcpDiscoveryTTL: asNumberString(
      mcpDiscovery.ttl,
      EMPTY_FORM.mcpDiscoveryTTL,
    ),
    mcpDiscoveryMaxSearchResults: asNumberString(
      mcpDiscovery.max_search_results,
      EMPTY_FORM.mcpDiscoveryMaxSearchResults,
    ),
    mcpDiscoveryUseBM25:
      mcpDiscovery.use_bm25 === undefined
        ? EMPTY_FORM.mcpDiscoveryUseBM25
        : asBool(mcpDiscovery.use_bm25),
    mcpDiscoveryUseRegex:
      mcpDiscovery.use_regex === undefined
        ? EMPTY_FORM.mcpDiscoveryUseRegex
        : asBool(mcpDiscovery.use_regex),
    mcpServers: mapMCPServers(mcp.servers),
    evolutionEnabled:
      evolution.enabled === undefined
        ? EMPTY_FORM.evolutionEnabled
        : asBool(evolution.enabled),
    evolutionMode: asString(evolution.mode) || EMPTY_FORM.evolutionMode,
    evolutionStateDir:
      asString(evolution.state_dir) || EMPTY_FORM.evolutionStateDir,
    evolutionMinTaskCount: asNumberString(
      evolution.min_task_count,
      EMPTY_FORM.evolutionMinTaskCount,
    ),
    evolutionMinSuccessRatio: asNumberString(
      evolution.min_success_ratio,
      EMPTY_FORM.evolutionMinSuccessRatio,
    ),
    evolutionColdPathTrigger:
      asString(evolution.cold_path_trigger) ||
      EMPTY_FORM.evolutionColdPathTrigger,
    evolutionColdPathTimesText: Array.isArray(evolution.cold_path_times)
      ? evolution.cold_path_times
          .filter((value): value is string => typeof value === "string")
          .join("\n")
      : EMPTY_FORM.evolutionColdPathTimesText,
  }
}

export function parseIntField(
  rawValue: string,
  label: string,
  options: { min?: number; max?: number } = {},
): number {
  const value = Number(rawValue)
  if (!Number.isInteger(value)) {
    throw new Error(`${label} must be an integer.`)
  }
  if (options.min !== undefined && value < options.min) {
    throw new Error(`${label} must be >= ${options.min}.`)
  }
  if (options.max !== undefined && value > options.max) {
    throw new Error(`${label} must be <= ${options.max}.`)
  }
  return value
}

export function parseFloatField(
  rawValue: string,
  label: string,
  options: { min?: number; max?: number } = {},
): number {
  const value = Number(rawValue)
  if (!Number.isFinite(value)) {
    throw new Error(`${label} must be a number.`)
  }
  if (options.min !== undefined && value < options.min) {
    throw new Error(`${label} must be >= ${options.min}.`)
  }
  if (options.max !== undefined && value > options.max) {
    throw new Error(`${label} must be <= ${options.max}.`)
  }
  return value
}

export function parseCIDRText(raw: string): string[] {
  if (!raw.trim()) {
    return []
  }
  return raw
    .split(/[\n,]/)
    .map((v) => v.trim())
    .filter((v) => v.length > 0)
}

export function parseMultilineList(raw: string): string[] {
  if (!raw.trim()) {
    return []
  }
  return raw
    .split("\n")
    .map((value) => value.trim())
    .filter((value) => value.length > 0)
}

export function parseJSONObjectField(
  rawValue: string,
  label: string,
): Record<string, string> {
  const trimmed = rawValue.trim()
  if (trimmed === "") {
    return {}
  }

  let parsed: unknown
  try {
    parsed = JSON.parse(trimmed)
  } catch {
    throw new Error(`${label} must be valid JSON.`)
  }

  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
    throw new Error(`${label} must be a JSON object.`)
  }

  const entries = Object.entries(parsed as Record<string, unknown>)
  const result: Record<string, string> = {}
  for (const [key, value] of entries) {
    if (typeof value !== "string") {
      throw new Error(`${label}.${key} must be a string.`)
    }
    result[key] = value
  }
  return result
}
