// ABOUTME: Template engine for system prompts using mustache with @path include functionality
// ABOUTME: Handles variable substitution and Claude Code-style @path includes with recursion guards

import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';
import mustache from 'mustache';
import { logger } from '@lace/agent/utils/logger';

export interface TemplateContext {
  [key: string]: unknown;
}

export class TemplateEngine {
  private readonly templateDirs: string[];
  private readonly processedIncludes = new Set<string>();
  private readonly useEmbedded: boolean;

  constructor(templateDirs: string | string[], opts: { useEmbedded?: boolean } = {}) {
    this.templateDirs = Array.isArray(templateDirs) ? templateDirs : [templateDirs];
    this.useEmbedded = opts.useEmbedded ?? false;
  }

  get usesEmbedded(): boolean {
    return this.useEmbedded;
  }

  /**
   * Render a template with the provided context
   */
  render(templatePath: string, context: TemplateContext): string {
    try {
      this.processedIncludes.clear();
      const templateContent = this.loadTemplate(templatePath);
      const processedContent = this.processIncludes(templateContent, path.dirname(templatePath));
      return mustache.render(processedContent, context);
    } catch (error) {
      logger.error('Failed to render template', {
        templatePath,
        error: error instanceof Error ? error.message : String(error),
      });
      return this.getFallbackContent(templatePath);
    }
  }

  /**
   * Render a template string directly (no file I/O) with the provided context.
   * Applies the same @path include expansion and mustache substitution as `render`.
   * Use this when the template body is already in memory (e.g. plugin-contributed
   * personas whose body is registered at runtime rather than written to disk).
   */
  renderString(content: string, context: TemplateContext): string {
    try {
      this.processedIncludes.clear();
      const processedContent = this.processIncludes(content, '.');
      return mustache.render(processedContent, context);
    } catch (error) {
      logger.error('Failed to render template string', {
        error: error instanceof Error ? error.message : String(error),
      });
      return this.getFallbackContent('<string>');
    }
  }

  /**
   * Load template content from embedded files or file system
   */
  private loadTemplate(templatePath: string): string {
    // First try embedded files - check for the template path directly
    try {
      if (
        this.useEmbedded &&
        typeof Bun !== 'undefined' &&
        'embeddedFiles' in Bun &&
        Bun.embeddedFiles
      ) {
        const targetPath = `packages/agent/config/agent-personas/${templatePath}`;

        for (const file of Bun.embeddedFiles) {
          if ((file as File).name.endsWith(targetPath)) {
            logger.debug('Loading template from embedded files', {
              templatePath,
              embeddedName: (file as File).name,
            });
            try {
              // For embedded files, use a sync approach by blocking on the Promise
              // This is not ideal but maintains the sync API for compatibility
              let content = '';
              let resolved = false;
              let error: Error | null = null;

              file
                .text()
                .then((text) => {
                  content = text;
                  resolved = true;
                })
                .catch((err) => {
                  error = err instanceof Error ? err : new Error(String(err));
                  resolved = true;
                });

              // Busy wait for the promise (not ideal but maintains sync compatibility)
              while (!resolved) {
                // Wait for async operation to complete
                spawnSync('sleep', ['0.001']);
              }

              if (error) throw new Error(String(error));
              return content;
            } catch (fileError) {
              logger.debug('Failed to read embedded file', {
                fileName: (file as File).name,
                error: String(fileError),
              });
              // Continue to try other files
            }
          }
        }
      }
    } catch (e) {
      logger.debug('Embedded template load failed, falling back to file system', {
        templatePath,
        error: String(e),
      });
    }

    // Fallback to file system approach (development)
    for (const templateDir of this.templateDirs) {
      const fullPath = path.resolve(templateDir, templatePath);

      if (fs.existsSync(fullPath)) {
        logger.debug('Loading template from file system', { templatePath, templateDir });
        return fs.readFileSync(fullPath, 'utf-8');
      } else {
        logger.error('Template file not found in directory', {
          templatePath,
          templateDir,
          fullPath,
          dirExists: fs.existsSync(templateDir),
          dirContents: fs.existsSync(templateDir)
            ? fs.readdirSync(templateDir)
            : 'directory does not exist',
        });
      }
    }

    logger.error('Template file not found in any directory', {
      templatePath,
      searchedDirs: this.templateDirs,
      allDirContents: this.templateDirs.map((dir) => ({
        dir,
        exists: fs.existsSync(dir),
        contents: fs.existsSync(dir) ? fs.readdirSync(dir) : 'not found',
      })),
    });

    throw new Error(`Template file not found in embedded files or directories: ${templatePath}`);
  }

  /**
   * Process Claude Code-style `@path/to/file.md` includes with recursion protection.
   *
   * A token is treated as a path reference only when:
   * - the `@` is at the start of the content or immediately preceded by whitespace
   *   (so `email@example.com` in prose is left alone), and
   * - the captured token contains at least one `/` or `.` (so bare `@username`
   *   mentions are left alone), and
   * - the token ends in an alphanumeric / underscore character (so trailing
   *   sentence punctuation is not captured into the path).
   *
   * Missing files: warn and emit the original `@path` text literally. Circular
   * references: detected via a per-render Set and replaced with an HTML comment
   * marker to break the cycle without crashing.
   */
  private processIncludes(content: string, currentDir: string): string {
    const includeRegex = /(?<=^|\s)@([A-Za-z0-9_/.-]*[A-Za-z0-9_])/g;
    let out = '';
    let lastIndex = 0;

    for (let m: RegExpExecArray | null; (m = includeRegex.exec(content)); ) {
      const includePath = m[1];
      out += content.slice(lastIndex, m.index);
      lastIndex = includeRegex.lastIndex;

      // Disambiguation: only treat as a path reference if it looks like a path.
      // Bare `@username` (no slash, no dot) is left untouched in prose.
      if (!includePath.includes('/') && !includePath.includes('.')) {
        out += `@${includePath}`;
        continue;
      }

      // Try embedded files first (Bun executable)
      let foundContent: string | null = null;
      if (
        this.useEmbedded &&
        typeof Bun !== 'undefined' &&
        'embeddedFiles' in Bun &&
        Bun.embeddedFiles
      ) {
        const targetPath = `packages/agent/config/agent-personas/${includePath}`;

        for (const file of Bun.embeddedFiles) {
          if ((file as File).name.endsWith(targetPath)) {
            try {
              // Use the same sync-over-async approach for consistency
              let content = '';
              let resolved = false;
              let error: Error | null = null;

              file
                .text()
                .then((text) => {
                  content = text;
                  resolved = true;
                })
                .catch((err) => {
                  error = err instanceof Error ? err : new Error(String(err));
                  resolved = true;
                });

              // Busy wait for the promise
              while (!resolved) {
                spawnSync('sleep', ['0.001']);
              }

              if (error) throw new Error(String(error));
              foundContent = content;
              break;
            } catch (_e) {
              // Continue to next file
            }
          }
        }
      }

      // Fallback: locate include file across templateDirs
      let foundPath: string | null = null;
      let foundTemplateDir = '';
      if (!foundContent) {
        for (const templateDir of this.templateDirs) {
          const fullIncludePath = path.resolve(templateDir, currentDir, includePath);
          const normalizedPath = path.normalize(fullIncludePath);
          if (fs.existsSync(normalizedPath)) {
            foundPath = normalizedPath;
            foundTemplateDir = templateDir;
            break;
          }
        }
      }

      if (!foundContent && !foundPath) {
        logger.warn('Include file not found', { includePath, searchedDirs: this.templateDirs });
        out += `@${includePath}`;
        continue;
      }

      const checkPath = foundPath || `embedded:${includePath}`;
      if (this.processedIncludes.has(checkPath)) {
        logger.warn('Circular include detected', { includePath, foundPath: checkPath });
        out += `<!-- Circular include: ${includePath} -->`;
        continue;
      }

      try {
        this.processedIncludes.add(checkPath);

        const includeContent = foundContent || fs.readFileSync(foundPath!, 'utf-8');
        const includeDir = foundTemplateDir
          ? path.dirname(path.relative(foundTemplateDir, foundPath!))
          : path.dirname(includePath);
        const processed = this.processIncludes(includeContent, includeDir);
        out += processed;
      } catch (error) {
        logger.error('Failed to process include', {
          includePath,
          foundPath: checkPath,
          error: error instanceof Error ? error.message : String(error),
        });
        out += `<!-- Include error: ${includePath} -->`;
      } finally {
        this.processedIncludes.delete(checkPath);
      }
    }
    out += content.slice(lastIndex);
    return out;
  }

  /**
   * Get fallback content when template processing fails
   */
  private getFallbackContent(templatePath: string): string {
    logger.warn('Using fallback content for template', { templatePath });
    return 'You are Lace, an AI coding assistant. Use the available tools to help with programming tasks.';
  }
}
