#!/usr/bin/env node
/**
 * context-builder.cjs - Context/reminder building for session injection
 *
 * Extracted from dev-rules-reminder.cjs for reuse in both Claude hooks and OpenCode plugins.
 * Builds session context, rules, paths, and plan information.
 *
 * @module context-builder
 */

const fs = require('fs');
const os = require('os');
const path = require('path');
const { execSync } = require('child_process');

// Usage cache file path (written by usage-context-awareness.cjs hook)
const USAGE_CACHE_FILE = path.join(os.tmpdir(), 'ck-usage-limits-cache.json');
const RECENT_INJECTION_TTL_MS = 5 * 60 * 1000;
const PENDING_INJECTION_TTL_MS = 30 * 1000;
const WARN_THRESHOLD = 70;
const CRITICAL_THRESHOLD = 90;
const {
  loadConfig,
  resolvePlanPath,
  getReportsPath,
  resolveNamingPattern,
  normalizePath,
  getGitBranch,
  readSessionState,
  updateSessionState
} = require('./ck-config-utils.cjs');

function execSafe(cmd) {
  try {
    return execSync(cmd, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
  } catch {
    return null;
  }
}

/**
 * Resolve rules file path (local or global) with backward compat
 * @param {string} filename - Rules filename
 * @param {string} [configDirName='.claude'] - Config directory name
 * @returns {string|null} Resolved path or null
 */
function resolveRulesPath(filename, configDirName = '.claude') {
  // Try rules/ first (new location)
  const localRulesPath = path.join(process.cwd(), configDirName, 'rules', filename);
  const globalRulesPath = path.join(os.homedir(), '.claude', 'rules', filename);

  if (fs.existsSync(localRulesPath)) return `${configDirName}/rules/${filename}`;
  if (fs.existsSync(globalRulesPath)) return `~/.opencode/rules/${filename}`;

  // Backward compat: try workflows/ (legacy location)
  const localWorkflowsPath = path.join(process.cwd(), configDirName, 'workflows', filename);
  const globalWorkflowsPath = path.join(os.homedir(), '.claude', 'workflows', filename);

  if (fs.existsSync(localWorkflowsPath)) return `${configDirName}/workflows/${filename}`;
  if (fs.existsSync(globalWorkflowsPath)) return `~/.opencode/workflows/${filename}`;

  return null;
}

/**
 * Resolve script file path (local or global)
 * @param {string} filename - Script filename
 * @param {string} [configDirName='.claude'] - Config directory name
 * @returns {string|null} Resolved path or null
 */
function resolveScriptPath(filename, configDirName = '.claude') {
  const localPath = path.join(process.cwd(), configDirName, 'scripts', filename);
  const globalPath = path.join(os.homedir(), '.claude', 'scripts', filename);
  if (fs.existsSync(localPath)) return `${configDirName}/scripts/${filename}`;
  if (fs.existsSync(globalPath)) return `~/.opencode/scripts/${filename}`;
  return null;
}

/**
 * Resolve skills venv Python path (local or global)
 * @param {string} [configDirName='.claude'] - Config directory name
 * @returns {string|null} Resolved venv Python path or null
 */
function resolveSkillsVenv(configDirName = '.claude') {
  const isWindows = process.platform === 'win32';
  const venvBin = isWindows ? 'Scripts' : 'bin';
  const pythonExe = isWindows ? 'python.exe' : 'python3';

  const localVenv = path.join(process.cwd(), configDirName, 'skills', '.venv', venvBin, pythonExe);
  const globalVenv = path.join(os.homedir(), '.claude', 'skills', '.venv', venvBin, pythonExe);

  if (fs.existsSync(localVenv)) {
    return isWindows
      ? `${configDirName}\\skills\\.venv\\Scripts\\python.exe`
      : `${configDirName}/skills/.venv/bin/python3`;
  }
  if (fs.existsSync(globalVenv)) {
    return isWindows
      ? '~\\.claude\\skills\\.venv\\Scripts\\python.exe'
      : '~/.opencode/skills/.venv/bin/python3';
  }
  return null;
}

/**
 * Build plan context from config and git info
 * @param {string|null} sessionId - Session ID
 * @param {Object} config - Loaded config
 * @returns {Object} Plan context object
 */
function buildPlanContext(sessionId, config) {
  const { plan, paths } = config;
  const gitBranch = getGitBranch();
  const resolved = resolvePlanPath(sessionId, config);
  const reportsPath = getReportsPath(resolved.path, resolved.resolvedBy, plan, paths);

  // Compute naming pattern directly for reliable injection
  const namePattern = resolveNamingPattern(plan, gitBranch);

  const planLine = resolved.resolvedBy === 'session'
    ? `- Plan: ${resolved.path}`
    : resolved.resolvedBy === 'branch'
      ? `- Plan: none | Suggested: ${resolved.path}`
      : `- Plan: none`;

  // Validation config (injected so LLM can reference it)
  const validation = plan.validation || {};
  const validationMode = validation.mode || 'prompt';
  const validationMin = validation.minQuestions || 3;
  const validationMax = validation.maxQuestions || 8;

  return { reportsPath, gitBranch, planLine, namePattern, validationMode, validationMin, validationMax };
}

/**
 * Build a scope key for reminder dedup so cwd-sensitive output can re-inject when needed.
 * @param {Object} params
 * @param {string} [params.baseDir] - Working directory for the hook invocation
 * @returns {string} Stable scope key
 */
function buildInjectionScopeKey({ baseDir } = {}) {
  const cwdKey = normalizePath(path.resolve(baseDir || process.cwd())) || process.cwd();
  return cwdKey;
}

function parseTimestamp(value) {
  if (typeof value === 'number') return value;
  if (typeof value === 'string') return Date.parse(value);
  return NaN;
}

function getReminderScopeState(reminderState, scopeKey) {
  const scopes = reminderState?.scopes;
  if (!scopes || typeof scopes !== 'object') return null;
  const scopeState = scopes[scopeKey];
  return scopeState && typeof scopeState === 'object' ? scopeState : null;
}

function hasRecentInjection(scopeState, now = Date.now()) {
  const injectedTs = parseTimestamp(scopeState?.lastInjectedAt);
  return Number.isFinite(injectedTs) && now - injectedTs < RECENT_INJECTION_TTL_MS;
}

function hasPendingInjection(scopeState, now = Date.now()) {
  const pendingTs = parseTimestamp(scopeState?.pendingAt);
  return Number.isFinite(pendingTs) && now - pendingTs < PENDING_INJECTION_TTL_MS;
}

function pruneReminderScopes(scopes, now = Date.now()) {
  const nextScopes = {};
  for (const [scopeKey, scopeState] of Object.entries(scopes || {})) {
    if (!scopeState || typeof scopeState !== 'object') continue;
    if (hasRecentInjection(scopeState, now) || hasPendingInjection(scopeState, now)) {
      nextScopes[scopeKey] = scopeState;
    }
  }
  return nextScopes;
}

function wasTranscriptRecentlyInjected(transcriptPath, scopeKey = null) {
  try {
    if (!transcriptPath || !fs.existsSync(transcriptPath)) return false;
    const tail = fs.readFileSync(transcriptPath, 'utf-8').split('\n').slice(-150);
    const hasReminderMarker = tail.some(line => line.includes('[IMPORTANT] Consider Modularization'));
    if (!hasReminderMarker) return false;
    if (!scopeKey) return true;

    // The reminder output is cwd-sensitive; only treat transcript fallback as a match
    // when the same cwd-specific session lines were already injected recently.
    return tail.some(line => line === `- CWD: ${scopeKey}` || line === `- Working directory: ${scopeKey}`);
  } catch {
    return false;
  }
}

/**
 * Check if context was recently injected (prevent duplicate injection).
 * Uses session-scoped markers when a session ID is available, otherwise falls back to transcript scan.
 * @param {string} transcriptPath - Path to transcript file
 * @param {string|null} [sessionId] - Session identifier for temp-state dedup
 * @param {string|null} [scopeKey='session'] - Scope key for cwd/transcript-aware dedup
 * @returns {boolean} true if recently injected
 */
function wasRecentlyInjected(transcriptPath, sessionId = null, scopeKey = 'session') {
  try {
    if (sessionId) {
      const reminderState = readSessionState(sessionId)?.devRulesReminder;
      if (hasRecentInjection(getReminderScopeState(reminderState, scopeKey))) {
        return true;
      }
    }

    return wasTranscriptRecentlyInjected(transcriptPath, scopeKey);
  } catch {
    return false;
  }
}

/**
 * Reserve an injection slot atomically so concurrent hooks do not double-inject.
 * @param {string|null} sessionId - Session identifier
 * @param {string|null} [scopeKey='session'] - Scope key for cwd/transcript-aware dedup
 * @param {string|null} [transcriptPath] - Transcript path for legacy fallback when no session ID exists
 * @returns {{ shouldInject: boolean, reserved: boolean }} Whether to inject and whether a pending reservation was written
 */
function reserveInjectionScope(sessionId, scopeKey = 'session', transcriptPath = null) {
  const transcriptAlreadyInjected = wasTranscriptRecentlyInjected(transcriptPath, scopeKey);

  if (!sessionId) {
    return {
      shouldInject: !transcriptAlreadyInjected,
      reserved: false
    };
  }

  try {
    let shouldInject = false;
    const now = Date.now();
    const updated = updateSessionState(sessionId, (state) => {
      const reminderState = state.devRulesReminder && typeof state.devRulesReminder === 'object'
        ? state.devRulesReminder
        : {};
      const scopes = pruneReminderScopes(reminderState.scopes, now);
      const scopeState = getReminderScopeState({ scopes }, scopeKey) || {};

      if (hasRecentInjection(scopeState, now) || hasPendingInjection(scopeState, now)) {
        return state;
      }

      if (transcriptAlreadyInjected) {
        scopes[scopeKey] = {
          ...scopeState,
          lastInjectedAt: new Date(now).toISOString()
        };

        return {
          ...state,
          devRulesReminder: {
            ...reminderState,
            scopes
          }
        };
      }

      shouldInject = true;
      scopes[scopeKey] = {
        ...scopeState,
        pendingAt: new Date(now).toISOString()
      };

      return {
        ...state,
        devRulesReminder: {
          ...reminderState,
          scopes
        }
      };
    });

    if (!updated) {
      return {
        shouldInject: !transcriptAlreadyInjected,
        reserved: false
      };
    }

    return { shouldInject, reserved: shouldInject };
  } catch {
    return {
      shouldInject: !transcriptAlreadyInjected,
      reserved: false
    };
  }
}

/**
 * Persist a recent injection marker for the current session and clear the pending reservation.
 * @param {string|null} sessionId - Session identifier
 * @param {string|null} [scopeKey='session'] - Scope key for cwd/transcript-aware dedup
 * @returns {boolean} true when the marker is written
 */
function markRecentlyInjected(sessionId, scopeKey = 'session') {
  if (!sessionId) return false;

  try {
    return updateSessionState(sessionId, (state) => {
      const reminderState = state.devRulesReminder && typeof state.devRulesReminder === 'object'
        ? state.devRulesReminder
        : {};
      const scopes = pruneReminderScopes(reminderState.scopes);
      const scopeState = getReminderScopeState({ scopes }, scopeKey) || {};

      scopes[scopeKey] = {
        ...scopeState,
        lastInjectedAt: new Date().toISOString()
      };
      delete scopes[scopeKey].pendingAt;

      return {
        ...state,
        devRulesReminder: {
          ...reminderState,
          scopes
        }
      };
    });
  } catch {
    return false;
  }
}

/**
 * Clear a pending reservation when the hook fails after reserving a slot.
 * @param {string|null} sessionId - Session identifier
 * @param {string|null} [scopeKey='session'] - Scope key for cwd/transcript-aware dedup
 * @returns {boolean} true when cleanup succeeds
 */
function clearPendingInjection(sessionId, scopeKey = 'session') {
  if (!sessionId) return false;

  try {
    return updateSessionState(sessionId, (state) => {
      const reminderState = state.devRulesReminder && typeof state.devRulesReminder === 'object'
        ? state.devRulesReminder
        : {};
      const scopes = pruneReminderScopes(reminderState.scopes);
      const scopeState = getReminderScopeState({ scopes }, scopeKey);

      if (!scopeState || !scopeState.pendingAt) {
        return state;
      }

      const nextScopeState = { ...scopeState };
      delete nextScopeState.pendingAt;

      if (Object.keys(nextScopeState).length === 0) {
        delete scopes[scopeKey];
      } else {
        scopes[scopeKey] = nextScopeState;
      }

      return {
        ...state,
        devRulesReminder: {
          ...reminderState,
          scopes
        }
      };
    });
  } catch {
    return false;
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// SECTION BUILDERS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Build language section
 * @param {Object} params
 * @param {string} [params.thinkingLanguage] - Language for thinking
 * @param {string} [params.responseLanguage] - Language for response
 * @returns {string[]} Lines for language section
 */
function buildLanguageSection({ thinkingLanguage, responseLanguage }) {
  // Auto-default thinkingLanguage to 'en' when only responseLanguage is set
  const effectiveThinking = thinkingLanguage || (responseLanguage ? 'en' : null);
  const hasThinking = effectiveThinking && effectiveThinking !== responseLanguage;
  const hasResponse = responseLanguage;
  const lines = [];

  if (hasThinking || hasResponse) {
    lines.push(`## Language`);
    if (hasThinking) {
      lines.push(`- Thinking: Use ${effectiveThinking} for reasoning (logic, precision).`);
    }
    if (hasResponse) {
      lines.push(`- Response: Respond in ${responseLanguage} (natural, fluent).`);
    }
    lines.push(``);
  }

  return lines;
}

/**
 * Build session section
 * @param {Object} [staticEnv] - Pre-computed static environment info
 * @returns {string[]} Lines for session section
 */
function buildSessionSection(staticEnv = {}) {
  const memUsed = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
  const memTotal = Math.round(os.totalmem() / 1024 / 1024);
  const memPercent = Math.round((memUsed / memTotal) * 100);
  const cpuUsage = Math.round((process.cpuUsage().user / 1000000) * 100);
  const cpuSystem = Math.round((process.cpuUsage().system / 1000000) * 100);

  return [
    `## Session`,
    `- DateTime: ${new Date().toLocaleString()}`,
    `- CWD: ${staticEnv.cwd || process.cwd()}`,
    `- Timezone: ${staticEnv.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone}`,
    `- Working directory: ${staticEnv.cwd || process.cwd()}`,
    `- OS: ${staticEnv.osPlatform || process.platform}`,
    `- User: ${staticEnv.user || process.env.USERNAME || process.env.USER}`,
    `- Locale: ${staticEnv.locale || process.env.LANG || ''}`,
    `- Memory usage: ${memUsed}MB/${memTotal}MB (${memPercent}%)`,
    `- CPU usage: ${cpuUsage}% user / ${cpuSystem}% system`,
    `- Spawning multiple subagents can cause performance issues, spawn and delegate tasks intelligently based on the available system resources.`,
    `- Remember that each subagent only has 200K tokens in context window, spawn and delegate tasks intelligently to make sure their context windows don't get bloated.`,
    `- IMPORTANT: Include these environment information when prompting subagents to perform tasks.`,
    ``
  ];
}

/**
 * Read usage limits from cache file (written by usage-context-awareness.cjs)
 * @returns {Object|null} Usage data or null if unavailable
 */
function readUsageCache() {
  try {
    if (fs.existsSync(USAGE_CACHE_FILE)) {
      const cache = JSON.parse(fs.readFileSync(USAGE_CACHE_FILE, 'utf-8'));
      // Cache is valid for 5 minutes for injection purposes
      if (Date.now() - cache.timestamp < 300000 && cache.data) {
        return cache.data;
      }
    }
  } catch { }
  return null;
}

/**
 * Format time until reset
 * @param {string} resetAt - ISO timestamp
 * @returns {string|null} Formatted time or null
 */
function formatTimeUntilReset(resetAt) {
  if (!resetAt) return null;
  const resetTime = new Date(resetAt);
  const remaining = Math.floor(resetTime.getTime() / 1000) - Math.floor(Date.now() / 1000);
  if (remaining <= 0 || remaining > 18000) return null; // Only show if < 5 hours
  const hours = Math.floor(remaining / 3600);
  const mins = Math.floor((remaining % 3600) / 60);
  return `${hours}h ${mins}m`;
}

/**
 * Format percentage with warning level
 * @param {number} value - Percentage value
 * @param {string} label - Label prefix
 * @returns {string} Formatted string with warning if applicable
 */
function formatUsagePercent(value, label) {
  const pct = Math.round(value);
  if (pct >= CRITICAL_THRESHOLD) return `${label}: ${pct}% [CRITICAL]`;
  if (pct >= WARN_THRESHOLD) return `${label}: ${pct}% [WARNING]`;
  return `${label}: ${pct}%`;
}

/**
 * Build context window section from statusline cache
 * @param {string} sessionId - Session ID
 * @returns {string[]} Lines for context section
 */
function buildContextSection(sessionId) {
  // TEMPORARILY DISABLED
  return [];
  if (!sessionId) return [];

  // RE-ENABLED IF NEEDED IN THE FUTURE
  try {
    const contextPath = path.join(os.tmpdir(), `ck-context-${sessionId}.json`);
    if (!fs.existsSync(contextPath)) return [];

    const data = JSON.parse(fs.readFileSync(contextPath, 'utf-8'));
    // Only use fresh data (< 5 min old - statusline updates every 300ms when active)
    if (Date.now() - data.timestamp > 300000) return [];

    const lines = [`## Current Session's Context`];

    // Format: 48% used (96K/200K tokens)
    const usedK = Math.round(data.tokens / 1000);
    const sizeK = Math.round(data.size / 1000);
    lines.push(`- Context: ${data.percent}% used (${usedK}K/${sizeK}K tokens)`);
    lines.push(`- **NOTE:** Optimize the workflow for token efficiency`);

    // Warning if high usage
    if (data.percent >= CRITICAL_THRESHOLD) {
      lines.push(`- **CRITICAL:** Context nearly full. Before compaction hits:`);
      lines.push(`  1. Update TodoWrite with current progress (completed + remaining)`);
      lines.push(`  2. Be extremely concise — no verbose explanations`);
      lines.push(`  3. Session state will auto-restore after compaction`);
    } else if (data.percent >= WARN_THRESHOLD) {
      lines.push(`- **WARNING:** Context usage moderate - be concise, optimize token efficiency, keep tool outputs short.`);
    }

    lines.push(``);
    return lines;
  } catch {
    return [];
  }
}

/**
 * Build usage section from cache
 * @returns {string[]} Lines for usage section
 */
function buildUsageSection() {
  // TEMPORARILY DISABLED
  return [];

  // RE-ENABLED IF NEEDED IN THE FUTURE
  const usage = readUsageCache();
  if (!usage) return [];

  const lines = [];
  const parts = [];

  // 5-hour limit
  if (usage.five_hour) {
    const util = usage.five_hour.utilization;
    if (typeof util === 'number') {
      parts.push(formatUsagePercent(util, '5h'));
    }
    const timeLeft = formatTimeUntilReset(usage.five_hour.resets_at);
    if (timeLeft) {
      parts.push(`resets in ${timeLeft}`);
    }
  }

  // 7-day limit
  if (usage.seven_day?.utilization != null) {
    parts.push(formatUsagePercent(usage.seven_day.utilization, '7d'));
  }

  if (parts.length > 0) {
    lines.push(`## Usage Limits`);
    lines.push(`- ${parts.join(' | ')}`);
    lines.push(``);
  }

  return lines;
}

/**
 * Build rules section
 * @param {Object} params
 * @param {string} [params.devRulesPath] - Path to dev rules
 * @param {string} [params.skillsVenv] - Path to skills venv
 * @param {string} [params.plansPath] - Absolute plans path (Issue #476: prevents wrong subdirectory creation)
 * @param {string} [params.docsPath] - Absolute docs path
 * @returns {string[]} Lines for rules section
 */
function buildRulesSection({ devRulesPath, skillsVenv, plansPath, docsPath }) {
  const lines = [`## Rules`];

  if (devRulesPath) {
    lines.push(`- Read and follow development rules: "${devRulesPath}"`);
  }

  // Issue #476: Use absolute paths to prevent LLM confusion in multi-CLAUDE.md projects
  const plansRef = plansPath || 'plans';
  const docsRef = docsPath || 'docs';
  lines.push(`- Markdown files are organized in: Plans → "${plansRef}" directory, Docs → "${docsRef}" directory`);
  lines.push(`- **IMPORTANT:** DO NOT create markdown files outside of "${plansRef}" or "${docsRef}" UNLESS the user explicitly requests it.`);

  if (skillsVenv) {
    lines.push(`- Python scripts in .opencode/skills/: Use \`${skillsVenv}\``);
  }

  lines.push(`- When skills' scripts are failed to execute, always fix them and run again, repeat until success.`);
  lines.push(`- Follow **YAGNI (You Aren't Gonna Need It) - KISS (Keep It Simple, Stupid) - DRY (Don't Repeat Yourself)** principles`);
  lines.push(`- Sacrifice grammar for the sake of concision when writing reports.`);
  lines.push(`- In reports, list any unresolved questions at the end, if any.`);
  lines.push(`- IMPORTANT: Ensure token consumption efficiency while maintaining high quality.`);
  lines.push(``);

  return lines;
}

/**
 * Build modularization section
 * @returns {string[]} Lines for modularization section
 */
function buildModularizationSection() {
  return [
    `## **[IMPORTANT] Consider Modularization:**`,
    `- Check existing modules before creating new`,
    `- Analyze logical separation boundaries (functions, classes, concerns)`,
    `- Prefer kebab-case for JS/TS/Python/shell; respect language conventions (C#/Java use PascalCase, Go/Rust use snake_case)`,
    `- Write descriptive code comments`,
    `- After modularization, continue with main task`,
    `- When not to modularize: Markdown files, plain text files, bash scripts, configuration files, environment variables files, etc.`,
    ``
  ];
}

/**
 * Build paths section
 * @param {Object} params
 * @param {string} params.reportsPath - Reports path
 * @param {string} params.plansPath - Plans path
 * @param {string} params.docsPath - Docs path
 * @param {number} [params.docsMaxLoc=800] - Max lines of code for docs
 * @returns {string[]} Lines for paths section
 */
function buildPathsSection({ reportsPath, plansPath, docsPath, docsMaxLoc = 800 }) {
  return [
    `## Paths`,
    `Reports: ${reportsPath} | Plans: ${plansPath}/ | Docs: ${docsPath}/ | docs.maxLoc: ${docsMaxLoc}`,
    ``
  ];
}

/**
 * Build plan context section
 * @param {Object} params
 * @param {string} params.planLine - Plan status line
 * @param {string} params.reportsPath - Reports path
 * @param {string} [params.gitBranch] - Git branch
 * @param {string} params.validationMode - Validation mode
 * @param {number} params.validationMin - Min questions
 * @param {number} params.validationMax - Max questions
 * @returns {string[]} Lines for plan context section
 */
function buildPlanContextSection({ planLine, reportsPath, gitBranch, validationMode, validationMin, validationMax }) {
  const lines = [
    `## Plan Context`,
    planLine,
    `- Reports: ${reportsPath}`
  ];

  if (gitBranch) {
    lines.push(`- Branch: ${gitBranch}`);
  }

  lines.push(`- Validation: mode=${validationMode}, questions=${validationMin}-${validationMax}`);
  lines.push(``);

  return lines;
}

/**
 * Build naming section
 * @param {Object} params
 * @param {string} params.reportsPath - Reports path
 * @param {string} params.plansPath - Plans path
 * @param {string} params.namePattern - Naming pattern
 * @returns {string[]} Lines for naming section
 */
function buildNamingSection({ reportsPath, plansPath, namePattern }) {
  return [
    `## Naming`,
    `- Report: \`${reportsPath}{type}-${namePattern}.md\``,
    `- Plan dir: \`${plansPath}/${namePattern}/\``,
    `- Replace \`{type}\` with: agent name, report type, or context`,
    `- Replace \`{slug}\` in pattern with: descriptive-kebab-slug`
  ];
}

// ═══════════════════════════════════════════════════════════════════════════
// MAIN ENTRY POINTS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Build full reminder content from all sections
 * @param {Object} params - All parameters for building reminder
 * @returns {string[]} Array of lines
 */
function buildReminder(params) {
  const {
    sessionId,
    thinkingLanguage,
    responseLanguage,
    devRulesPath,
    skillsVenv,
    reportsPath,
    plansPath,
    docsPath,
    docsMaxLoc,
    planLine,
    gitBranch,
    namePattern,
    validationMode,
    validationMin,
    validationMax,
    staticEnv,
    hooks
  } = params;

  // Respect hooks config — skip sections when their corresponding hook is disabled
  const hooksConfig = hooks || {};
  const contextEnabled = hooksConfig['context-tracking'] !== false;
  const usageEnabled = hooksConfig['usage-context-awareness'] !== false;

  return [
    ...buildLanguageSection({ thinkingLanguage, responseLanguage }),
    ...buildSessionSection(staticEnv),
    ...(contextEnabled ? buildContextSection(sessionId) : []),
    ...(usageEnabled ? buildUsageSection() : []),
    ...buildRulesSection({ devRulesPath, skillsVenv, plansPath, docsPath }),
    ...buildModularizationSection(),
    ...buildPathsSection({ reportsPath, plansPath, docsPath, docsMaxLoc }),
    ...buildPlanContextSection({ planLine, reportsPath, gitBranch, validationMode, validationMin, validationMax }),
    ...buildNamingSection({ reportsPath, plansPath, namePattern })
  ];
}

/**
 * Build complete reminder context (unified entry point for plugins)
 *
 * @param {Object} [params]
 * @param {string} [params.sessionId] - Session ID
 * @param {Object} [params.config] - CK config (auto-loaded if not provided)
 * @param {Object} [params.staticEnv] - Pre-computed static environment info
 * @param {string} [params.configDirName='.claude'] - Config directory name
 * @param {string} [params.baseDir] - Base directory for absolute path resolution (Issue #327)
 * @returns {{
 *   content: string,
 *   lines: string[],
 *   sections: Object
 * }}
 */
function buildReminderContext({ sessionId, config, staticEnv, configDirName = '.claude', baseDir } = {}) {
  // Load config if not provided
  const cfg = config || loadConfig({ includeProject: false, includeAssertions: false });

  // Resolve paths
  const devRulesPath = resolveRulesPath('development-rules.md', configDirName);
  const skillsVenv = resolveSkillsVenv(configDirName);

  // Build plan context
  const planCtx = buildPlanContext(sessionId, cfg);

  // Issue #327: Use baseDir for absolute path resolution (subdirectory workflow support)
  // If baseDir provided, resolve paths as absolute; otherwise use relative paths
  const effectiveBaseDir = baseDir || null;
  const plansPathRel = normalizePath(cfg.paths?.plans) || 'plans';
  const docsPathRel = normalizePath(cfg.paths?.docs) || 'docs';

  // Build all parameters with absolute paths if baseDir provided
  const params = {
    sessionId,
    thinkingLanguage: cfg.locale?.thinkingLanguage,
    responseLanguage: cfg.locale?.responseLanguage,
    devRulesPath,
    skillsVenv,
    reportsPath: effectiveBaseDir ? path.join(effectiveBaseDir, planCtx.reportsPath) : planCtx.reportsPath,
    plansPath: effectiveBaseDir ? path.join(effectiveBaseDir, plansPathRel) : plansPathRel,
    docsPath: effectiveBaseDir ? path.join(effectiveBaseDir, docsPathRel) : docsPathRel,
    docsMaxLoc: Math.max(1, parseInt(cfg.docs?.maxLoc, 10) || 800),
    planLine: planCtx.planLine,
    gitBranch: planCtx.gitBranch,
    namePattern: planCtx.namePattern,
    validationMode: planCtx.validationMode,
    validationMin: planCtx.validationMin,
    validationMax: planCtx.validationMax,
    staticEnv,
    hooks: cfg.hooks
  };

  const lines = buildReminder(params);

  // Respect hooks config for sections object too
  const hooksConfig = cfg.hooks || {};
  const contextEnabled = hooksConfig['context-tracking'] !== false;
  const usageEnabled = hooksConfig['usage-context-awareness'] !== false;

  return {
    content: lines.join('\n'),
    lines,
    sections: {
      language: buildLanguageSection({ thinkingLanguage: params.thinkingLanguage, responseLanguage: params.responseLanguage }),
      session: buildSessionSection(staticEnv),
      context: contextEnabled ? buildContextSection(sessionId) : [],
      usage: usageEnabled ? buildUsageSection() : [],
      rules: buildRulesSection({ devRulesPath, skillsVenv, plansPath: params.plansPath, docsPath: params.docsPath }),
      modularization: buildModularizationSection(),
      paths: buildPathsSection({ reportsPath: params.reportsPath, plansPath: params.plansPath, docsPath: params.docsPath, docsMaxLoc: params.docsMaxLoc }),
      planContext: buildPlanContextSection(planCtx),
      naming: buildNamingSection({ reportsPath: params.reportsPath, plansPath: params.plansPath, namePattern: params.namePattern })
    }
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════════════

module.exports = {
  // Main entry points
  buildReminderContext,
  buildReminder,

  // Section builders
  buildLanguageSection,
  buildSessionSection,
  buildContextSection,
  buildUsageSection,
  buildRulesSection,
  buildModularizationSection,
  buildPathsSection,
  buildPlanContextSection,
  buildNamingSection,

  // Helpers
  execSafe,
  resolveRulesPath,
  resolveScriptPath,
  resolveSkillsVenv,
  buildPlanContext,
  buildInjectionScopeKey,
  wasRecentlyInjected,
  reserveInjectionScope,
  markRecentlyInjected,
  clearPendingInjection,

  // Backward compat alias
  resolveWorkflowPath: resolveRulesPath
};
