import { readFile, readdir } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import yaml from 'js-yaml';
import type { MCPClientConnector } from '../mcp-client.js';
import type { WorkspaceManager } from '../../workspace/workspace-manager.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Knowledge directory: gateway/knowledge/ (3 levels up from harness/spawn/)
const KNOWLEDGE_DIR = join(__dirname, '..', '..', '..', 'knowledge');

// Prompt directory: gateway/prompt/ — base prompts and agent system prompts
// These are injected directly into context and are not surfaced via memory_lookup.
const PROMPT_DIR = join(__dirname, '..', '..', '..', 'prompt');

export interface WikiFrontmatter {
  maxTokens?: number;
  recursionLimit?: number;
  spawnsImages?: boolean;
  static_imports?: string[];
  dynamic_imports?: string[];
}

export interface WikiPage {
  frontmatter: WikiFrontmatter;
  body: string;
}

export interface SpawnContext {
  mcpClient?: MCPClientConnector;
  workspaceManager?: WorkspaceManager;
}

export type VirtualPageFn = (ctx: SpawnContext) => Promise<string>;

/**
 * WikiLoader loads markdown knowledge pages from disk, caches them in memory,
 * and supports virtual pages that are generated dynamically at spawn time.
 *
 * Page resolution order: virtual registry first, then disk.
 */
export class WikiLoader {
  private readonly pageCache = new Map<string, WikiPage>();
  private readonly virtualPages = new Map<string, VirtualPageFn>();
  private basePromptCache: string | null = null;

  /**
   * Register a virtual page that generates markdown dynamically.
   * Virtual pages are never cached — they run fresh every time they are imported.
   */
  registerVirtual(name: string, fn: VirtualPageFn): void {
    this.virtualPages.set(name, fn);
  }

  /**
   * Load and cache a wiki page from disk.
   * Looks in KNOWLEDGE_DIR only — prompt files are not surfaced here.
   * Returns null if the file does not exist.
   */
  async loadPage(name: string): Promise<WikiPage | null> {
    if (this.pageCache.has(name)) {
      return this.pageCache.get(name)!;
    }

    const filePath = join(KNOWLEDGE_DIR, `${name}.md`);
    let content: string;
    try {
      content = await readFile(filePath, 'utf-8');
    } catch {
      return null;
    }

    const page = this.parsePage(content);
    this.pageCache.set(name, page);
    return page;
  }

  /**
   * Load a prompt file from PROMPT_DIR (never exposed via memory_lookup).
   * Returns null if the file does not exist.
   */
  private async loadPromptPage(name: string): Promise<WikiPage | null> {
    const cacheKey = `__prompt__${name}`;
    if (this.pageCache.has(cacheKey)) {
      return this.pageCache.get(cacheKey)!;
    }

    const filePath = join(PROMPT_DIR, `${name}.md`);
    let content: string;
    try {
      content = await readFile(filePath, 'utf-8');
    } catch {
      return null;
    }

    const page = this.parsePage(content);
    this.pageCache.set(cacheKey, page);
    return page;
  }

  /**
   * Load an agent-specific page from PROMPT_DIR (throws if not found).
   */
  async loadAgentPage(agentName: string): Promise<WikiPage> {
    const page = await this.loadPromptPage(`agent-${agentName}`);
    if (!page) {
      throw new Error(`Agent prompt page not found: prompt/agent-${agentName}.md`);
    }
    return page;
  }

  /**
   * Load static imports (real .md files) and concatenate their bodies.
   * Results are cached via individual page caches.
   */
  async loadStaticImports(names: string[]): Promise<string> {
    const parts: string[] = [];
    for (const name of names) {
      const page = await this.loadPage(name);
      if (page) {
        parts.push(page.body);
      }
    }
    return parts.join('\n\n---\n\n');
  }

  /**
   * Load dynamic imports (virtual pages or disk fallback, never cached).
   * Failures are non-fatal and logged to stderr.
   */
  async loadDynamicImports(names: string[], ctx: SpawnContext): Promise<string> {
    const parts: string[] = [];
    for (const name of names) {
      const fn = this.virtualPages.get(name);
      if (fn) {
        try {
          const content = await fn(ctx);
          if (content) parts.push(content);
        } catch (err) {
          console.warn(`[WikiLoader] Virtual page '${name}' failed:`, err);
        }
      } else {
        // Fall back to disk (dynamic import, so bypass the in-memory cache)
        const filePath = join(KNOWLEDGE_DIR, `${name}.md`);
        try {
          const raw = await readFile(filePath, 'utf-8');
          const page = this.parsePage(raw);
          parts.push(page.body);
        } catch {
          console.warn(`[WikiLoader] Dynamic page '${name}' not found on disk or in registry`);
        }
      }
    }
    return parts.join('\n\n---\n\n');
  }

  /**
   * Return the base prompt text: prompt/index.md body + prompt/tools.md body, concatenated.
   * Result is cached for the lifetime of this WikiLoader instance.
   */
  async getBasePrompt(): Promise<string> {
    if (this.basePromptCache !== null) return this.basePromptCache;

    const [index, tools] = await Promise.all([
      this.loadPromptPage('index'),
      this.loadPromptPage('tools'),
    ]);

    const parts: string[] = [];
    if (index) parts.push(index.body);
    if (tools) parts.push(tools.body);
    this.basePromptCache = parts.join('\n\n');
    return this.basePromptCache;
  }

  /**
   * List all .md file names available in the knowledge directory (without extension).
   */
  async listPages(): Promise<string[]> {
    try {
      const files = await readdir(KNOWLEDGE_DIR);
      return files
        .filter(f => f.endsWith('.md'))
        .map(f => f.slice(0, -3));
    } catch {
      return [];
    }
  }

  /**
   * Parse a markdown file with optional YAML frontmatter.
   */
  private parsePage(content: string): WikiPage {
    const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
    if (fmMatch) {
      const frontmatter = (yaml.load(fmMatch[1]) as WikiFrontmatter) ?? {};
      const body = fmMatch[2].trim();
      return { frontmatter, body };
    }
    return { frontmatter: {}, body: content.trim() };
  }
}
