/**
 * Agent Config Sync
 *
 * Reads agents from openclaw.json and upserts them into the MC database.
 * Used by both the /api/agents/sync endpoint and the startup scheduler.
 */

import { config } from './config'
import { getDatabase, db_helpers, logAuditEvent } from './db'
import { eventBus } from './event-bus'
import { join, isAbsolute, resolve } from 'path'
import { existsSync, readFileSync, statSync } from 'fs'
import { resolveWithin } from './paths'
import { logger } from './logger'
import { parseJsonRelaxed } from './json-relaxed'

interface OpenClawAgent {
  id: string
  name?: string
  default?: boolean
  workspace?: string
  agentDir?: string
  model?: {
    primary?: string
    fallbacks?: string[]
  }
  identity?: {
    name?: string
    theme?: string
    emoji?: string
  }
  subagents?: any
  sandbox?: {
    mode?: string
    workspaceAccess?: string
    scope?: string
    docker?: any
  }
  tools?: {
    allow?: string[]
    deny?: string[]
  }
  memorySearch?: any
}

export interface SyncResult {
  synced: number
  created: number
  updated: number
  agents: Array<{
    id: string
    name: string
    action: 'created' | 'updated' | 'unchanged'
  }>
  error?: string
}

export interface SyncDiff {
  inConfig: number
  inMC: number
  newAgents: string[]
  updatedAgents: string[]
  onlyInMC: string[]
}

function parseIdentityFromFile(content: string): { name?: string; theme?: string; emoji?: string; content?: string } {
  if (!content.trim()) return {}
  const lines = content.split('\n').map((line) => line.trim()).filter(Boolean)
  let name: string | undefined
  let theme: string | undefined
  let emoji: string | undefined

  for (const line of lines) {
    if (!name && line.startsWith('#')) {
      name = line.replace(/^#+\s*/, '').trim()
      continue
    }

    if (!theme) {
      const themeMatch = line.match(/^theme\s*:\s*(.+)$/i)
      if (themeMatch?.[1]) {
        theme = themeMatch[1].trim()
        continue
      }
    }

    if (!emoji) {
      const emojiMatch = line.match(/^emoji\s*:\s*(.+)$/i)
      if (emojiMatch?.[1]) {
        emoji = emojiMatch[1].trim()
      }
    }
  }

  return {
    ...(name ? { name } : {}),
    ...(theme ? { theme } : {}),
    ...(emoji ? { emoji } : {}),
    content: lines.slice(0, 8).join('\n'),
  }
}

function parseToolsFromFile(content: string): { allow?: string[]; raw?: string } {
  if (!content.trim()) return {}

  const parsedTools = new Set<string>()
  for (const line of content.split('\n')) {
    const cleaned = line.trim()
    if (!cleaned || cleaned.startsWith('#')) continue

    const listMatch = cleaned.match(/^[-*]\s+`?([^`]+?)`?\s*$/)
    if (listMatch?.[1]) {
      parsedTools.add(listMatch[1].trim())
      continue
    }

    const inlineMatch = cleaned.match(/^`([^`]+)`$/)
    if (inlineMatch?.[1]) {
      parsedTools.add(inlineMatch[1].trim())
    }
  }

  const allow = [...parsedTools].filter(Boolean)
  return {
    ...(allow.length > 0 ? { allow } : {}),
    raw: content.split('\n').map((line) => line.trim()).filter(Boolean).slice(0, 24).join('\n'),
  }
}

function getConfigPath(): string | null {
  return config.openclawConfigPath || null
}

function resolveAgentWorkspacePath(workspace: string): string {
  if (isAbsolute(workspace)) return resolve(workspace)
  if (!config.openclawStateDir) {
    throw new Error('OPENCLAW_STATE_DIR not configured')
  }
  return resolveWithin(config.openclawStateDir, workspace)
}

const MAX_WORKSPACE_FILE_BYTES = 1024 * 1024 // 1 MB

/** Safely read a file from an agent's workspace directory */
function readWorkspaceFile(workspace: string | undefined, filename: string): string | null {
  if (!workspace) return null
  try {
    const safeWorkspace = resolveAgentWorkspacePath(workspace)
    const safePath = resolveWithin(safeWorkspace, filename)
    if (existsSync(safePath)) {
      const size = statSync(safePath).size
      if (size > MAX_WORKSPACE_FILE_BYTES) {
        logger.warn({ workspace, filename, size }, `Workspace file exceeds ${MAX_WORKSPACE_FILE_BYTES} byte limit, skipping`)
        return null
      }
      return readFileSync(safePath, 'utf-8')
    }
  } catch (err) {
    logger.warn({ err, workspace, filename }, 'Failed to read workspace file')
  }
  return null
}

export function enrichAgentConfigFromWorkspace(configData: any): any {
  if (!configData || typeof configData !== 'object') return configData
  const workspace = typeof configData.workspace === 'string' ? configData.workspace : undefined
  if (!workspace) return configData

  const identityFile = readWorkspaceFile(workspace, 'identity.md')
  const toolsFile = readWorkspaceFile(workspace, 'TOOLS.md')

  const mergedIdentity = {
    ...parseIdentityFromFile(identityFile || ''),
    ...((configData.identity && typeof configData.identity === 'object') ? configData.identity : {}),
  }
  const mergedTools = {
    ...parseToolsFromFile(toolsFile || ''),
    ...((configData.tools && typeof configData.tools === 'object') ? configData.tools : {}),
  }

  return {
    ...configData,
    identity: Object.keys(mergedIdentity).length > 0 ? mergedIdentity : configData.identity,
    tools: Object.keys(mergedTools).length > 0 ? mergedTools : configData.tools,
  }
}

/** Read and parse openclaw.json agents list */
async function readOpenClawAgents(): Promise<OpenClawAgent[]> {
  const configPath = getConfigPath()
  if (!configPath) throw new Error('OPENCLAW_CONFIG_PATH not configured')

  const { readFile } = require('fs/promises')
  const raw = await readFile(configPath, 'utf-8')
  const parsed = parseJsonRelaxed<any>(raw)
  return parsed?.agents?.list || []
}

/** Extract MC-friendly fields from an OpenClaw agent config */
function mapAgentToMC(agent: OpenClawAgent): {
  name: string
  role: string
  config: any
  soul_content: string | null
} {
  const name = agent.identity?.name || agent.name || agent.id
  const role = agent.identity?.theme || 'agent'
  // Store the full config minus systemPrompt/soul (which can be large)
  const configData = enrichAgentConfigFromWorkspace({
    openclawId: agent.id,
    model: agent.model,
    identity: agent.identity,
    sandbox: agent.sandbox,
    tools: agent.tools,
    subagents: agent.subagents,
    memorySearch: agent.memorySearch,
    workspace: agent.workspace,
    agentDir: agent.agentDir,
    isDefault: agent.default || false,
  })

  // Read soul.md from the agent's workspace if available
  const soul_content = readWorkspaceFile(agent.workspace, 'soul.md')

  return { name, role, config: configData, soul_content }
}

/** Sync agents from openclaw.json into the MC database */
export async function syncAgentsFromConfig(actor: string = 'system'): Promise<SyncResult> {
  let agents: OpenClawAgent[]
  try {
    agents = await readOpenClawAgents()
  } catch (err: any) {
    return { synced: 0, created: 0, updated: 0, agents: [], error: err.message }
  }

  if (agents.length === 0) {
    return { synced: 0, created: 0, updated: 0, agents: [] }
  }

  const db = getDatabase()
  const now = Math.floor(Date.now() / 1000)
  let created = 0
  let updated = 0
  const results: SyncResult['agents'] = []

  const findByName = db.prepare('SELECT id, name, role, config, soul_content FROM agents WHERE name = ?')
  const insertAgent = db.prepare(`
    INSERT INTO agents (name, role, soul_content, status, created_at, updated_at, config)
    VALUES (?, ?, ?, 'offline', ?, ?, ?)
  `)
  const updateAgent = db.prepare(`
    UPDATE agents SET role = ?, config = ?, soul_content = ?, updated_at = ? WHERE name = ?
  `)

  db.transaction(() => {
    for (const agent of agents) {
      const mapped = mapAgentToMC(agent)
      const configJson = JSON.stringify(mapped.config)
      const existing = findByName.get(mapped.name) as any

      if (existing) {
        // Check if config or soul_content actually changed
        const existingConfig = existing.config || '{}'
        const existingSoul = existing.soul_content || null
        const configChanged = existingConfig !== configJson || existing.role !== mapped.role
        const soulChanged = mapped.soul_content !== null && mapped.soul_content !== existingSoul

        if (configChanged || soulChanged) {
          // Only overwrite soul_content if we read a new value from workspace
          const soulToWrite = mapped.soul_content ?? existingSoul
          updateAgent.run(mapped.role, configJson, soulToWrite, now, mapped.name)
          results.push({ id: agent.id, name: mapped.name, action: 'updated' })
          updated++
        } else {
          results.push({ id: agent.id, name: mapped.name, action: 'unchanged' })
        }
      } else {
        insertAgent.run(mapped.name, mapped.role, mapped.soul_content, now, now, configJson)
        results.push({ id: agent.id, name: mapped.name, action: 'created' })
        created++
      }
    }
  })()

  const synced = agents.length

  // Log audit event
  if (created > 0 || updated > 0) {
    logAuditEvent({
      action: 'agent_config_sync',
      actor,
      detail: { synced, created, updated, agents: results.filter(a => a.action !== 'unchanged').map(a => a.name) },
    })

    // Broadcast sync event
    eventBus.broadcast('agent.created', { type: 'sync', synced, created, updated })
  }

  logger.info({ synced, created, updated }, 'Agent sync complete')
  return { synced, created, updated, agents: results }
}

/** Preview the diff between openclaw.json and MC database without writing */
export async function previewSyncDiff(): Promise<SyncDiff> {
  let agents: OpenClawAgent[]
  try {
    agents = await readOpenClawAgents()
  } catch {
    return { inConfig: 0, inMC: 0, newAgents: [], updatedAgents: [], onlyInMC: [] }
  }

  const db = getDatabase()
  const allMCAgents = db.prepare('SELECT name, role, config FROM agents').all() as Array<{ name: string; role: string; config: string }>
  const mcNames = new Set(allMCAgents.map(a => a.name))

  const newAgents: string[] = []
  const updatedAgents: string[] = []
  const configNames = new Set<string>()

  for (const agent of agents) {
    const mapped = mapAgentToMC(agent)
    configNames.add(mapped.name)

    const existing = allMCAgents.find(a => a.name === mapped.name)
    if (!existing) {
      newAgents.push(mapped.name)
    } else {
      const configJson = JSON.stringify(mapped.config)
      if (existing.config !== configJson || existing.role !== mapped.role) {
        updatedAgents.push(mapped.name)
      }
    }
  }

  const onlyInMC = allMCAgents
    .map(a => a.name)
    .filter(name => !configNames.has(name))

  return {
    inConfig: agents.length,
    inMC: allMCAgents.length,
    newAgents,
    updatedAgents,
    onlyInMC,
  }
}

/** Write an agent config back to openclaw.json agents.list */
export async function writeAgentToConfig(agentConfig: any): Promise<void> {
  const configPath = getConfigPath()
  if (!configPath) throw new Error('OPENCLAW_CONFIG_PATH not configured')

  const { readFile, writeFile } = require('fs/promises')
  const raw = await readFile(configPath, 'utf-8')
  const parsed = parseJsonRelaxed<any>(raw)

  if (!parsed.agents) parsed.agents = {}
  if (!parsed.agents.list) parsed.agents.list = []

  const normalizedAgentConfig = normalizeAgentConfigForOpenClaw(agentConfig)

  // Find existing by id
  const idx = parsed.agents.list.findIndex((a: any) => a.id === normalizedAgentConfig.id)
  if (idx >= 0) {
    // Deep merge: preserve fields not in update
    parsed.agents.list[idx] = normalizeAgentConfigForOpenClaw(
      deepMerge(parsed.agents.list[idx], normalizedAgentConfig),
    )
  } else {
    parsed.agents.list.push(normalizedAgentConfig)
  }

  await writeFile(configPath, JSON.stringify(parsed, null, 2) + '\n')
}

export async function removeAgentFromConfig(match: {
  id?: string | null
  name?: string | null
}): Promise<{ removed: boolean }> {
  const configPath = getConfigPath()
  if (!configPath) throw new Error('OPENCLAW_CONFIG_PATH not configured')

  const id = String(match.id || '').trim()
  const name = String(match.name || '').trim()
  if (!id && !name) {
    return { removed: false }
  }

  const { readFile, writeFile } = require('fs/promises')
  const raw = await readFile(configPath, 'utf-8')
  const parsed = parseJsonRelaxed<any>(raw)
  const existingList = Array.isArray(parsed?.agents?.list) ? parsed.agents.list : []

  const nextList = existingList.filter((agent: any) => {
    const agentId = String(agent?.id || '').trim()
    const agentName = String(agent?.name || '').trim()
    const identityName = String(agent?.identity?.name || '').trim()

    if (id && agentId === id) return false
    if (name && (agentName === name || identityName === name)) return false
    return true
  })

  if (nextList.length === existingList.length) {
    return { removed: false }
  }

  if (!parsed.agents) parsed.agents = {}
  parsed.agents.list = nextList
  await writeFile(configPath, JSON.stringify(parsed, null, 2) + '\n')
  return { removed: true }
}

/** Deep merge two objects (target <- source), preserving target fields not in source */
function deepMerge(target: any, source: any): any {
  const result = { ...target }
  for (const key of Object.keys(source)) {
    if (
      source[key] &&
      typeof source[key] === 'object' &&
      !Array.isArray(source[key]) &&
      target[key] &&
      typeof target[key] === 'object' &&
      !Array.isArray(target[key])
    ) {
      result[key] = deepMerge(target[key], source[key])
    } else {
      result[key] = source[key]
    }
  }
  return result
}

function normalizeModelConfig(model: unknown): unknown {
  if (!model || typeof model !== 'object' || Array.isArray(model)) return model

  const current = { ...(model as Record<string, unknown>) }
  let primary = current.primary

  while (primary && typeof primary === 'object' && !Array.isArray(primary)) {
    const nestedPrimary = (primary as Record<string, unknown>).primary
    if (typeof nestedPrimary !== 'string') break
    primary = nestedPrimary
  }

  const normalizedFallbacks = Array.isArray(current.fallbacks)
    ? [...new Set(current.fallbacks.map((value) => String(value || '').trim()).filter(Boolean))]
    : current.fallbacks

  return {
    ...current,
    ...(typeof primary === 'string' ? { primary } : {}),
    ...(Array.isArray(normalizedFallbacks) ? { fallbacks: normalizedFallbacks } : {}),
  }
}

function normalizeAgentConfigForOpenClaw(agentConfig: any): any {
  if (!agentConfig || typeof agentConfig !== 'object') return agentConfig
  if (!('model' in agentConfig)) return agentConfig
  return {
    ...agentConfig,
    model: normalizeModelConfig(agentConfig.model),
  }
}
