'use strict';

const fs = require('node:fs');
const path = require('node:path');

const REPO_ROOT = path.resolve(__dirname, '..', '..');
const BUNDLED_DIR = path.join(REPO_ROOT, 'wall-e', 'skills', '_bundled');

let cachedEntries = new Map();

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 loadAllSkillEntries(workspaceDir) {
  const cacheKey = workspaceDir ? path.resolve(workspaceDir) : '';
  if (cachedEntries.has(cacheKey)) return cachedEntries.get(cacheKey);

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

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

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 skillSearchLocations(workspaceDir) {
  const home = process.env.HOME || process.env.USERPROFILE || '';
  const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
  return [
    ...projectSkillLocations(workspaceDir),
    { dir: path.join(home, '.wall-e', 'skills'), source: 'user' },
    { dir: path.join(codexHome, 'skills'), source: 'codex-user' },
    { dir: path.join(home, '.config', 'opencode', 'skills'), source: 'opencode-user' },
    { dir: path.join(home, '.claude', 'skills'), source: 'claude-user' },
    { dir: path.join(home, '.gemini', 'skills'), source: 'gemini-user' },
    { dir: path.join(home, '.agents', 'skills'), source: 'agents-user' },
    { dir: BUNDLED_DIR, source: 'bundled' },
    { dir: path.join(home, '.wall-e', 'registry-skills'), source: 'registry' },
  ];
}

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;
  }
}

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

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

function loadSkill(skillDir, source) {
  const skillMdPath = path.join(skillDir, 'SKILL.md');
  let content;
  try {
    content = fs.readFileSync(skillMdPath, 'utf8');
  } catch {
    return null;
  }

  const parsed = parseSkillMd(content);
  const name = normalizeSkillName(parsed.meta.name || path.basename(skillDir));
  if (!name) return null;
  const declaredAgents = normalizeSkillAgents(
    parsed.meta.agents ||
    parsed.meta.agent ||
    parsed.meta.compatible_agents ||
    parsed.meta.compatibleAgents
  );
  return {
    name,
    description: oneLine(parsed.meta.description || extractMarkdownDescription(parsed.body)),
    execution: parsed.meta.execution || '',
    source,
    agents: declaredAgents.length ? declaredAgents : skillCompatibleAgentsForSource(source),
    capabilities: normalizeSkillCapabilities(
      parsed.meta.capabilities ||
      parsed.meta.capability ||
      parsed.meta.skills_capabilities ||
      parsed.meta.skill_capabilities
    ),
    intents: normalizeSkillCapabilities(parsed.meta.intents || parsed.meta.intent),
    invocation: normalizeSkillInvocation(parsed.meta.invocation || parsed.meta.command_prefix || parsed.meta.prefix),
    filePath: skillMdPath,
    path: skillMdPath,
  };
}

function parseSkillMd(content) {
  const text = String(content || '');
  const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
  if (!match) return { meta: {}, body: text };
  return {
    meta: parseFrontmatter(match[1]),
    body: match[2] || '',
  };
}

function parseFrontmatter(text) {
  const meta = {};
  const lines = String(text || '').split(/\r?\n/);
  for (let i = 0; i < lines.length; i += 1) {
    const raw = lines[i];
    const line = raw.trim();
    if (!line || line.startsWith('#')) continue;
    const match = line.match(/^([A-Za-z0-9_.-]+):\s*(.*)$/);
    if (!match) continue;
    const valueText = String(match[2] || '').trim();
    if (!valueText) {
      const list = [];
      let j = i + 1;
      for (; j < lines.length; j += 1) {
        const next = lines[j];
        if (!next.trim()) continue;
        const itemMatch = next.match(/^\s*-\s*(.+)$/);
        if (!itemMatch) break;
        list.push(parseFrontmatterValue(itemMatch[1]));
      }
      if (list.length) {
        meta[match[1]] = list.flat ? list.flat(Infinity) : list;
        i = j - 1;
        continue;
      }
    }
    const blockStyle = valueText.match(/^([>|])[-+]?$/);
    if (blockStyle) {
      const block = [];
      let j = i + 1;
      for (; j < lines.length; j += 1) {
        if (!/^\s+/.test(lines[j]) && lines[j].trim()) break;
        const value = lines[j].replace(/^\s+/, '');
        if (value) block.push(value);
      }
      meta[match[1]] = blockStyle[1] === '>' ? block.join(' ') : block.join('\n');
      i = j - 1;
    } else {
      meta[match[1]] = parseFrontmatterValue(match[2]);
    }
  }
  return meta;
}

function parseFrontmatterValue(value) {
  const text = String(value || '').trim();
  if (!text) return '';
  if (text.startsWith('[') && text.endsWith(']')) {
    return text.slice(1, -1).split(',').map((item) => stripQuotes(item.trim())).filter(Boolean);
  }
  return stripQuotes(text);
}

function stripQuotes(value) {
  return String(value || '').replace(/^['"]|['"]$/g, '');
}

function normalizeSkillAgents(value) {
  if (!value) return [];
  const raw = Array.isArray(value)
    ? value
    : typeof value === 'string'
      ? value.split(/[,\s]+/)
      : [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 normalizeSkillCapabilities(value) {
  if (!value) return [];
  const raw = Array.isArray(value)
    ? value
    : typeof value === 'string'
      ? value.split(/[,\s]+/)
      : [value];
  const out = [];
  const seen = new Set();
  for (const item of raw.flat ? raw.flat(Infinity) : raw) {
    const normalized = String(item || '')
      .trim()
      .toLowerCase()
      .replace(/_/g, '.')
      .replace(/:+/g, '.')
      .replace(/[^a-z0-9.-]+/g, '.')
      .replace(/^\.+|\.+$/g, '');
    if (!normalized || seen.has(normalized)) continue;
    seen.add(normalized);
    out.push(normalized);
  }
  return out;
}

function normalizeSkillInvocation(value) {
  const text = String(value || '').trim();
  if (text === '$' || text === '/') return text;
  if (/^dollar$/i.test(text)) return '$';
  if (/^slash$/i.test(text)) return '/';
  return '';
}

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') 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 === 'user') return 1;
    if (sourceText === 'bundled') return 2;
    if (sourceText === 'registry') return 3;
    if (/^(codex|claude|opencode|gemini)-/.test(sourceText)) return 4;
  }
  return 20;
}

function normalizeSkillAutocompleteQuery(value) {
  const text = String(value || '').trim().replace(/^[$/]+/, '').toLowerCase();
  return text.replace(/[^a-z0-9_.:-]/g, '');
}

function skillAutocompleteMatch(name, query) {
  const q = normalizeSkillAutocompleteQuery(query);
  const n = normalizeSkillAutocompleteQuery(name);
  if (!n) return null;
  if (!q) return {
    tier: 100,
    index: 0,
    gap: 0,
    lengthDelta: n.length,
  };

  const qCompact = q.replace(/[_.:-]+/g, '');
  const nCompact = n.replace(/[_.:-]+/g, '');

  if (n === q) return { tier: 0, index: 0, gap: 0, lengthDelta: 0 };
  if (n.startsWith(q)) return { tier: 1, index: 0, gap: 0, lengthDelta: n.length - q.length };

  const segments = n.split(/[_.:-]+/).filter(Boolean);
  let bestSegmentDelta = Infinity;
  for (const segment of segments) {
    if (segment.startsWith(q)) bestSegmentDelta = Math.min(bestSegmentDelta, segment.length - q.length);
  }
  if (Number.isFinite(bestSegmentDelta)) {
    return { tier: 2, index: 0, gap: 0, lengthDelta: bestSegmentDelta };
  }

  if (qCompact && nCompact === qCompact) return { tier: 3, index: 0, gap: 0, lengthDelta: 0 };
  if (qCompact && nCompact.startsWith(qCompact)) {
    return { tier: 4, index: 0, gap: 0, lengthDelta: nCompact.length - qCompact.length };
  }

  const index = n.indexOf(q);
  if (index >= 0) {
    return { tier: 5, index, gap: 0, lengthDelta: n.length - q.length };
  }

  const fuzzy = fuzzySubsequenceMatch(n, q);
  if (fuzzy) {
    return {
      tier: 8,
      index: fuzzy.firstIndex,
      gap: fuzzy.gap,
      lengthDelta: n.length - q.length,
    };
  }

  return null;
}

function fuzzySubsequenceMatch(name, query) {
  if (!query) return { firstIndex: 0, gap: 0 };
  let qi = 0;
  let firstIndex = -1;
  let previousIndex = -1;
  let gap = 0;
  for (let i = 0; i < name.length && qi < query.length; i += 1) {
    if (name[i] !== query[qi]) continue;
    if (firstIndex < 0) firstIndex = i;
    if (previousIndex >= 0) gap += i - previousIndex - 1;
    previousIndex = i;
    qi += 1;
  }
  return qi === query.length ? { firstIndex: Math.max(0, firstIndex), gap } : null;
}

function filterAndSortSkillAutocompleteItems(items, query, agentType) {
  const q = normalizeSkillAutocompleteQuery(query);
  const agent = normalizeSkillAgentType(agentType);
  const matches = [];
  for (const item of safeArray(items)) {
    const match = skillAutocompleteMatch(item?.name, q);
    if (!match) continue;
    matches.push({ item, match });
  }

  matches.sort((a, b) => compareSkillAutocompleteMatches(a, b, q, agent));
  return matches.map((entry) => entry.item);
}

function compareSkillAutocompleteMatches(a, b, query, agentType) {
  const aMatch = a?.match || {};
  const bMatch = b?.match || {};
  if (query) {
    if (aMatch.tier !== bMatch.tier) return aMatch.tier - bMatch.tier;
    if (aMatch.index !== bMatch.index) return aMatch.index - bMatch.index;
    if (aMatch.gap !== bMatch.gap) return aMatch.gap - bMatch.gap;
    if (aMatch.lengthDelta !== bMatch.lengthDelta) return aMatch.lengthDelta - bMatch.lengthDelta;
    const nameLengthDelta = normalizeSkillAutocompleteQuery(a?.item?.name).length -
      normalizeSkillAutocompleteQuery(b?.item?.name).length;
    if (nameLengthDelta) return nameLengthDelta;
  } else {
    const aFreq = Number(a?.item?.frequency || 0);
    const bFreq = Number(b?.item?.frequency || 0);
    if (bFreq !== aFreq) return bFreq - aFreq;
  }

  const sourceDelta = skillSourcePriorityForAgent(a?.item?.source, agentType) -
    skillSourcePriorityForAgent(b?.item?.source, agentType);
  if (sourceDelta) return sourceDelta;

  if (query) {
    const aFreq = Number(a?.item?.frequency || 0);
    const bFreq = Number(b?.item?.frequency || 0);
    if (bFreq !== aFreq) return bFreq - aFreq;
  }

  return String(a?.item?.name || '').localeCompare(String(b?.item?.name || ''), undefined, {
    numeric: true,
    sensitivity: 'base',
  });
}

function normalizeSkillName(value) {
  const name = String(value || '').trim().replace(/^@skill\//, '').replace(/\s+/g, '-');
  return /^[A-Za-z0-9_.:-]{1,80}$/.test(name) ? name : '';
}

function extractMarkdownDescription(content) {
  for (const raw of String(content || '').split(/\r?\n/)) {
    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;
}

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

function reloadSkills() {
  cachedEntries = new Map();
}

module.exports = {
  filterAndSortSkillAutocompleteItems,
  filterSkillsForAgent,
  isSkillCompatibleWithAgent,
  loadAllSkillEntries,
  normalizeSkillAgentType,
  normalizeSkillAutocompleteQuery,
  normalizeSkillCapabilities,
  reloadSkills,
  skillAutocompleteMatch,
  skillSourcePriorityForAgent,
};
