/**
 * Skills Registry
 *
 * Manages loading and accessing skills from:
 * 1. Built-in skills (in package)
 * 2. User skills (.emdash/skills/)
 */

import { promises as fs } from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import {
  type Skill,
  type SkillConfig,
  type SkillInvocation,
  parseFrontmatter,
  generateSkillContent,
  isValidSkillName,
} from './types.js';
import { createLogger, type Logger } from '../utils/logger.js';

/**
 * Default user skills directory
 */
const USER_SKILLS_DIR = '.emdash/skills';

/**
 * Skill file name
 */
const SKILL_FILE = 'SKILL.md';

/**
 * Skills Registry Options
 */
export interface SkillRegistryOptions {
  /** Repository root directory */
  repoRoot?: string;
  /** User skills directory (relative to repoRoot) */
  userSkillsDir?: string;
  /** Built-in skills directory */
  builtinSkillsDir?: string;
  /** Logger instance */
  logger?: Logger;
}

/**
 * Skills Registry
 *
 * Singleton that manages skill loading and access.
 */
export class SkillRegistry {
  private static instance: SkillRegistry | null = null;

  private repoRoot: string;
  private userSkillsDir: string;
  private builtinSkillsDir: string;
  private skills: Map<string, Skill> = new Map();
  private logger: Logger;
  private loaded = false;

  private constructor(options?: SkillRegistryOptions) {
    this.repoRoot = options?.repoRoot ?? process.cwd();
    this.userSkillsDir = path.join(
      this.repoRoot,
      options?.userSkillsDir ?? USER_SKILLS_DIR
    );

    // Built-in skills directory (in package)
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    this.builtinSkillsDir = options?.builtinSkillsDir ??
      path.join(__dirname, '..', '..', 'skills');

    this.logger = options?.logger ?? createLogger({ name: 'skill-registry' });
  }

  /**
   * Get singleton instance
   */
  static getInstance(options?: SkillRegistryOptions): SkillRegistry {
    if (!SkillRegistry.instance) {
      SkillRegistry.instance = new SkillRegistry(options);
    }
    return SkillRegistry.instance;
  }

  /**
   * Reset singleton (for testing)
   */
  static resetInstance(): void {
    SkillRegistry.instance = null;
  }

  /**
   * Load all skills from both directories
   */
  async loadSkills(): Promise<Map<string, Skill>> {
    this.skills.clear();

    // Load built-in skills first
    await this.loadSkillsFromDir(this.builtinSkillsDir, true);

    // Load user skills (can override built-in)
    await this.loadSkillsFromDir(this.userSkillsDir, false);

    this.loaded = true;
    this.logger.debug({ count: this.skills.size }, 'Loaded skills');
    return this.skills;
  }

  /**
   * Load skills from a directory
   */
  private async loadSkillsFromDir(dir: string, builtin: boolean): Promise<void> {
    try {
      const entries = await fs.readdir(dir, { withFileTypes: true });

      for (const entry of entries) {
        if (!entry.isDirectory()) continue;

        const skillDir = path.join(dir, entry.name);
        const skillFile = path.join(skillDir, SKILL_FILE);

        try {
          const content = await fs.readFile(skillFile, 'utf-8');
          const skill = await this.parseSkill(entry.name, content, skillFile, skillDir, builtin);
          this.skills.set(skill.name, skill);
        } catch (error) {
          if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
            this.logger.warn({ skill: entry.name, error }, 'Failed to load skill');
          }
        }
      }
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        this.logger.debug({ dir, error }, 'Skills directory not found');
      }
    }
  }

  /**
   * Parse skill from file content
   */
  private async parseSkill(
    dirName: string,
    content: string,
    filePath: string,
    skillDir: string,
    builtin: boolean
  ): Promise<Skill> {
    const { config, instructions } = parseFrontmatter(content);

    // Discover scripts in skill directory
    const scripts = await this.discoverScripts(skillDir);

    return {
      name: config.name ?? dirName,
      description: config.description ?? '',
      instructions,
      tools: config.tools ?? [],
      userInvocable: config.user_invocable ?? false,
      filePath,
      scripts,
      builtin,
    };
  }

  /**
   * Discover executable scripts in skill directory
   */
  private async discoverScripts(skillDir: string): Promise<string[]> {
    const scripts: string[] = [];

    try {
      const entries = await fs.readdir(skillDir, { withFileTypes: true });

      for (const entry of entries) {
        if (!entry.isFile()) continue;
        if (entry.name === SKILL_FILE) continue;

        const filePath = path.join(skillDir, entry.name);

        // Check if .sh file or has shebang
        if (entry.name.endsWith('.sh')) {
          scripts.push(filePath);
          continue;
        }

        try {
          const content = await fs.readFile(filePath, 'utf-8');
          if (content.startsWith('#!')) {
            scripts.push(filePath);
          }
        } catch {
          // Ignore read errors
        }
      }
    } catch {
      // Ignore directory read errors
    }

    return scripts;
  }

  /**
   * Ensure skills are loaded
   */
  private async ensureLoaded(): Promise<void> {
    if (!this.loaded) {
      await this.loadSkills();
    }
  }

  /**
   * Get a skill by name
   */
  async getSkill(name: string): Promise<Skill | null> {
    await this.ensureLoaded();
    return this.skills.get(name) ?? null;
  }

  /**
   * List all skill names
   */
  async listSkills(): Promise<string[]> {
    await this.ensureLoaded();
    return Array.from(this.skills.keys()).sort();
  }

  /**
   * Get all skills
   */
  async getAllSkills(): Promise<Map<string, Skill>> {
    await this.ensureLoaded();
    return new Map(this.skills);
  }

  /**
   * Get user-invocable skills
   */
  async getUserInvocableSkills(): Promise<Skill[]> {
    await this.ensureLoaded();
    return Array.from(this.skills.values())
      .filter((s) => s.userInvocable)
      .sort((a, b) => a.name.localeCompare(b.name));
  }

  /**
   * Invoke a skill
   */
  async invokeSkill(name: string, args: string = ''): Promise<SkillInvocation | null> {
    const skill = await this.getSkill(name);
    if (!skill) return null;

    return {
      skillName: skill.name,
      description: skill.description,
      instructions: skill.instructions,
      tools: skill.tools,
      scripts: skill.scripts,
      args,
    };
  }

  /**
   * Generate skills section for system prompt
   */
  async getSkillsForPrompt(): Promise<string> {
    const invocable = await this.getUserInvocableSkills();

    if (invocable.length === 0) {
      return '';
    }

    const lines = [
      '## Available Skills',
      '',
      'The following skills can be invoked:',
      '',
    ];

    for (const skill of invocable) {
      lines.push(`- **/${skill.name}**: ${skill.description}`);
    }

    return lines.join('\n');
  }

  /**
   * Create a new user skill
   */
  async createSkill(
    name: string,
    description: string,
    options?: { userInvocable?: boolean; tools?: string[]; instructions?: string }
  ): Promise<Skill> {
    if (!isValidSkillName(name)) {
      throw new Error(`Invalid skill name: ${name}. Must be lowercase with hyphens/underscores, max 64 chars.`);
    }

    const skillDir = path.join(this.userSkillsDir, name);
    const skillFile = path.join(skillDir, SKILL_FILE);

    // Check if already exists
    try {
      await fs.access(skillFile);
      throw new Error(`Skill "${name}" already exists`);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
        throw error;
      }
    }

    // Create skill directory and file
    await fs.mkdir(skillDir, { recursive: true });

    const config: SkillConfig = {
      name,
      description,
      user_invocable: options?.userInvocable ?? true,
      tools: options?.tools ?? [],
    };

    const content = generateSkillContent(config, options?.instructions ?? `# ${name}\n\nSkill instructions here.`);
    await fs.writeFile(skillFile, content, 'utf-8');

    // Reload to pick up new skill
    this.loaded = false;
    await this.ensureLoaded();

    return this.skills.get(name)!;
  }

  /**
   * Delete a user skill
   */
  async deleteSkill(name: string): Promise<boolean> {
    const skill = await this.getSkill(name);
    if (!skill) return false;
    if (skill.builtin) {
      throw new Error(`Cannot delete built-in skill: ${name}`);
    }

    const skillDir = path.join(this.userSkillsDir, name);

    try {
      await fs.rm(skillDir, { recursive: true });
      this.skills.delete(name);
      this.logger.info({ name }, 'Deleted skill');
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Reload skills
   */
  async reload(): Promise<void> {
    this.loaded = false;
    await this.loadSkills();
  }
}

/**
 * Convenience functions
 */

/**
 * Get the skill registry singleton
 */
export function getSkillRegistry(options?: SkillRegistryOptions): SkillRegistry {
  return SkillRegistry.getInstance(options);
}

/**
 * Load all skills
 */
export async function loadSkills(skillsDir?: string): Promise<Map<string, Skill>> {
  const registry = SkillRegistry.getInstance({ userSkillsDir: skillsDir });
  return registry.loadSkills();
}

/**
 * Get a skill by name
 */
export async function getSkill(name: string): Promise<Skill | null> {
  return SkillRegistry.getInstance().getSkill(name);
}

/**
 * List all skill names
 */
export async function listSkills(): Promise<string[]> {
  return SkillRegistry.getInstance().listSkills();
}

/**
 * Get user-invocable skills
 */
export async function getUserInvocableSkills(): Promise<Skill[]> {
  return SkillRegistry.getInstance().getUserInvocableSkills();
}
