import { readFileSync, readdirSync, existsSync } from 'fs';
import { join } from 'path';
import { parse as parseYaml } from 'yaml';
import { userPiecesDir } from '../user-folder/paths.js';
import { logger } from '../logger.js';

export interface CatalogEntry {
  name: string;
  description: string;
  keywords: string[];
  source: 'builtin' | 'custom';
}

/**
 * Loads built-in pieces once at construction time and layers per-user custom
 * pieces (data/users/{userId}/pieces/*.yaml) on top with a 60-second TTL
 * cache.  Custom pieces with the same name as a built-in win (override).
 *
 * Usage:
 *   const catalog = new PieceCatalog('pieces', config.userFolderRoot ?? './data/users');
 *   const entries = catalog.getForUser(userId);   // used by classifyPiece
 *   catalog.invalidate(userId);                   // called after silent-fork / reflection write
 */
export class PieceCatalog {
  private builtins: CatalogEntry[] = [];
  private cache = new Map<string, { ts: number; entries: CatalogEntry[] }>();
  private readonly ttlMs = 60_000;

  constructor(
    private readonly builtinDir: string,
    private readonly dataDir: string,
  ) {
    this.loadBuiltins();
  }

  private loadBuiltins(): void {
    if (!existsSync(this.builtinDir)) {
      logger.warn(`[piece-catalog] builtinDir not found: ${this.builtinDir}`);
      return;
    }
    this.builtins = readdirSync(this.builtinDir)
      .filter(f => f.endsWith('.yaml'))
      .flatMap(f => {
        try {
          const doc = parseYaml(readFileSync(join(this.builtinDir, f), 'utf-8')) as Record<string, unknown> | null;
          return [{
            name: f.replace(/\.yaml$/, ''),
            description: typeof doc?.description === 'string' ? doc.description : '',
            keywords: Array.isArray((doc?.triggers as Record<string, unknown> | null)?.keywords)
              ? (doc!.triggers as { keywords: string[] }).keywords
              : [],
            source: 'builtin' as const,
          }];
        } catch (e) {
          logger.warn(`[piece-catalog] failed to parse builtin piece ${f}: ${e}`);
          return [];
        }
      });
    logger.info(`[piece-catalog] loaded ${this.builtins.length} builtin pieces from ${this.builtinDir}`);
  }

  /**
   * Returns the merged catalog for userId: built-ins layered with any custom
   * pieces the user has in data/users/{userId}/pieces/.  Result is cached for
   * ttlMs (60 s) and invalidated by invalidate().
   */
  getForUser(userId: string): CatalogEntry[] {
    const cached = this.cache.get(userId);
    if (cached && Date.now() - cached.ts < this.ttlMs) return cached.entries;

    const userDir = userPiecesDir(this.dataDir, userId);
    const overrides: CatalogEntry[] = existsSync(userDir)
      ? readdirSync(userDir)
          .filter(f => f.endsWith('.yaml'))
          .flatMap(f => {
            try {
              const doc = parseYaml(readFileSync(join(userDir, f), 'utf-8')) as Record<string, unknown> | null;
              return [{
                name: f.replace(/\.yaml$/, ''),
                description: typeof doc?.description === 'string' ? doc.description : '',
                keywords: Array.isArray((doc?.triggers as Record<string, unknown> | null)?.keywords)
                  ? (doc!.triggers as { keywords: string[] }).keywords
                  : [],
                source: 'custom' as const,
              }];
            } catch (e) {
              logger.warn(`[piece-catalog] failed to parse user piece ${f} for userId=${userId}: ${e}`);
              return [];
            }
          })
      : [];

    // Built-ins are the base; custom pieces with the same name override.
    const byName = new Map<string, CatalogEntry>(this.builtins.map(b => [b.name, b]));
    for (const o of overrides) byName.set(o.name, o);
    const entries = Array.from(byName.values());

    this.cache.set(userId, { ts: Date.now(), entries });
    logger.debug(`[piece-catalog] getForUser userId=${userId} builtins=${this.builtins.length} overrides=${overrides.length} total=${entries.length}`);
    return entries;
  }

  /**
   * Drops the TTL cache entry for userId so the next getForUser() call
   * re-reads disk.  Call after silent-fork, reflection piece write, or
   * any manual edit to the user's custom pieces directory.
   */
  invalidate(userId: string): void {
    this.cache.delete(userId);
    logger.debug(`[piece-catalog] invalidated cache for userId=${userId}`);
  }
}
