/**
 * Skills Loader - Discover and parse skills from disk
 */

import { existsSync, readdirSync, readFileSync, mkdirSync, cpSync } from 'node:fs';
import { execSync } from 'node:child_process';
import { join, resolve, delimiter } from 'node:path';
import matter from 'gray-matter';
import type { SkillEntry, ClawdbotMetadata } from './types.js';
import { getWorkingDir } from '../utils/paths.js';

// Skills directories (in priority order: project > agent > global > bundled > skills.sh)
const HOME = process.env.HOME || process.env.USERPROFILE || '';
const LETTA_HOME = process.env.RAILWAY_VOLUME_MOUNT_PATH
  ? join(process.env.RAILWAY_VOLUME_MOUNT_PATH, '.letta')
  : join(HOME, '.letta');
export const WORKING_DIR = getWorkingDir();
export const PROJECT_SKILLS_DIR = resolve(process.cwd(), '.skills');
export const WORKING_SKILLS_DIR = join(WORKING_DIR, '.skills'); // skills enabled via CLI
export const GLOBAL_SKILLS_DIR = join(LETTA_HOME, 'skills');
export const SKILLS_SH_DIR = join(HOME, '.agents', 'skills'); // skills.sh global installs

// Bundled skills from the lettabot repo itself
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
import { createLogger } from '../logger.js';

const log = createLogger('Skills');
const __dirname = dirname(fileURLToPath(import.meta.url));
export const BUNDLED_SKILLS_DIR = resolve(__dirname, '../../skills'); // lettabot/skills/

/**
 * Get the agent-scoped skills directory for a specific agent
 */
export function getAgentSkillsDir(agentId: string): string {
  return join(LETTA_HOME, 'agents', agentId, 'skills');
}

/**
 * Resolve subdirectories that contain executable skill files.
 */
function resolveSkillExecutableDirs(skillsDir: string): string[] {
  // Only add dirs that contain at least one executable (non-.md) file
  return readdirSync(skillsDir, { withFileTypes: true })
    .filter(d => d.isDirectory())
    .map(d => join(skillsDir, d.name))
    .filter(dir => {
      try {
        return readdirSync(dir).some(f => !f.endsWith('.md'));
      } catch { return false; }
    });
}

/**
 * Get executable skill directories for a specific agent.
 */
export function getAgentSkillExecutableDirs(agentId: string): string[] {
  const skillsDir = getAgentSkillsDir(agentId);
  if (!existsSync(skillsDir)) return [];
  return resolveSkillExecutableDirs(skillsDir);
}

/**
 * Get executable skill directories from the working-dir .skills/ folder.
 * These are skills enabled via `lettabot skills enable` or the sync wizard.
 */
export function getWorkingSkillExecutableDirs(): string[] {
  if (!existsSync(WORKING_SKILLS_DIR)) return [];
  return resolveSkillExecutableDirs(WORKING_SKILLS_DIR);
}

/**
 * Permanently prepend skill directories to PATH so that subprocesses
 * spawned subsequently inherit them.
 *
 * Includes both agent-scoped skills (.letta/agents/{id}/skills) and
 * working-dir skills (WORKING_DIR/.skills/) so that skills enabled via
 * `lettabot skills enable` are available without needing a feature-gate.
 *
 * This must be called BEFORE createSession/resumeSession -- the SDK spawns
 * the subprocess at session-creation time, not during initialize(). The
 * subprocess inherits its environment at fork; calling this afterward has
 * no effect on an already-running process.
 *
 * Unlike withAgentSkillsOnPath this does not restore the original PATH.
 * The prepend is idempotent: dirs already present are not added twice.
 */
export function prependSkillDirsToPath(agentId: string): void {
  const agentDirs = getAgentSkillExecutableDirs(agentId);
  const workingDirs = getWorkingSkillExecutableDirs();
  const seen = new Set(agentDirs);
  const dirs = [...agentDirs, ...workingDirs.filter(d => !seen.has(d))];

  if (dirs.length === 0) return;

  const originalPath = process.env.PATH || '';
  const existing = new Set(originalPath.split(delimiter).filter(Boolean));
  const prepend = dirs.filter(d => !existing.has(d));

  if (prepend.length > 0) {
    process.env.PATH = [...prepend, originalPath].filter(Boolean).join(delimiter);
    log.info(`Prepended ${prepend.length} skill dir(s) to PATH: ${prepend.join(', ')}`);
  }
}

/**
 * @deprecated Use prependSkillDirsToPath() before session creation instead.
 * This wrapper restores PATH after the callback, but the SDK spawns the
 * subprocess during createSession/resumeSession -- before initialize() is
 * called -- so PATH changes here don't reach the subprocess.
 */
export async function withAgentSkillsOnPath<T>(agentId: string, fn: () => Promise<T>): Promise<T> {
  prependSkillDirsToPath(agentId);
  return fn();
}

/**
 * Whether TTS credentials are configured enough to use voice-memo skill.
 */
export function isVoiceMemoConfigured(env: NodeJS.ProcessEnv = process.env): boolean {
  const provider = (env.TTS_PROVIDER || 'elevenlabs').toLowerCase();
  if (provider === 'openai') {
    return !!env.OPENAI_API_KEY;
  }
  if (provider === 'elevenlabs') {
    return !!env.ELEVENLABS_API_KEY;
  }
  return false;
}

/**
 * Check if a binary exists on PATH
 */
export function hasBinary(name: string): boolean {
  try {
    execSync(`command -v ${name}`, { stdio: 'ignore' });
    return true;
  } catch {
    return false;
  }
}

/**
 * Parse ClawdBot metadata from frontmatter
 * The metadata field is JSON-encoded in the YAML frontmatter
 */
function parseClawdbotMetadata(frontmatter: Record<string, unknown>): ClawdbotMetadata | undefined {
  const metadataRaw = frontmatter.metadata;
  
  if (!metadataRaw) return undefined;
  
  try {
    // metadata is typically a JSON string
    const parsed = typeof metadataRaw === 'string' 
      ? JSON.parse(metadataRaw)
      : metadataRaw;
    
    return parsed?.clawdbot as ClawdbotMetadata | undefined;
  } catch {
    return undefined;
  }
}

/**
 * Parse a single SKILL.md file
 */
export function parseSkillFile(filePath: string): SkillEntry | null {
  try {
    if (!existsSync(filePath)) return null;
    
    const content = readFileSync(filePath, 'utf-8');
    const { data: frontmatter } = matter(content);
    
    const name = frontmatter.name as string | undefined;
    const description = frontmatter.description as string | undefined;
    
    if (!name) return null;
    
    const clawdbot = parseClawdbotMetadata(frontmatter);
    
    return {
      name,
      description: description || '',
      emoji: clawdbot?.emoji || (frontmatter.emoji as string | undefined),
      homepage: frontmatter.homepage as string | undefined,
      filePath,
      baseDir: resolve(filePath, '..'),
      clawdbot,
    };
  } catch (e) {
    log.error(`Failed to parse skill at ${filePath}:`, e);
    return null;
  }
}

/**
 * Load all skills from a directory
 */
export function loadSkillsFromDir(skillsDir: string): SkillEntry[] {
  const skills: SkillEntry[] = [];
  
  if (!existsSync(skillsDir)) return skills;
  
  try {
    const entries = readdirSync(skillsDir, { withFileTypes: true });
    
    for (const entry of entries) {
      if (!entry.isDirectory()) continue;
      if (entry.name.startsWith('.')) continue;
      
      const skillPath = join(skillsDir, entry.name, 'SKILL.md');
      const skill = parseSkillFile(skillPath);
      
      if (skill) {
        skills.push(skill);
      }
    }
  } catch (e) {
    log.error(`Failed to load skills from ${skillsDir}:`, e);
  }
  
  return skills;
}

/**
 * Load all skills from the global skills directory
 */
export function loadGlobalSkills(): SkillEntry[] {
  return loadSkillsFromDir(GLOBAL_SKILLS_DIR);
}

/**
 * Load skills from multiple directories, merging results
 * Later directories override earlier ones (by skill name)
 */
export function loadSkills(dirs: string[] = [GLOBAL_SKILLS_DIR]): SkillEntry[] {
  const byName = new Map<string, SkillEntry>();
  
  for (const dir of dirs) {
    const skills = loadSkillsFromDir(dir);
    for (const skill of skills) {
      byName.set(skill.name, skill);
    }
  }
  
  return Array.from(byName.values());
}

/**
 * Load skills with full hierarchy support
 * Priority: project (.skills/) > agent (~/.letta/agents/{id}/skills/) > global (~/.letta/skills/) > skills.sh (~/.agents/skills/)
 */
export function loadAllSkills(agentId?: string | null): SkillEntry[] {
  const dirs: string[] = [];
  
  // skills.sh global installs (lowest priority)
  dirs.push(SKILLS_SH_DIR);
  
  // Bundled skills (ship with the project in skills/)
  dirs.push(BUNDLED_SKILLS_DIR);

  // Global skills (override bundled defaults)
  dirs.push(GLOBAL_SKILLS_DIR);
  
  // Agent-scoped skills (middle priority)
  if (agentId) {
    dirs.push(getAgentSkillsDir(agentId));
  }
  
  // Project skills (highest priority)
  dirs.push(PROJECT_SKILLS_DIR);
  
  return loadSkills(dirs);
}

/**
 * Install skills from a source directory to target directory
 */
function installSkillsFromDir(sourceDir: string, targetDir: string): string[] {
  const installed: string[] = [];
  
  if (!existsSync(sourceDir)) {
    return installed;
  }
  
  try {
    const skills = readdirSync(sourceDir, { withFileTypes: true })
      .filter(d => d.isDirectory() && !d.name.startsWith('.'))
      .map(d => d.name);
    
    for (const skill of skills) {
      const src = join(sourceDir, skill);
      const dest = join(targetDir, skill);
      if (!existsSync(dest)) {
        cpSync(src, dest, { recursive: true });
        installed.push(skill);
      }
    }
  } catch (e) {
    log.error(`Failed to install from ${sourceDir}:`, e);
  }
  
  return installed;
}

/**
 * Feature-gated skills - only installed when their feature is enabled
 */
export const FEATURE_SKILLS: Record<string, string[]> = {
  cron: ['scheduling'],      // Scheduling handles both one-off reminders and recurring cron jobs
  google: ['gog', 'google'], // Installed when Google/Gmail is configured
  bluesky: ['bluesky'],      // Installed when Bluesky is configured
  tts: ['voice-memo'],       // Voice memo replies via lettabot-tts helper
};

/**
 * Install specific skills by name from source directories
 */
function installSpecificSkills(
  skillNames: string[],
  sourceDirs: string[],
  targetDir: string
): string[] {
  const installed: string[] = [];
  
  for (const skillName of skillNames) {
    // Skip if already installed
    const dest = join(targetDir, skillName);
    if (existsSync(dest)) continue;
    
    // Find skill in source directories (later dirs have priority)
    for (const sourceDir of sourceDirs) {
      const src = join(sourceDir, skillName);
      if (existsSync(src) && existsSync(join(src, 'SKILL.md'))) {
        cpSync(src, dest, { recursive: true });
        installed.push(skillName);
        break;
      }
    }
  }
  
  return installed;
}

export interface SkillsInstallConfig {
  cronEnabled?: boolean;
  googleEnabled?: boolean;  // Gmail polling or Google integration
  blueskyEnabled?: boolean; // Bluesky integration
  ttsEnabled?: boolean;     // Voice memo replies via TTS providers
  additionalSkills?: string[]; // Explicitly enabled skills
}

/**
 * Install feature-gated skills to the working directory's .skills/ folder
 * 
 * Skills are NOT installed by default. They are enabled based on:
 * 1. Feature flags (cronEnabled, googleEnabled)
 * 2. Explicit list (additionalSkills)
 * 
 * Called on server startup
 */
export function installSkillsToWorkingDir(workingDir: string, config: SkillsInstallConfig = {}): void {
  const targetDir = join(workingDir, '.skills');
  
  // Ensure target directory exists
  mkdirSync(targetDir, { recursive: true });
  
  // Collect skills to install based on enabled features
  const requestedSkills: string[] = [];
  
  // Cron skills (always if cron is enabled)
  if (config.cronEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.cron);
  }
  
  // Google skills (if Gmail polling or Google is configured)
  if (config.googleEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.google);
  }

  // Voice memo skill (if TTS is configured)
  if (config.ttsEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.tts);
  }

  // Bluesky skills (if Bluesky is configured)
  if (config.blueskyEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.bluesky);
  }

  // Additional explicitly enabled skills
  if (config.additionalSkills?.length) {
    requestedSkills.push(...config.additionalSkills);
  }

  const skillsToInstall = Array.from(new Set(requestedSkills));

  if (skillsToInstall.length === 0) {
    log.info('No feature-gated skills to install');
    return;
  }
  
  // Source directories (later has priority)
  const sourceDirs = [SKILLS_SH_DIR, BUNDLED_SKILLS_DIR, PROJECT_SKILLS_DIR];
  
  // Install the specific skills
  const installed = installSpecificSkills(skillsToInstall, sourceDirs, targetDir);
  
  if (installed.length > 0) {
    log.info(`Installed ${installed.length} skill(s): ${installed.join(', ')}`);
  }
}



/**
 * Install feature-gated skills to the agent-scoped skills directory
 * (.letta/agents/{agentId}/skills)
 * 
 * This aligns with Letta Code CLI which uses agent-scoped skills.
 * Called after agent creation in bot.ts.
 */
export function installSkillsToAgent(agentId: string, config: SkillsInstallConfig = {}): void {
  const targetDir = getAgentSkillsDir(agentId);
  
  // Ensure target directory exists
  mkdirSync(targetDir, { recursive: true });
  
  // Collect skills to install based on enabled features
  const requestedSkills: string[] = [];
  
  // Cron skills (always if cron is enabled)
  if (config.cronEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.cron);
  }
  
  // Google skills (if Gmail polling or Google is configured)
  if (config.googleEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.google);
  }

  // Voice memo skill (if TTS is configured)
  if (config.ttsEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.tts);
  }

  // Bluesky skills (if Bluesky is configured)
  if (config.blueskyEnabled) {
    requestedSkills.push(...FEATURE_SKILLS.bluesky);
  }

  // Additional explicitly enabled skills
  if (config.additionalSkills?.length) {
    requestedSkills.push(...config.additionalSkills);
  }

  const skillsToInstall = Array.from(new Set(requestedSkills));

  if (skillsToInstall.length === 0) {
    return; // No skills to install - silent return
  }
  
  // Source directories (later has priority)
  const sourceDirs = [SKILLS_SH_DIR, BUNDLED_SKILLS_DIR, PROJECT_SKILLS_DIR];
  
  // Install the specific skills
  const installed = installSpecificSkills(skillsToInstall, sourceDirs, targetDir);
  
  if (installed.length > 0) {
    log.info(`Installed ${installed.length} skill(s) to agent: ${installed.join(', ')}`);
  }
}
