'use strict';
/**
 * Skill Loader -- scans skill directories for SKILL.md files,
 * parses YAML frontmatter + markdown body, returns structured metadata.
 *
 * Precedence (highest first): workspace > user > bundled > registry
 */
const fs = require('fs');
const path = require('path');
const yaml = require('js-yaml');

const BUNDLED_DIR = path.join(__dirname, '_bundled');
const USER_DIR = path.join(process.env.HOME || '', '.wall-e', 'skills');
const REGISTRY_DIR = path.join(process.env.HOME || '', '.wall-e', 'registry-skills');
const CODEX_HOME = process.env.CODEX_HOME || path.join(process.env.HOME || '', '.codex');
const CODEX_USER_DIR = path.join(CODEX_HOME, 'skills');
const OPENCODE_USER_DIR = path.join(process.env.HOME || '', '.config', 'opencode', 'skills');
const CLAUDE_USER_DIR = path.join(process.env.HOME || '', '.claude', 'skills');
const GEMINI_USER_DIR = path.join(process.env.HOME || '', '.gemini', 'skills');
const AGENTS_USER_DIR = path.join(process.env.HOME || '', '.agents', 'skills');

// In-memory cache keyed by workspace dir -- call reloadSkills() to clear.
let _cachedSkills = new Map();
let _cachedSkillEntries = new Map();

// Last parse error per file path, for diagnostics. Cleared on reloadSkills().
const _parseErrors = new Map();

// ── YAML Parser (js-yaml) ────────────────────────────────────────────

/**
 * Parse a SKILL.md file content into { meta, body }.
 * meta = YAML frontmatter fields, body = markdown instructions.
 *
 * Returns null if the file lacks a valid frontmatter block.
 * Throws YAMLException with line info if the frontmatter YAML is malformed --
 * callers (loadSkill) catch and surface this via console.warn + _parseErrors.
 */
function parseSkillMd(content) {
  const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
  if (!match) return null;

  const yamlText = match[1];
  const body = match[2].trim();
  const meta = yaml.load(yamlText) || {};

  if (typeof meta !== 'object' || Array.isArray(meta)) {
    throw new yaml.YAMLException('frontmatter must be a YAML mapping (key: value pairs)');
  }

  return { meta, body };
}

/**
 * Back-compat shim. The old hand-rolled parser was exported as `parseYaml`;
 * keep the name so any external caller continues to work, but route through
 * js-yaml. Returns an empty object on parse failure (old behavior was lossy
 * too, just silently). Throws are reserved for parseSkillMd which carries
 * file context.
 */
function parseYaml(text) {
  try {
    const out = yaml.load(text);
    return out && typeof out === 'object' && !Array.isArray(out) ? out : {};
  } catch {
    return {};
  }
}

// ── Skill Loading ────────────────────────────────────────────────────

/**
 * Load a single skill from its directory path.
 * Returns null if SKILL.md is missing or invalid.
 */
function loadSkill(skillDir, source) {
  const skillMdPath = path.join(skillDir, 'SKILL.md');
  if (!fs.existsSync(skillMdPath)) return null;

  let parsed;
  let content;
  try {
    content = fs.readFileSync(skillMdPath, 'utf8');
    parsed = parseSkillMd(content);
  } catch (err) {
    if (isExternalSkillSource(source) && content) {
      _parseErrors.delete(skillMdPath);
      return buildExternalMarkdownSkill(skillDir, skillMdPath, content, source);
    }
    // Malformed YAML frontmatter -- surface diagnostics so the user can fix it,
    // instead of silently dropping the skill.
    const reason = err && err.reason ? err.reason : (err && err.message) || String(err);
    const mark = err && err.mark;
    const where = mark ? ` (line ${mark.line + 1}, col ${mark.column + 1})` : '';
    console.warn(`[skill-loader] Skipping ${skillMdPath}: YAML parse error${where}: ${reason}`);
    _parseErrors.set(skillMdPath, { reason, line: mark ? mark.line + 1 : null, column: mark ? mark.column + 1 : null });
    return null;
  }

  if (!parsed) {
    if (isExternalSkillSource(source)) {
      _parseErrors.delete(skillMdPath);
      return buildExternalMarkdownSkill(skillDir, skillMdPath, content, source);
    }
    console.warn(`[skill-loader] Skipping ${skillMdPath}: missing frontmatter (expected '---' delimited YAML block at top of file)`);
    _parseErrors.set(skillMdPath, { reason: 'missing frontmatter' });
    return null;
  }
  if (!parsed.meta || !parsed.meta.name) {
    if (isExternalSkillSource(source)) {
      _parseErrors.delete(skillMdPath);
      return buildExternalMarkdownSkill(skillDir, skillMdPath, content, source, parsed);
    }
    console.warn(`[skill-loader] Skipping ${skillMdPath}: frontmatter missing required 'name' field`);
    _parseErrors.set(skillMdPath, { reason: "missing required 'name' field" });
    return null;
  }

  // Cleared on successful load.
  _parseErrors.delete(skillMdPath);

  // Plugin SDK Item A: optional walle.plugin.json sibling manifest.
  // If present and kind=skill, augment fields (manifest wins on conflict).
  let pluginManifest = null;
  try {
    const { loadManifestFromDir } = require('../plugins/manifest-loader');
    const result = loadManifestFromDir(skillDir, source);
    if (result.manifest && result.manifest.kind === 'skill') {
      pluginManifest = result.manifest;
    }
    if (result.diagnostics && result.diagnostics.length) {
      try {
        const { addDiagnostics } = require('../plugins/diagnostics');
        addDiagnostics(result.diagnostics, { component: 'skill-loader' });
      } catch {}
    }
  } catch {}

  try {
    const sourceName = source || (skillDir.includes('_bundled') ? 'bundled' : 'user');
    const declaredAgents = normalizeSkillAgents(
      pluginManifest?.agents ||
      pluginManifest?.compatibleAgents ||
      parsed.meta.agents ||
      parsed.meta.agent ||
      parsed.meta.compatible_agents ||
      parsed.meta.compatibleAgents ||
      parsed.meta.compatibility?.agents
    );

    return {
      name: pluginManifest?.id || parsed.meta.name,
      description: pluginManifest?.description || parsed.meta.description || '',
      version: pluginManifest?.version || parsed.meta.version || '0.0.0',
      execution: parsed.meta.execution || 'agent',
      entry: parsed.meta.entry || null,
      args: parsed.meta.args || [],
      author: parsed.meta.author || null,
      requires: pluginManifest?.requires || parsed.meta.requires || {},
      trigger: parsed.meta.trigger || {},
      config: parsed.meta.config || {},
      tags: parsed.meta.tags || [],
      permissions: parsed.meta.permissions || [],
      // Item B (multi-agent deep-dive): lane groups skills for UI filtering;
      // model_policy structures the candidate-model fallback chain. Both
      // optional and additive — existing skills work unchanged.
      lane: parsed.meta.lane || null,
      model_policy: parsed.meta.model_policy || null,
      auth_profile: parsed.meta.auth_profile || null,
      // Item A (error-recovery deep-dive): per-skill retry config.
      // Currently supports `backoff_schedule_ms: [...]`; can extend.
      retry: parsed.meta.retry || null,
      // Item A (plugin SDK deep-dive): plugin manifest passthrough.
      capabilities: pluginManifest?.capabilities || {},
      agents: declaredAgents.length ? declaredAgents : skillCompatibleAgentsForSource(sourceName),
      pluginManifest,
      pluginApi: pluginManifest?.pluginApi || null,
      instructions: parsed.body,
      dir: skillDir,
      filePath: skillMdPath,
      source: sourceName,
    };
  } catch (err) {
    console.error(`[skill-loader] Failed to load ${skillDir}: ${err.message}`);
    try {
      const { addDiagnostic } = require('../plugins/diagnostics');
      addDiagnostic({
        level: 'error',
        message: `Failed to load skill ${skillDir}: ${err.message}`,
        source: source || 'unknown',
        component: 'skill-loader',
      });
    } catch {}
    return null;
  }
}

function isExternalSkillSource(source) {
  return /^(codex|opencode|claude|gemini|agents)(?:-|$)/.test(String(source || ''));
}

function buildExternalMarkdownSkill(skillDir, skillMdPath, content, source, parsed = null) {
  const meta = parsed?.meta && typeof parsed.meta === 'object' ? parsed.meta : {};
  const instructions = String(parsed?.body || stripLooseFrontmatter(content) || content || '').trim();
  const name = normalizeExternalSkillName(meta.name || path.basename(skillDir));
  const sourceName = source || 'external';
  const declaredAgents = normalizeSkillAgents(
    meta.agents ||
    meta.agent ||
    meta.compatible_agents ||
    meta.compatibleAgents ||
    meta.compatibility?.agents
  );
  return {
    name,
    description: oneLine(meta.description || extractMarkdownDescription(instructions) || ''),
    version: meta.version || '0.0.0',
    execution: meta.execution || 'agent',
    entry: meta.entry || null,
    args: meta.args || [],
    author: meta.author || null,
    requires: meta.requires || {},
    trigger: meta.trigger || {},
    config: meta.config || {},
    tags: meta.tags || [],
    permissions: meta.permissions || [],
    lane: meta.lane || null,
    model_policy: meta.model_policy || null,
    auth_profile: meta.auth_profile || null,
    retry: meta.retry || null,
    capabilities: {},
    agents: declaredAgents.length ? declaredAgents : skillCompatibleAgentsForSource(sourceName),
    pluginManifest: null,
    pluginApi: null,
    instructions,
    dir: skillDir,
    filePath: skillMdPath,
    source: sourceName,
  };
}

function normalizeExternalSkillName(value) {
  return String(value || '')
    .trim()
    .replace(/^@skill\//, '')
    .replace(/\s+/g, '-');
}

function stripLooseFrontmatter(content) {
  const text = String(content || '');
  const match = text.match(/^---\s*\n[\s\S]*?\n---\s*\n?([\s\S]*)$/);
  return match ? match[1] : text;
}

function extractMarkdownDescription(content) {
  const lines = String(content || '').split(/\r?\n/);
  for (const raw of lines) {
    const line = raw.trim();
    if (!line || line === '---' || line.startsWith('```')) continue;
    const heading = line.match(/^#{1,3}\s+(.+)$/);
    if (heading) return heading[1].trim();
    if (!line.startsWith('#') && !line.startsWith('---')) return line;
  }
  return '';
}

function oneLine(value, limit = 180) {
  const text = String(value || '').replace(/\s+/g, ' ').trim();
  return text.length > limit ? `${text.slice(0, Math.max(0, limit - 3))}...` : text;
}

/**
 * Scan a directory for subdirectories containing SKILL.md.
 * Returns array of skill objects.
 */
function loadSkillsFromDir(dir, source) {
  if (!fs.existsSync(dir)) return [];

  const results = [];
  let entries;
  try {
    entries = fs.readdirSync(dir, { withFileTypes: true });
  } catch {
    return [];
  }

  for (const entry of entries) {
    if (!entry.isDirectory()) continue;
    const skill = loadSkill(path.join(dir, entry.name), source);
    if (skill) results.push(skill);
  }

  return results;
}

function skillSearchLocations(workspaceDir) {
  const locations = projectSkillLocations(workspaceDir);

  // User/global skills, highest precedence first.
  locations.push({ dir: USER_DIR, source: 'user' });
  locations.push({ dir: CODEX_USER_DIR, source: 'codex-user' });
  locations.push({ dir: OPENCODE_USER_DIR, source: 'opencode-user' });
  locations.push({ dir: CLAUDE_USER_DIR, source: 'claude-user' });
  locations.push({ dir: GEMINI_USER_DIR, source: 'gemini-user' });
  locations.push({ dir: AGENTS_USER_DIR, source: 'agents-user' });

  // Bundled skills
  locations.push({ dir: BUNDLED_DIR, source: 'bundled' });

  // Registry skills (lowest precedence)
  locations.push({ dir: REGISTRY_DIR, source: 'registry' });

  return locations;
}

/**
 * Load every skill entry from every search directory without de-duping names.
 * Agent-specific callers should filter first and de-dupe second so a same-named
 * skill from one agent cannot hide the matching skill for another agent.
 */
function loadAllSkillEntries(workspaceDir) {
  const cacheKey = workspaceDir ? path.resolve(workspaceDir) : '';
  if (_cachedSkillEntries.has(cacheKey)) return _cachedSkillEntries.get(cacheKey);

  const entries = [];
  for (const loc of skillSearchLocations(workspaceDir)) {
    const skills = loadSkillsFromDir(loc.dir, loc.source);
    for (const skill of skills) entries.push(skill);
  }

  _cachedSkillEntries.set(cacheKey, entries);
  return entries;
}

/**
 * Load all installed skills from all search directories.
 * Precedence (highest first): workspace > user > bundled > registry.
 * Higher-precedence directories win on name conflicts.
 * Results are cached -- call reloadSkills() to refresh.
 */
function loadAllSkills(workspaceDir) {
  const cacheKey = workspaceDir ? path.resolve(workspaceDir) : '';
  if (_cachedSkills.has(cacheKey)) return _cachedSkills.get(cacheKey);

  const skillsByName = new Map();

  for (const skill of loadAllSkillEntries(workspaceDir)) {
    // Higher precedence (earlier in list) wins -- don't overwrite
    if (!skillsByName.has(skill.name)) {
      skillsByName.set(skill.name, skill);
    }
  }

  const skills = [...skillsByName.values()].sort((a, b) => a.name.localeCompare(b.name));
  _cachedSkills.set(cacheKey, skills);
  return skills;
}

function projectSkillLocations(workspaceDir) {
  if (!workspaceDir) return [];
  const locations = [];
  for (const dir of projectAncestors(workspaceDir)) {
    locations.push({ dir: path.join(dir, '.wall-e', 'skills'), source: 'workspace' });
    locations.push({ dir: path.join(dir, '.codex', 'skills'), source: 'codex-project' });
    locations.push({ dir: path.join(dir, '.opencode', 'skills'), source: 'opencode-project' });
    locations.push({ dir: path.join(dir, '.claude', 'skills'), source: 'claude-project' });
    locations.push({ dir: path.join(dir, '.gemini', 'skills'), source: 'gemini-project' });
    locations.push({ dir: path.join(dir, '.agents', 'skills'), source: 'agents-project' });
  }
  return locations;
}

function projectAncestors(workspaceDir) {
  const out = [];
  let current = path.resolve(workspaceDir);
  const seen = new Set();
  while (current && !seen.has(current)) {
    seen.add(current);
    out.push(current);
    if (isGitRoot(current)) break;
    const parent = path.dirname(current);
    if (!parent || parent === current) break;
    current = parent;
  }
  return out;
}

function isGitRoot(dir) {
  try {
    return fs.existsSync(path.join(dir, '.git'));
  } catch {
    return false;
  }
}

/**
 * Find a skill by exact name.
 */
function findSkill(name, workspaceDir) {
  const skills = loadAllSkills(workspaceDir);
  return skills.find(s => s.name === name) || null;
}

/**
 * Search skills by matching query against name, description, and tags.
 * Returns skills sorted by relevance (number of matching terms).
 */
function searchSkills(query, workspaceDir) {
  const skills = loadAllSkills(workspaceDir);
  if (!query || !query.trim()) return skills;

  const terms = query.toLowerCase().trim().split(/\s+/);

  return skills.filter(skill => {
    const searchable = [
      skill.name,
      skill.description,
      ...(skill.tags || []),
    ].join(' ').toLowerCase();

    return terms.some(term => searchable.includes(term));
  }).sort((a, b) => {
    const aText = [a.name, a.description, ...(a.tags || [])].join(' ').toLowerCase();
    const bText = [b.name, b.description, ...(b.tags || [])].join(' ').toLowerCase();
    const aScore = terms.filter(t => aText.includes(t)).length;
    const bScore = terms.filter(t => bText.includes(t)).length;
    return bScore - aScore;
  });
}

/**
 * Clear the cache so next loadAllSkills() re-reads from disk.
 */
function reloadSkills() {
  _cachedSkills.clear();
  _cachedSkillEntries.clear();
  _parseErrors.clear();
}

function normalizeSkillAgentType(value) {
  if (!value) return null;
  const s = String(value).trim().toLowerCase().replace(/_/g, '-');
  if (!s) return null;
  if (s === '*' || s === 'any' || s === 'all') return 'all';
  if (s === 'claude-code' || s === 'claude') return 'claude';
  if (s === 'codex-cli' || s === 'codex') return 'codex';
  if (s === 'gemini-cli' || s === 'gemini') return 'gemini';
  if (s === 'open-code' || s === 'opencode-cli' || s === 'opencode') return 'opencode';
  if (s === 'wall-e' || s === 'walle') return 'walle';
  return null;
}

function normalizeSkillAgents(value) {
  if (!value) return [];
  const raw = Array.isArray(value)
    ? value
    : typeof value === 'string'
      ? value.split(/[,\s]+/)
      : typeof value === 'object' && value.agents
        ? normalizeSkillAgents(value.agents)
        : [value];
  const out = [];
  const seen = new Set();
  for (const item of raw.flat ? raw.flat(Infinity) : raw) {
    const normalized = normalizeSkillAgentType(item);
    if (!normalized || seen.has(normalized)) continue;
    seen.add(normalized);
    out.push(normalized);
  }
  return out;
}

function skillCompatibleAgentsForSource(source) {
  const s = String(source || '').toLowerCase();
  if (s.startsWith('agents-')) return ['claude', 'codex', 'gemini', 'opencode', 'walle'];
  if (s.startsWith('codex-')) return ['codex', 'walle'];
  if (s.startsWith('claude-')) return ['claude', 'walle'];
  if (s.startsWith('opencode-')) return ['opencode', 'walle'];
  if (s.startsWith('gemini-')) return ['gemini', 'walle'];
  if (s === 'workspace' || s === 'user' || s === 'bundled' || s === 'registry' || s === 'project') return ['walle'];
  return [];
}

function skillCompatibleAgents(skill) {
  const declared = normalizeSkillAgents(skill?.agents || skill?.agent || skill?.compatibleAgents || skill?.compatible_agents);
  return declared.length ? declared : skillCompatibleAgentsForSource(skill?.source);
}

function isSkillCompatibleWithAgent(skill, agentType) {
  const agent = normalizeSkillAgentType(agentType);
  if (!agent || agent === 'all') return true;
  const agents = skillCompatibleAgents(skill);
  return agents.includes('all') || agents.includes(agent);
}

function skillSourcePriorityForAgent(source, agentType) {
  const sourceText = String(source || '').toLowerCase();
  const agent = normalizeSkillAgentType(agentType);
  if (!agent || agent === 'all') return 50;
  if (sourceText === `${agent}-project`) return 0;
  if (sourceText === `${agent}-user`) return 1;
  if (sourceText === 'agents-project') return 2;
  if (sourceText === 'agents-user') return 3;
  if (agent === 'walle') {
    if (sourceText === 'workspace') return 0;
    if (sourceText === 'project') return 1;
    if (sourceText === 'user') return 2;
    if (sourceText === 'bundled') return 3;
    if (sourceText === 'registry') return 4;
    if (/^(codex|claude|opencode|gemini)-/.test(sourceText)) return 5;
  }
  return 20;
}

function filterSkillsForAgent(skills, agentType) {
  const agent = normalizeSkillAgentType(agentType);
  const candidates = !agent || agent === 'all'
    ? [...safeArray(skills)]
    : safeArray(skills).filter(skill => isSkillCompatibleWithAgent(skill, agent));
  candidates.sort((a, b) => {
    const sourceDelta = skillSourcePriorityForAgent(a.source, agent) - skillSourcePriorityForAgent(b.source, agent);
    if (sourceDelta) return sourceDelta;
    return String(a.name || '').localeCompare(String(b.name || ''));
  });
  const byName = new Map();
  for (const skill of candidates) {
    if (skill?.name && !byName.has(skill.name)) byName.set(skill.name, skill);
  }
  return [...byName.values()].sort((a, b) => String(a.name).localeCompare(String(b.name)));
}

function safeArray(value) {
  return Array.isArray(value) ? value : [];
}

/**
 * Diagnostics: returns a snapshot of parse errors keyed by SKILL.md file path.
 * Each entry: { reason, line?, column? }. Useful for surfacing in /api/wall-e
 * status pages or tests.
 */
function getParseErrors() {
  return Object.fromEntries(_parseErrors);
}

// ── Project-Level JSON Skills (Phase 10, inspired by cmux CmuxConfig commands) ──

/**
 * Load skills from a .walle/skills.json file in a project directory.
 * Format: { "skills": [{ "name", "description", "command", "env", "cwd", "confirm", "tags", "timeout" }] }
 * Returns array of skill objects compatible with loadAllSkills() output.
 */
function loadProjectSkills(projectDir) {
  if (!projectDir) return [];
  const jsonPath = path.join(projectDir, '.walle', 'skills.json');
  if (!fs.existsSync(jsonPath)) return [];

  try {
    const raw = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
    const defs = Array.isArray(raw) ? raw : (raw.skills || []);
    return defs.filter(d => d.name && d.command).map(d => ({
      name: d.name,
      description: d.description || d.command,
      version: '0.0.0',
      execution: 'script',
      entry: null,
      args: [],
      author: null,
      requires: {},
      trigger: { type: 'manual' },
      config: {},
      tags: d.tags || [],
      permissions: [],
      agents: normalizeSkillAgents(d.agents || d.agent || 'walle'),
      instructions: `Run: ${d.command}`,
      dir: projectDir,
      filePath: jsonPath,
      source: 'project',
      // Extra fields for script execution
      _command: d.command,
      _env: d.env || {},
      _cwd: d.cwd ? path.resolve(projectDir, d.cwd) : projectDir,
      _confirm: d.confirm || false,
      _timeout: d.timeout || 300,
    }));
  } catch (e) {
    console.warn(`[skill-loader] Failed to load ${jsonPath}: ${e.message}`);
    return [];
  }
}

module.exports = {
  loadAllSkills,
  loadAllSkillEntries,
  loadSkill,
  findSkill,
  searchSkills,
  parseSkillMd,
  parseYaml,
  reloadSkills,
  loadProjectSkills,
  getParseErrors,
  BUNDLED_DIR,
  USER_DIR,
  REGISTRY_DIR,
  CODEX_HOME,
  CODEX_USER_DIR,
  OPENCODE_USER_DIR,
  CLAUDE_USER_DIR,
  GEMINI_USER_DIR,
  AGENTS_USER_DIR,
  projectSkillLocations,
  skillSearchLocations,
  normalizeSkillAgentType,
  normalizeSkillAgents,
  skillCompatibleAgents,
  isSkillCompatibleWithAgent,
  skillSourcePriorityForAgent,
  filterSkillsForAgent,
};
