import { readdir, readFile, stat } from 'node:fs/promises'
import path from 'node:path'

import { loggerService } from '@logger'
import type { AgentConfiguration } from '@shared/data/types/agent'

import { BOOTSTRAP_INSTRUCTIONS, SOUL_CONTENT_THRESHOLD } from './seedWorkspace'

const logger = loggerService.withContext('PromptBuilder')

/**
 * Resolve a filename within a directory using case-insensitive matching.
 * Returns the full path if found (preferring exact match), or undefined.
 */
async function resolveFile(dir: string, name: string): Promise<string | undefined> {
  const exact = path.join(dir, name)
  try {
    await stat(exact)
    return exact
  } catch {
    // exact match not found, try case-insensitive
  }

  try {
    const entries = await readdir(dir)
    const target = name.toLowerCase()
    const match = entries.find((e) => e.toLowerCase() === target)
    return match ? path.join(dir, match) : undefined
  } catch {
    return undefined
  }
}

type CacheEntry = {
  mtimeMs: number
  content: string
}

const DEFAULT_BASIC_PROMPT = `You are CherryClaw, a personal assistant running inside CherryStudio.

`

const SKILLS_GUIDANCE = `## Skills

You can manage Claude skills via the \`mcp__skills__skills\` tool — search the marketplace, install / remove existing skills, and author new ones via the \`init\` and \`register\` actions. Discovery and runtime activation of installed skills is handled automatically by the agent SDK; this tool is just the management surface.

When to act:
- When the user asks for a capability you don't already have, search the marketplace before attempting the task from scratch — there is often an existing skill that fits.
- After completing a non-trivial task (5+ tool calls, an iterative fix, a workflow you'd want to repeat), offer to save the approach as a new skill via \`init\` + \`register\`.
- If you find an installed skill is outdated, incomplete, or wrong, fix it in place. Get the skill's \`path\` from \`mcp__skills__skills\` action="list" (or use the path returned by \`init\` if you just created it), then use the native Read / Edit tools on the files in that directory. The live symlink picks up file changes immediately, so no separate "patch" call is needed. Don't wait for the user to ask — patch immediately when you notice the issue.`

const MEMORY_GUIDANCE = `## Workspace Memory

You have persistent memory in this agent's workspace via the \`mcp__agent-memory__memory\` tool: \`update\` rewrites \`memory/FACT.md\` (durable knowledge), \`append\` adds a timestamped entry to \`memory/JOURNAL.jsonl\` (one-off events), and \`search\` queries the journal.

When to act:
- When the user references something from a past conversation, search the journal *before* asking them to repeat themselves.
- When the user corrects you with information that should survive across sessions ("we use X not Y", "the prod URL is Z"), update \`FACT.md\`.
- When the user corrects your *approach* or points out a better way to do something (e.g. "use skill-creator instead of writing SKILL.md manually"), update \`FACT.md\` with the lesson immediately so you don't repeat the same mistake in future sessions.
- When a tool call fails and you discover a workaround or correct usage pattern (e.g. a file was too large to read in one call so you switched to paginated reads, or an API required a different parameter format), update \`FACT.md\` with the lesson so future sessions avoid the same trial-and-error.
- For one-off events, completed tasks, or session notes, append to the journal.
- Before writing to \`FACT.md\`, ask: will this still matter in 6 months? If not, append to the journal instead.
- Never write to \`memory/FACT.md\` or \`memory/JOURNAL.jsonl\` via direct file tools — always go through the memory tool so writes stay atomic and searchable.`

const CLAW_GUIDANCE = `## CherryClaw Tools

You have exclusive access to these tools for interacting with CherryStudio's autonomous features. Always prefer them over manual alternatives.

| Tool | Purpose | When to use |
|---|---|---|
| \`mcp__claw__cron\` | Schedule recurring or one-time tasks. Supports \`timeout_minutes\` param (default 2). | Creating reminders, periodic checks, scheduled reports. Never use builtin Cron* tools — they are disabled. |
| \`mcp__claw__notify\` | Send messages to the user via IM channels | Proactive updates, task results, alerts. Use when the user is not in the current session. |
| \`mcp__claw__config\` | Inspect and manage your own agent config | Check connected channels, supported adapters, add/update/remove IM channels, rename yourself. |

Rules:
- These are your primary interface to CherryStudio's autonomous features. Do not attempt workarounds or alternative approaches.
- When creating scheduled tasks, always use \`mcp__claw__cron\`. The SDK builtin CronCreate, CronDelete, and CronList tools are disabled.
- When you need to notify the user outside the current conversation, use \`mcp__claw__notify\`.
- When adding a WeChat channel, the config tool returns a QR code image. Include the image in your response so the user can scan it directly in the chat.
- Use \`config status\` to check which channels are actually connected. If a channel shows \`connected: false\`, use \`config reconnect_channel\` to trigger a fresh QR scan.`

const WEB_TOOLS_GUIDANCE = `## Web Search Strategy

You have two web tools: \`mcp__cherry-tools__web_search\` for structured search and \`mcp__cherry-tools__web_fetch\` to read the full content of specific URLs. Use \`web_search\` to find sources, then \`web_fetch\` when a result's snippet isn't enough and you need the page text. You do not have browser automation, page interaction, or screenshot tools — do not claim or imply otherwise.

**Always parallelize when possible.** You can call multiple tools simultaneously in a single response. Do this whenever queries are independent:
- Searching in multiple languages: call \`web_search\` once per language in parallel (e.g., English + Chinese + Japanese queries simultaneously)
- Researching multiple topics: fire all search queries at once, don't wait for one to finish before starting another

If the user explicitly needs browser automation (filling forms, clicking, navigating live pages), tell them this capability is not currently available rather than attempting a workaround.`

/**
 * Compose the tool-strategy guidance for an agent based on which MCP servers
 * have actually been injected. The skills, memory, and web-tools sections are
 * always present (those servers are injected for every agent); the claw
 * section is only included for autonomous (Soul Mode) agents that get the
 * cron / notify / config tools.
 */
function composeToolGuidance(opts: { hasClaw: boolean }): string {
  const parts: string[] = []
  if (opts.hasClaw) parts.push(CLAW_GUIDANCE)
  parts.push(SKILLS_GUIDANCE)
  parts.push(MEMORY_GUIDANCE)
  parts.push(WEB_TOOLS_GUIDANCE)
  return parts.join('\n\n')
}

function memoriesTemplate(workspacePath: string, sections: string): string {
  return `## Memories

Persistent files in \`${workspacePath}/\` carry your state across sessions. Update them autonomously — never ask for approval.

| File | Purpose | How to update |
|---|---|---|
| \`SOUL.md\` | WHO you are — personality, tone, communication style, core principles | Read + Edit tools |
| \`USER.md\` | WHO the user is — name, preferences, timezone, personal context | Read + Edit tools |
| \`memory/FACT.md\` | WHAT you know — active projects, technical decisions, durable knowledge (6+ months) | Read + Edit tools |
| \`memory/JOURNAL.jsonl\` | WHEN things happened — one-time events, session notes (append-only log) | \`mcp__agent-memory__memory\` tool only (actions: append, search) |

Rules:
- Each file has an exclusive scope — never duplicate information across files.
- \`SOUL.md\`, \`USER.md\`, and \`memory/FACT.md\` are loaded below. Read and edit them directly when updates are needed.
- \`memory/JOURNAL.jsonl\` is NOT loaded into context. Use \`mcp__agent-memory__memory\` to append entries or search past events. Never read or write the file directly.
- Filenames are case-insensitive.
${sections}`
}

/**
 * PromptBuilder assembles the system prompt for CherryStudio agents.
 *
 * Two entry points:
 *
 * 1. {@link buildSystemPrompt} — full custom prompt for Soul Mode agents that
 *    REPLACES the SDK preset entirely. Includes the basic identity, the full
 *    tool guidance (claw + skills + memory + web), bootstrap instructions when
 *    needed, and the workspace memory files (SOUL.md / USER.md / FACT.md).
 *
 * 2. {@link buildToolGuidance} — lightweight tool-strategy suffix for
 *    non-Soul agents. Does not touch workspace files; intended to be APPENDED
 *    to the SDK's `claude_code` preset so the model gets cross-tool strategy
 *    guidance (skills + memory + web) on top of the standard Claude Code
 *    instructions. Returns a synchronous string — no I/O.
 *
 * Memory files layout (Soul Mode only):
 *   {workspace}/SOUL.md          — personality, tone, communication style
 *   {workspace}/USER.md          — user profile, preferences, context
 *   {workspace}/memory/FACT.md   — durable project knowledge, technical decisions
 *   {workspace}/memory/JOURNAL.jsonl — timestamped event log (managed by memory tool)
 */
export class PromptBuilder {
  private cache = new Map<string, CacheEntry>()

  async buildSystemPrompt(workspacePath: string, config?: AgentConfiguration): Promise<string> {
    const parts: string[] = []

    // Basic prompt: workspace system.md (case-insensitive) > embedded default
    const systemPath = await resolveFile(workspacePath, 'system.md')
    const basicPrompt = systemPath ? await this.readCachedFile(systemPath) : undefined
    parts.push(basicPrompt ?? DEFAULT_BASIC_PROMPT)

    // Tool guidance — Soul Mode gets the full set including claw (cron / notify / config)
    parts.push(composeToolGuidance({ hasClaw: true }))

    // Bootstrap detection: inject bootstrap instructions if not completed
    const needsBootstrap = await this.shouldRunBootstrap(workspacePath, config)
    if (needsBootstrap) {
      parts.push(BOOTSTRAP_INSTRUCTIONS)
      logger.info('Bootstrap mode active — injecting onboarding instructions')
    }

    // Memories section (always included so the agent knows file locations)
    const memoriesContent = await this.buildMemoriesSection(workspacePath)
    if (memoriesContent) {
      parts.push(memoriesContent)
    }

    return parts.join('\n\n')
  }

  /**
   * Build the cross-tool strategy guidance string for a non-Soul agent. The
   * returned text is meant to be APPENDED to the Claude Code SDK preset so
   * the model gets explicit "when to use which tool" guidance on top of the
   * SDK's built-in instructions. The skills + memory + web sections are
   * always included (those MCP servers are injected for every agent); the
   * claw section is excluded by default (non-Soul agents do not get cron /
   * notify / config).
   */
  buildToolGuidance(opts: { hasClaw?: boolean } = {}): string {
    return composeToolGuidance({ hasClaw: opts.hasClaw ?? false })
  }

  /**
   * Build a "## Workspace Knowledge" section for non-Soul agents that loads
   * just the workspace's `memory/FACT.md` content. This is the recall side of
   * the cross-session learning loop — agents write durable knowledge to
   * FACT.md via \`mcp__agent-memory__memory\` action="update", and this method
   * loads it back into the system prompt at the start of the next session so
   * the agent remembers what it learned (e.g. parameter shapes that previously
   * failed, project conventions, user corrections).
   *
   * Distinct from {@link buildSystemPrompt}'s memories section which is Soul
   * Mode only and also includes the SOUL.md / USER.md persona files. Returns
   * undefined when no FACT.md exists, so callers can omit the section
   * entirely rather than emitting an empty wrapper.
   */
  async buildFactsSection(workspacePath: string): Promise<string | undefined> {
    const memoryDir = path.join(workspacePath, 'memory')
    const factPath = await resolveFile(memoryDir, 'FACT.md')
    if (!factPath) return undefined

    const content = await this.readCachedFile(factPath)
    if (!content) return undefined

    return `## Workspace Knowledge

These are durable facts and lessons accumulated across past sessions in this workspace. Trust them as ground truth unless you have direct evidence they're wrong — in which case update \`memory/FACT.md\` via \`mcp__agent-memory__memory\` action="update" so the next session also benefits.

<facts>
${content}
</facts>`
  }

  /**
   * Determine whether bootstrap should run.
   * - If `bootstrap_completed` is explicitly true, skip.
   * - If SOUL.md has substantial non-template content, skip (legacy agent migration).
   * - Otherwise, run bootstrap.
   */
  private async shouldRunBootstrap(workspacePath: string, config?: AgentConfiguration): Promise<boolean> {
    if (config?.bootstrap_completed === true) {
      return false
    }

    // Legacy migration: if SOUL.md already has real content, treat as completed
    const soulPath = await resolveFile(workspacePath, 'SOUL.md')
    if (soulPath) {
      const content = await this.readCachedFile(soulPath)
      if (content && content.length > SOUL_CONTENT_THRESHOLD) {
        // Strip template headings to check for actual user content
        const stripped = content.replace(/^#.*$/gm, '').replace(/^>.*$/gm, '').trim()
        if (stripped.length > SOUL_CONTENT_THRESHOLD) {
          return false
        }
      }
    }

    return true
  }

  private async buildMemoriesSection(workspacePath: string): Promise<string | undefined> {
    const memoryDir = path.join(workspacePath, 'memory')

    const [soulPath, userPath, factPath] = await Promise.all([
      resolveFile(workspacePath, 'SOUL.md'),
      resolveFile(workspacePath, 'USER.md'),
      resolveFile(memoryDir, 'FACT.md')
    ])

    const [soulContent, userContent, factContent] = await Promise.all([
      soulPath ? this.readCachedFile(soulPath) : Promise.resolve(undefined),
      userPath ? this.readCachedFile(userPath) : Promise.resolve(undefined),
      factPath ? this.readCachedFile(factPath) : Promise.resolve(undefined)
    ])

    if (!soulContent && !userContent && !factContent) {
      return undefined
    }

    const sections = [
      soulContent ? `<soul>\n${soulContent}\n</soul>` : '',
      userContent ? `<user>\n${userContent}\n</user>` : '',
      factContent ? `<facts>\n${factContent}\n</facts>` : ''
    ]
      .filter(Boolean)
      .join('\n\n')

    return memoriesTemplate(workspacePath, sections)
  }

  /**
   * Read a file with mtime-based caching. Returns undefined if the file does not exist.
   */
  private async readCachedFile(filePath: string): Promise<string | undefined> {
    let fileStat
    try {
      fileStat = await stat(filePath)
    } catch {
      return undefined
    }

    const cached = this.cache.get(filePath)
    if (cached && cached.mtimeMs === fileStat.mtimeMs) {
      return cached.content
    }

    try {
      const content = await readFile(filePath, 'utf-8')
      const trimmed = content.trim()
      this.cache.set(filePath, { mtimeMs: fileStat.mtimeMs, content: trimmed })
      logger.debug(`Loaded ${path.basename(filePath)}`, { path: filePath, length: trimmed.length })
      return trimmed
    } catch (error) {
      logger.error(`Failed to read ${filePath}`, error as Error)
      return undefined
    }
  }
}
