// @lifecycle canonical - JSON escaping/unescaping helpers for prompt templates.
// JSON utility functions

import * as path from 'node:path'; // Import path module
import { fileURLToPath } from 'url'; // For ES module __dirname equivalent

import nunjucks from 'nunjucks';

import type { PromptArgument } from '../types/index.js';

type PromptDefinition = { arguments: PromptArgument[] };
// JSON escaping utilities (moved here to avoid circular dependency)
function escapeJsonForNunjucks(jsonStr: string): string {
  return jsonStr
    .replace(/\{\{/g, '\\{\\{') // Escape Nunjucks variable syntax
    .replace(/\}\}/g, '\\}\\}') // Escape Nunjucks variable syntax
    .replace(/\{%/g, '\\{\\%') // Escape Nunjucks tag syntax
    .replace(/%\}/g, '\\%\\}') // Escape Nunjucks tag syntax
    .replace(/\{#/g, '\\{\\#') // Escape Nunjucks comment syntax
    .replace(/#\}/g, '\\#\\}'); // Escape Nunjucks comment syntax
}

function unescapeJsonFromNunjucks(escapedStr: string): string {
  return escapedStr
    .replace(/\\{\\{/g, '{{') // Unescape Nunjucks variable syntax
    .replace(/\\}\\}/g, '}}') // Unescape Nunjucks variable syntax
    .replace(/\\{\\%/g, '{%') // Unescape Nunjucks tag syntax
    .replace(/\\%\\}/g, '%}') // Unescape Nunjucks tag syntax
    .replace(/\\{\\#/g, '{#') // Unescape Nunjucks comment syntax
    .replace(/\\#\\}/g, '#}'); // Unescape Nunjucks comment syntax
}

// Lazy initialization to avoid Jest import.meta.url issues
let nunjucksEnv: nunjucks.Environment | null = null;

// Get prompt templates path (Jest-compatible version)
// Uses __dirname in Jest/CommonJS, import.meta.url in ES modules
function getPromptTemplatesPath(): string {
  // Check for test environment override first
  const promptsPathEnv = process.env['PROMPTS_PATH'];
  if (promptsPathEnv) {
    return promptsPathEnv;
  }

  if (typeof __dirname !== 'undefined') {
    // Jest/CommonJS environment - __dirname is available
    return path.resolve(__dirname, '../../prompts');
  }

  // ES modules environment - use import.meta.url
  // This code path only runs in real ES modules (not Jest)
  try {
    // Using eval to prevent Jest from parsing import.meta at compile time
    const metaUrl = eval('import.meta.url');
    const currentFileUrl = fileURLToPath(metaUrl);
    const currentDirPath = path.dirname(currentFileUrl);
    return path.resolve(currentDirPath, '../../prompts');
  } catch (error) {
    // No fallback - environment must support either __dirname (CommonJS/Jest) or import.meta.url (ESM)
    throw new Error(
      'Cannot determine prompt templates path: neither __dirname nor import.meta.url available. ' +
        'Set PROMPTS_PATH environment variable explicitly.'
    );
  }
}

// Initialize Nunjucks environment (called lazily)
function getNunjucksEnv(): nunjucks.Environment {
  if (!nunjucksEnv) {
    const promptTemplatesPath = getPromptTemplatesPath();
    nunjucksEnv = nunjucks.configure(promptTemplatesPath, {
      autoescape: false, // We're generating plain text prompts for LLM, not HTML
      throwOnUndefined: false, // Renders undefined variables as empty string for better compatibility
      watch: false, // Hot reload is handled by FileObserver/chokidar elsewhere; do not use Nunjucks watcher
      noCache: process.env['NODE_ENV'] === 'development', // Disable cache in development, enable in production
      tags: {
        blockStart: '{%',
        blockEnd: '%}',
        variableStart: '{{',
        variableEnd: '}}',
        commentStart: '{#',
        commentEnd: '#}',
      },
    });
  }
  return nunjucksEnv;
}

/**
 * Validates JSON arguments against the prompt's expected arguments
 * @param jsonArgs The JSON arguments to validate
 * @param prompt The prompt data containing expected arguments
 * @returns Object with validation results and sanitized arguments
 */
export function validateJsonArguments(
  jsonArgs: any,
  prompt: PromptDefinition
): {
  valid: boolean;
  errors?: string[];
  sanitizedArgs?: Record<string, string | number | boolean | null | any[]>;
} {
  const errors: string[] = [];
  const sanitizedArgs: Record<string, string | number | boolean | null | any[]> = {};

  // Check for unexpected properties
  const expectedArgNames = prompt.arguments.map((arg) => arg.name);
  const providedArgNames = Object.keys(jsonArgs);

  for (const argName of providedArgNames) {
    if (!expectedArgNames.includes(argName)) {
      errors.push(`Unexpected argument: ${argName}`);
    }
  }

  // Check for and sanitize expected arguments
  for (const arg of prompt.arguments) {
    const value = jsonArgs[arg.name];

    // All arguments are treated as optional now
    if (value !== undefined) {
      // Sanitize the value based on expected type
      // This is a simple implementation - expand as needed for your use case
      if (typeof value === 'string') {
        // Sanitize string inputs
        sanitizedArgs[arg.name] = value
          .replace(/[<>]/g, '') // Remove potentially dangerous HTML characters
          .trim();
      } else if (typeof value === 'number') {
        // Ensure it's a valid number
        sanitizedArgs[arg.name] = isNaN(value) ? 0 : value;
      } else if (typeof value === 'boolean') {
        sanitizedArgs[arg.name] = !!value; // Ensure boolean type
      } else if (Array.isArray(value)) {
        // For arrays, sanitize each element if they're strings
        sanitizedArgs[arg.name] = value.map((item) =>
          typeof item === 'string' ? item.replace(/[<>]/g, '').trim() : item
        );
      } else if (value !== null && typeof value === 'object') {
        // For objects, convert to string for simplicity
        sanitizedArgs[arg.name] = JSON.stringify(value);
      } else {
        // For any other type, convert to string
        sanitizedArgs[arg.name] = String(value);
      }
    }
  }

  const result: {
    valid: boolean;
    errors?: string[];
    sanitizedArgs?: Record<string, string | number | boolean | null | any[]>;
  } = {
    valid: errors.length === 0,
  };

  if (errors.length > 0) {
    result.errors = errors;
  }
  if (Object.keys(sanitizedArgs).length > 0) {
    result.sanitizedArgs = sanitizedArgs;
  }

  return result;
}

/**
 * Processes a template string by replacing placeholders with values using Nunjucks
 * @param template The template string with placeholders and potential Nunjucks logic
 * @param args The arguments to replace placeholders with
 * @param specialContext Special context values to replace first
 * @returns The processed template string
 */
export function processTemplate(
  template: string,
  args: Record<string, any>,
  specialContext: Record<string, string> = {}
): string {
  // Pre-escape any string values that might contain Nunjucks syntax
  const escapedArgs: Record<string, any> = {};
  for (const [key, value] of Object.entries(args)) {
    if (
      typeof value === 'string' &&
      (value.includes('{{') || value.includes('{%') || value.includes('{#'))
    ) {
      escapedArgs[key] = escapeJsonForNunjucks(value);
    } else {
      // Pass non-string values (arrays, objects) directly to Nunjucks
      escapedArgs[key] = value;
    }
  }

  const context = { ...specialContext, ...escapedArgs };

  try {
    // Use Nunjucks to render the template with the combined context
    const env = getNunjucksEnv();
    const rendered = env.renderString(template, context);

    // Unescape any values that were escaped for Nunjucks
    let unescapedResult = rendered;
    for (const [key, value] of Object.entries(escapedArgs)) {
      if (typeof value === 'string' && value !== args[key]) {
        // This arg was escaped, so we need to unescape it in the result
        const originalValue = args[key];
        const escapedValue = value;
        unescapedResult = unescapedResult.replace(
          new RegExp(escapedValue.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'),
          originalValue
        );
      }
    }

    return unescapedResult;
  } catch (error) {
    // Log the Nunjucks rendering error for debugging purposes.
    // The error will be re-thrown and should be handled by the calling function
    // (e.g., in TemplateProcessor) which can add more context like Prompt ID.
    if (error instanceof Error) {
      console.error('[Nunjucks Render Error] Failed to process template:', error.message);
      // Optionally, log error.stack for more detailed debugging if needed in development
      // if (process.env.NODE_ENV === 'development' && error.stack) {
      //   console.error(error.stack);
      // }
    } else {
      console.error(
        '[Nunjucks Render Error] Failed to process template with an unknown error object:',
        error
      );
    }
    throw error; // Re-throw the original error
  }
}

/**
 * Result of template processing with reference resolution.
 */
export interface ProcessTemplateWithRefsResult {
  /** The fully rendered template content */
  content: string;
  /** Script results from referenced prompts, keyed by "promptId:toolId" */
  scriptResults: Map<string, unknown>;
  /** All prompt IDs that were resolved during reference resolution */
  resolvedPromptIds: Set<string>;
  /** Inline script results from {{script:...}} references */
  inlineScriptResults?: Map<string, unknown>;
}

/**
 * Interface for script reference resolver.
 * Resolves {{script:id}} patterns in templates.
 */
export interface ScriptReferenceResolverPort {
  preResolve: (
    template: string,
    context: Record<string, unknown>,
    promptDir?: string
  ) => Promise<{
    resolvedTemplate: string;
    scriptResults: Map<string, unknown>;
    diagnostics: {
      scriptsResolved: number;
      warnings: string[];
      resolutionTimeMs: number;
    };
  }>;
  hasScriptReferences: (template: string) => boolean;
}

/**
 * Options for processTemplateWithRefs.
 */
export interface ProcessTemplateOptions {
  /** Prompt reference resolver for {{ref:...}} patterns */
  promptResolver?: {
    preResolve: (
      template: string,
      context: Record<string, unknown>
    ) => Promise<{
      resolvedTemplate: string;
      scriptResults: Map<string, unknown>;
      resolvedPromptIds: Set<string>;
    }>;
  };
  /** Script reference resolver for {{script:...}} patterns */
  scriptResolver?: ScriptReferenceResolverPort;
  /** Prompt directory for prompt-local script lookup */
  promptDir?: string;
}

/**
 * Processes a template string with {{ref:prompt_id}} and {{script:id}} reference resolution.
 *
 * This async wrapper:
 * 1. Pre-resolves all {{ref:...}} patterns using PromptReferenceResolver
 * 2. Pre-resolves all {{script:...}} patterns using ScriptReferenceResolver
 * 3. Processes the resolved template with Nunjucks via processTemplate()
 *
 * Use this instead of processTemplate() when templates may contain prompt or script references.
 *
 * @param template The template string with placeholders and potential {{ref:...}} or {{script:...}} patterns
 * @param args The arguments to replace placeholders with
 * @param specialContext Special context values to replace first
 * @param resolver Optional PromptReferenceResolver instance for reference resolution (legacy parameter)
 * @param options Optional options including script resolver
 * @returns The processed template string with references resolved, plus metadata
 *
 * @example
 * ```typescript
 * const result = await processTemplateWithRefs(
 *   'Intro: {{ref:shared_intro}}\nCount: {{script:analyzer.row_count}}',
 *   { topic: 'AI Safety' },
 *   {},
 *   promptResolver,
 *   { scriptResolver, promptDir: '/path/to/prompt' }
 * );
 * console.log(result.content); // Rendered with shared_intro and script output included
 * ```
 */
export async function processTemplateWithRefs(
  template: string,
  args: Record<string, unknown>,
  specialContext: Record<string, string> = {},
  resolver?: {
    preResolve: (
      template: string,
      context: Record<string, unknown>
    ) => Promise<{
      resolvedTemplate: string;
      scriptResults: Map<string, unknown>;
      resolvedPromptIds: Set<string>;
    }>;
  },
  options?: ProcessTemplateOptions
): Promise<ProcessTemplateWithRefsResult> {
  let resolvedTemplate = template;
  const scriptResults = new Map<string, unknown>();
  const resolvedPromptIds = new Set<string>();
  const inlineScriptResults = new Map<string, unknown>();

  // 1. Resolve {{ref:...}} patterns first (prompt references)
  const promptResolver = resolver ?? options?.promptResolver;
  if (promptResolver) {
    const combinedContext = { ...specialContext, ...args };
    const preResolveResult = await promptResolver.preResolve(resolvedTemplate, combinedContext);
    resolvedTemplate = preResolveResult.resolvedTemplate;

    // Copy script results from referenced prompts
    for (const [key, value] of preResolveResult.scriptResults) {
      scriptResults.set(key, value);
    }

    // Copy resolved IDs
    for (const id of preResolveResult.resolvedPromptIds) {
      resolvedPromptIds.add(id);
    }
  }

  // 2. Resolve {{script:...}} patterns (inline script references)
  const scriptResolver = options?.scriptResolver;
  if (scriptResolver?.hasScriptReferences(resolvedTemplate)) {
    const combinedContext = { ...specialContext, ...args };
    const scriptResolveResult = await scriptResolver.preResolve(
      resolvedTemplate,
      combinedContext,
      options?.promptDir
    );
    resolvedTemplate = scriptResolveResult.resolvedTemplate;

    // Copy inline script results
    for (const [key, value] of scriptResolveResult.scriptResults) {
      inlineScriptResults.set(key, value);
    }
  }

  // 3. Process with standard Nunjucks template processing
  const content = processTemplate(resolvedTemplate, args as Record<string, any>, specialContext);

  return {
    content,
    scriptResults,
    resolvedPromptIds,
    inlineScriptResults: inlineScriptResults.size > 0 ? inlineScriptResults : undefined,
  };
}
