'use strict';

/**
 * Workspace prompt loader — Item A of prompt-catalog deep-dive.
 *
 * Loads user-editable persona/context files from ~/.wall-e/:
 *   IDENTITY.md   — agent's name, vibe, emoji, avatar
 *   SOUL.md       — personality, boundaries, values
 *   USER.md       — what the agent knows about its human
 *   TOOLS.md      — per-tool usage guidance
 *   AGENTS.md     — default operating rules (per-loop overrides go in
 *                   wall-e/loops/<name>.directives.md per loop-directives.js)
 *
 * The chat system prompt builder + boot-check + heartbeat-style loops
 * all read these via this module. Edits take effect on the next read
 * (mtime-cached); no daemon restart needed.
 *
 * Inspired by OpenClaw's `docs/reference/templates/{IDENTITY,SOUL,USER,
 * TOOLS,BOOT,HEARTBEAT,AGENTS}.md` — same shape, slightly different file
 * locations (Wall-E uses `~/.wall-e/`, OpenClaw uses workspace root).
 */

const fs = require('fs');
const os = require('os');
const path = require('path');

/**
 * Workspace dir resolves to (in priority order):
 *   1. process.env.WALLE_DATA_DIR — explicit override
 *   2. $HOME/.walle (the established Wall-E data convention; same dir
 *      that loops/boot.js + utils/heartbeat-content already read)
 *
 * NOTE: We deliberately use `.walle` (not `.wall-e`) to match the
 * existing data convention from loops/boot.js + brain DB defaults.
 */
function _resolveWorkspaceDir() {
  if (process.env.WALLE_DATA_DIR) return process.env.WALLE_DATA_DIR;
  return path.join(process.env.HOME || os.homedir() || '', '.walle');
}

const WORKSPACE_DIR = _resolveWorkspaceDir();
const TEMPLATES_DIR = path.join(__dirname, '..', '_workspace-templates');

/** Names of the workspace prompt files we know how to load. */
const WORKSPACE_PROMPT_NAMES = Object.freeze([
  'IDENTITY',
  'SOUL',
  'USER',
  'TOOLS',
  'AGENTS',
  'BOOT',      // read by boot-check.js
  'HEARTBEAT', // read by heartbeat-style loops
]);

let _cache = new Map(); // name -> { mtime, content }

/** Path resolver — workspace dir wins over bundled template (templates are docs). */
function _resolveWorkspacePath(name) {
  if (typeof name !== 'string' || !name) return null;
  // Defensive: reject path traversal / absolute paths
  if (name.includes('..') || path.isAbsolute(name) || name.includes('/') || name.includes('\\')) {
    return null;
  }
  const wp = path.join(WORKSPACE_DIR, `${name}.md`);
  if (fs.existsSync(wp)) return wp;
  return null;
}

/**
 * Read the workspace prompt content for `name`, or null if absent.
 * mtime-cached: returns stale-cached content only when mtime unchanged.
 */
function readWorkspacePrompt(name) {
  const filePath = _resolveWorkspacePath(name);
  if (!filePath) return null;
  try {
    const stat = fs.statSync(filePath);
    const cached = _cache.get(filePath);
    if (cached && cached.mtime === stat.mtimeMs) return cached.content;
    const content = fs.readFileSync(filePath, 'utf8');
    _cache.set(filePath, { mtime: stat.mtimeMs, content });
    return content;
  } catch {
    return null;
  }
}

/**
 * Detect "this file is effectively empty" — content has no actionable
 * lines beyond whitespace, ATX headers, fence markers, and empty list
 * stubs / HTML comments.
 *
 * Mirrors OpenClaw's `isHeartbeatContentEffectivelyEmpty` but applied
 * uniformly across workspace prompts so callers can decide whether to
 * include them in the system prompt at all (saves tokens) or skip the
 * LLM call entirely (boot-check + heartbeat).
 *
 * Returns true on null/undefined (no file = effectively empty).
 */
function isContentEffectivelyEmpty(content) {
  if (content == null) return true;
  if (typeof content !== 'string') return false;
  const lines = content.split('\n');
  let inHtmlComment = false;
  for (let raw of lines) {
    const trimmed = raw.trim();
    if (!trimmed) continue;
    // Track multi-line HTML comments
    if (inHtmlComment) {
      if (trimmed.includes('-->')) inHtmlComment = false;
      continue;
    }
    if (trimmed.startsWith('<!--')) {
      if (!trimmed.includes('-->')) inHtmlComment = true;
      continue;
    }
    // ATX headers
    if (/^#{1,6}\s/.test(trimmed) || /^#{1,6}$/.test(trimmed)) continue;
    // Fence markers
    if (/^```[a-zA-Z0-9_-]*\s*$/.test(trimmed)) continue;
    // Empty list stubs
    if (/^[-*+]\s*$/.test(trimmed)) continue;
    if (/^[-*+]\s*\[\s*\]\s*$/.test(trimmed)) continue;
    // Horizontal rule
    if (/^---+\s*$/.test(trimmed)) continue;
    // Italics-only meta lines (template instructions like "_save at ..._")
    if (/^_.*_$/.test(trimmed)) continue;
    // Anything else means we have real content
    return false;
  }
  return true;
}

/**
 * Compose the user-customizable persona block for the chat system prompt.
 * Reads IDENTITY + SOUL + USER + TOOLS + AGENTS. Sections that are
 * effectively empty are omitted (so the user can leave any subset blank
 * without bloating the prompt).
 *
 * Returns a single string (or empty string) ready to inline into the
 * system prompt under a `--- workspace persona ---` heading.
 */
function composePersonaBlock() {
  const sections = [];
  for (const name of ['IDENTITY', 'SOUL', 'USER', 'TOOLS', 'AGENTS']) {
    const content = readWorkspacePrompt(name);
    if (!content) continue;
    if (isContentEffectivelyEmpty(content)) continue;
    sections.push(`--- workspace ${name} ---\n${content.trim()}`);
  }
  return sections.length ? sections.join('\n\n') : '';
}

function getWorkspacePersonaSections({ recipient = 'agent' } = {}) {
  const sections = [];
  for (const name of ['IDENTITY', 'SOUL', 'USER', 'TOOLS', 'AGENTS']) {
    const content = readWorkspacePrompt(name);
    if (!content || isContentEffectivelyEmpty(content)) continue;
    sections.push({
      id: `workspace-${name.toLowerCase()}`,
      source: `workspace:${name}.md`,
      content: content.trim(),
      recipient,
      cacheable: true,
      visibility: 'llm',
      mutableByHooks: true,
      redactionPolicy: name === 'USER' ? 'standard' : 'secrets',
    });
  }
  return sections;
}

/** Test helper. */
function clearWorkspacePromptsCache() {
  _cache.clear();
}

/**
 * Initialize ~/.wall-e/ from bundled templates. Returns
 * { initialized: [...names], skipped: [...names], error?: string }.
 *
 * - Creates ~/.wall-e/ if absent.
 * - Copies template → ~/.wall-e/<NAME>.md ONLY if dest doesn't exist.
 *   (Never overwrites — user edits are sacred.)
 * - Returns separately the names that were initialized vs already-present.
 *
 * Used by Item H endpoint POST /api/wall-e/workspace/init.
 */
function initWorkspaceFromTemplates(opts = {}) {
  const targetDir = opts.targetDir || WORKSPACE_DIR;
  const sourceDir = opts.sourceDir || TEMPLATES_DIR;
  const result = { initialized: [], skipped: [], errors: [] };

  try {
    fs.mkdirSync(targetDir, { recursive: true });
  } catch (err) {
    return { initialized: [], skipped: [], errors: [`mkdir ${targetDir}: ${err.message}`] };
  }

  for (const name of WORKSPACE_PROMPT_NAMES) {
    const src = path.join(sourceDir, `${name}.md`);
    const dst = path.join(targetDir, `${name}.md`);
    if (!fs.existsSync(src)) continue; // template not bundled (e.g. AGENTS shipped, others optional)
    if (fs.existsSync(dst)) {
      result.skipped.push(name);
      continue;
    }
    try {
      fs.copyFileSync(src, dst);
      result.initialized.push(name);
    } catch (err) {
      result.errors.push(`copy ${name}: ${err.message}`);
    }
  }

  return result;
}

module.exports = {
  WORKSPACE_DIR,
  TEMPLATES_DIR,
  WORKSPACE_PROMPT_NAMES,
  readWorkspacePrompt,
  isContentEffectivelyEmpty,
  composePersonaBlock,
  getWorkspacePersonaSections,
  initWorkspaceFromTemplates,
  clearWorkspacePromptsCache,
  _resolveWorkspacePath,
};
