/**
 * Dynamic System Prompt Assembler
 *
 * Reads the base system prompt, evaluates conditions, and applies matching
 * fragments (replace / remove / append) to produce the final prompt.
 *
 * See DYNAMIC-PROMPTS.md for the full playbook.
 */

import fs from 'fs';
import path from 'path';
import { log } from '../../shared/logger.js';
import { conditions, type ConditionResult } from './prompt-conditions.js';

const FRAGMENTS_FILE = path.join(import.meta.dirname, 'prompt-fragments.json');

/**
 * One base system prompt per harness. The dynamic-fragment machinery
 * (markers + prompt-fragments.json + prompt-conditions.ts) is shared across
 * all three — each file carries the same `<!-- dynamic:* -->` markers, so a
 * fragment applies regardless of which harness is active. Copy the markers
 * into every file when adding a new dynamic section (see DYNAMIC-PROMPTS.md).
 */
export type Harness = 'claude' | 'codex' | 'pi';

const DEFAULT_PROMPT_FILENAME = 'bloby-system-prompt.txt';
const PROMPT_FILENAMES: Record<Harness, string> = {
  claude: 'bloby-system-prompt.txt',
  codex: 'bloby-system-prompt-codex.txt',
  pi: 'bloby-system-prompt-pi.txt',
};

/** Absolute path to the base prompt file for a given harness. */
function promptFilePath(harness: Harness): string {
  const filename = PROMPT_FILENAMES[harness] ?? DEFAULT_PROMPT_FILENAME;
  return path.join(import.meta.dirname, filename);
}

// ── Types ────────────────────────────────────────────────────────────────────

export interface PromptFragment {
  id: string;
  description: string;
  /** Which <!-- dynamic:target --> block this fragment acts on */
  target: string;
  /** replace = swap marker content, remove = delete marker block, append = add after marker (or end) */
  action: 'replace' | 'remove' | 'append';
  /** Lower number = evaluated first. First matching fragment per target wins for replace/remove. */
  priority: number;
  /** The text to insert (supports {{variable}} interpolation). Not used for 'remove'. */
  content?: string;
}

// ── Helpers ──────────────────────────────────────────────────────────────────

/** Read and parse the fragments JSON file */
function loadFragments(): PromptFragment[] {
  try {
    const raw = fs.readFileSync(FRAGMENTS_FILE, 'utf-8');
    return JSON.parse(raw);
  } catch (err) {
    log.warn('Could not load prompt-fragments.json — using base prompt only');
    return [];
  }
}

/**
 * Read the base system prompt for `harness` with $BOT / $HUMAN replacement.
 * If the harness-specific file is missing or empty, fall back to the Claude
 * prompt (`bloby-system-prompt.txt`) so a not-yet-created codex/pi file never
 * collapses the agent to the minimal stub.
 */
function readBasePrompt(botName = 'Bloby', humanName = 'Human', harness: Harness = 'claude'): string {
  const primary = promptFilePath(harness);
  const fallback = path.join(import.meta.dirname, DEFAULT_PROMPT_FILENAME);
  const candidates = primary === fallback ? [primary] : [primary, fallback];

  for (const file of candidates) {
    try {
      const raw = fs.readFileSync(file, 'utf-8').trim();
      if (!raw) continue;
      if (file === fallback && primary !== fallback) {
        log.warn(`[prompt] "${harness}" prompt (${path.basename(primary)}) missing/empty — falling back to ${DEFAULT_PROMPT_FILENAME}`);
      }
      return raw.replace(/\$BOT/g, botName).replace(/\$HUMAN/g, humanName);
    } catch {
      // try next candidate
    }
  }

  log.warn('System prompt file not found — using minimal fallback');
  return `You are ${botName}, a helpful AI agent. Your human is ${humanName}.`;
}

/** Interpolate {{variable}} placeholders in content using vars map */
function interpolate(content: string, vars: Record<string, string>): string {
  return content.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? `{{${key}}}`);
}

/**
 * Regex to match a dynamic marker block:
 *   <!-- dynamic:target -->
 *   ...content...
 *   <!-- /dynamic:target -->
 */
function markerRegex(target: string): RegExp {
  return new RegExp(
    `<!-- dynamic:${target} -->\\n?([\\s\\S]*?)<!-- /dynamic:${target} -->`,
  );
}

// ── Main ─────────────────────────────────────────────────────────────────────

/**
 * Assemble the final system prompt by evaluating all fragment conditions
 * and applying the matching ones to the base prompt.
 */
export async function assembleSystemPrompt(
  botName = 'Bloby',
  humanName = 'Human',
  harness: Harness = 'claude',
): Promise<string> {
  let prompt = readBasePrompt(botName, humanName, harness);
  const fragments = loadFragments();

  if (!fragments.length) {
    log.info('[prompt] No fragments found — using base prompt only');
    return prompt;
  }

  log.info(`[prompt] Harness "${harness}" (${PROMPT_FILENAMES[harness] ?? DEFAULT_PROMPT_FILENAME}) — evaluating ${fragments.length} fragment(s)...`);

  // Sort by priority (lower = first)
  const sorted = [...fragments].sort((a, b) => a.priority - b.priority);

  // Evaluate all conditions in parallel
  const results = await Promise.all(
    sorted.map(async (frag): Promise<{ fragment: PromptFragment; result: ConditionResult }> => {
      const condFn = conditions[frag.id];
      if (!condFn) {
        log.warn(`No condition registered for fragment "${frag.id}" — skipping`);
        return { fragment: frag, result: false };
      }
      try {
        return { fragment: frag, result: await condFn() };
      } catch (err) {
        log.warn(`Condition "${frag.id}" threw — skipping: ${err}`);
        return { fragment: frag, result: false };
      }
    }),
  );

  // Track which targets have already been handled (for replace/remove — first match wins)
  const handledTargets = new Set<string>();

  for (const { fragment, result } of results) {
    if (result === false) {
      log.info(`[prompt] SKIP    "${fragment.id}" (condition false)`);
      continue;
    }

    const { target, action, content } = fragment;

    // For replace/remove, only the first matching fragment per target wins
    if ((action === 'replace' || action === 'remove') && handledTargets.has(target)) {
      log.info(`[prompt] SKIP    "${fragment.id}" (target "${target}" already handled)`);
      continue;
    }

    const vars = typeof result === 'object' ? result : {};
    const regex = markerRegex(target);

    switch (action) {
      case 'replace': {
        if (!content) break;
        const finalContent = interpolate(content, vars);
        if (regex.test(prompt)) {
          prompt = prompt.replace(regex, finalContent);
          handledTargets.add(target);
          const varsInfo = Object.keys(vars).length ? ` vars=${JSON.stringify(vars)}` : '';
          log.info(`[prompt] REPLACE [${target}] ← "${fragment.id}"${varsInfo}`);
        } else {
          log.warn(`[prompt] Marker "<!-- dynamic:${target} -->" not found in base prompt`);
        }
        break;
      }

      case 'remove': {
        if (regex.test(prompt)) {
          prompt = prompt.replace(regex, '');
          handledTargets.add(target);
          log.info(`[prompt] REMOVE  [${target}] by "${fragment.id}"`);
        }
        break;
      }

      case 'append': {
        if (!content) break;
        const finalContent = interpolate(content, vars);
        // If target specified, append after the closing marker; otherwise append to end
        if (target) {
          const closingMarker = `<!-- /dynamic:${target} -->`;
          const idx = prompt.indexOf(closingMarker);
          if (idx !== -1) {
            const insertAt = idx + closingMarker.length;
            prompt = prompt.slice(0, insertAt) + '\n\n' + finalContent + prompt.slice(insertAt);
          } else {
            prompt += '\n\n' + finalContent;
          }
        } else {
          prompt += '\n\n' + finalContent;
        }
        log.info(`[prompt] APPEND  [${target || 'end'}] ← "${fragment.id}"`);
        break;
      }
    }
  }

  // Clean up any remaining unhandled dynamic markers (leave their default content)
  // No action needed — unmatched markers keep their original content between the tags.
  // Strip the marker comments themselves so they don't leak into the prompt.
  prompt = prompt.replace(/<!-- \/?dynamic:\w[\w-]* -->\n?/g, '');

  return prompt;
}
