'use strict';

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

const DEFAULT_MAX_SKILLS = 12;
const DEFAULT_MAX_MCP = 16;
const DEFAULT_MAX_LOADED_SKILLS = 3;
const DEFAULT_SKILL_INSTRUCTION_BYTES = 24000;
const BUILTIN_SLASH_COMMANDS = new Set(['review', 'fix', 'test', 'skill']);
const PREFIX_AGENT_HINTS = {
  '$': 'codex',
  '/': 'claude',
};

function parsePromptCapabilityHints(prompt = '') {
  const text = String(prompt || '');
  const skillNames = new Set();
  const slashSkillNames = new Set();
  const skillRequests = [];
  const mcpServers = new Set();
  const mcpTools = new Map();

  collectSkillHints(text, skillNames, slashSkillNames, skillRequests);
  collectEmbeddedSkillHints(text, skillNames, skillRequests);
  collectMcpHints(text, mcpServers, mcpTools);

  return {
    skills: [...skillNames],
    slashSkills: [...slashSkillNames],
    skillRequests: dedupeSkillRequests(skillRequests),
    mcpServers: [...mcpServers],
    mcpTools: [...mcpTools.values()],
  };
}

async function resolvePromptCapabilities({
  prompt = '',
  cwd = process.cwd(),
  agentType = 'walle',
  skillLoader = null,
  skillRunner = null,
  mcpClient = null,
  maxSkills = DEFAULT_MAX_SKILLS,
  maxMcpTools = DEFAULT_MAX_MCP,
} = {}) {
  const hints = parsePromptCapabilityHints(prompt);
  const skills = hints.skillRequests?.length
    ? await resolveSkillRequests(hints.skillRequests, { cwd, agentType, skillLoader, skillRunner, maxSkills })
    : await resolveLegacySkillHints(hints, { cwd, agentType, skillLoader, skillRunner, maxSkills });
  const mcp = await resolveMcpHints(hints, { mcpClient, maxMcpTools });
  return {
    hints,
    skills,
    mcp,
    hasExplicitCapabilities: skills.length > 0 || mcp.servers.length > 0 || mcp.tools.length > 0,
  };
}

function buildCapabilityContext(resolution = {}, opts = {}) {
  const skills = Array.isArray(resolution.skills) ? resolution.skills : [];
  const loadedSkills = Array.isArray(resolution.loadedSkills) ? resolution.loadedSkills : [];
  const mcp = resolution.mcp || {};
  const mcpServers = Array.isArray(mcp.servers) ? mcp.servers : [];
  const mcpTools = Array.isArray(mcp.tools) ? mcp.tools : [];
  if (!skills.length && !loadedSkills.length && !mcpServers.length && !mcpTools.length) return '';

  const sections = ['# Dynamic Capabilities'];
  sections.push('The user prompt explicitly referenced skills or MCP tools. Treat these as available capabilities for this run, subject to normal tool permissions.');

  if (skills.length) {
    const loadSkillTool = Object.prototype.hasOwnProperty.call(opts, 'loadSkillTool') ? opts.loadSkillTool : 'load_skill';
    const runSkillTool = opts.runSkillTool || 'skill';
    const namedSkillTool = Object.prototype.hasOwnProperty.call(opts, 'namedSkillTool') ? opts.namedSkillTool : 'skill__name';
    const instructionLine = loadSkillTool
      ? `Explicitly requested instruction skills may already be loaded below. Use \`${loadSkillTool}\` when you need more detail or a requested skill was not preloaded. Use \`${runSkillTool}\`${namedSkillTool ? `/\`${namedSkillTool}\`` : ''} only when you need to execute a Wall-E skill that performs work or fetches data. Do not recursively run \`coding-agent\` as a skill.`
      : `Explicitly requested instruction skills may already be loaded below. Apply loaded instructions directly. Use \`${runSkillTool}\` only when you need to execute a Wall-E skill that performs work or fetches data. Do not recursively run \`coding-agent\` as a skill.`;
    sections.push([
      '## Requested Skills',
      ...skills.map(formatSkillLine),
      '',
      instructionLine,
    ].join('\n'));
  }

  if (loadedSkills.length) {
    const lines = ['## Loaded Requested Skill Instructions'];
    for (const skill of loadedSkills) {
      lines.push(`### ${skill.name}`);
      if (skill.error) {
        lines.push(`Could not load: ${skill.error}`);
        continue;
      }
      const source = skill.source ? `Source: ${skill.source}` : '';
      const path = skill.path ? `Path: ${skill.path}` : '';
      if (source || path) lines.push([source, path].filter(Boolean).join(' | '));
      lines.push(skill.instructions || '');
      if (skill.truncated) lines.push('[Skill instructions truncated. Call `load_skill` for a larger excerpt if needed.]');
    }
    lines.push('', 'Apply these loaded skill instructions when satisfying this request.');
    sections.push(lines.join('\n\n'));
  }

  if (mcpServers.length || mcpTools.length) {
    const lines = ['## Requested MCP'];
    for (const server of mcpServers) lines.push(`- server \`${server}\``);
    for (const tool of mcpTools) {
      const label = tool.server ? `${tool.server}.${tool.tool}` : tool.tool;
      lines.push(`- tool \`${label}\``);
    }
    lines.push('', 'Use a named `mcp__server__tool` alias when it is exposed. Otherwise call `mcp_call` with `{ server, tool, arguments }`. Prefer Wall-E MCP memory tools for private/session/work context before public web or guesswork.');
    sections.push(lines.join('\n'));
  }

  return sections.filter(Boolean).join('\n\n');
}

async function loadRequestedSkillInstructions(resolution = {}, skillRunner = null, {
  maxSkills = DEFAULT_MAX_LOADED_SKILLS,
  maxBytes = DEFAULT_SKILL_INSTRUCTION_BYTES,
} = {}) {
  const skills = Array.isArray(resolution.skills) ? resolution.skills : [];
  const canLoadByPath = skills.some((skill) => skill?.path);
  const canLoadByRunner = skillRunner && typeof skillRunner.loadSkill === 'function';
  if (!canLoadByPath && !canLoadByRunner) return resolution;
  const loadedSkills = [];
  for (const skill of skills) {
    if (loadedSkills.length >= maxSkills) break;
    if (!skill?.available || !skill.name) continue;
    try {
      const body = skill.path
        ? loadSkillInstructionFromPath(skill, maxBytes)
        : (await skillRunner.loadSkill(skill.name, {
          max_bytes: maxBytes,
          source: skill.source || undefined,
          path: skill.path || undefined,
          agentType: skill.agentType || undefined,
          prefix: skill.prefix || undefined,
        }))?.skill || {};
      loadedSkills.push({
        name: body.name || skill.name,
        source: body.source || skill.source || '',
        path: body.path || skill.path || '',
        instructions: body.instructions || '',
        truncated: Boolean(body.truncated),
      });
    } catch (err) {
      loadedSkills.push({
        name: skill.name,
        source: skill.source || '',
        path: skill.path || '',
        instructions: '',
        truncated: false,
        error: err.message,
      });
    }
  }
  return {
    ...resolution,
    loadedSkills,
  };
}

function collectSkillHints(text, skillNames, slashSkillNames = null, skillRequests = null) {
  const dollarRe = /(^|[^\w`])\$([A-Za-z0-9][A-Za-z0-9_.:-]{1,80})\b/g;
  let match;
  while ((match = dollarRe.exec(text))) {
    const name = normalizeSkillName(match[2]);
    if (isLikelySkillName(name)) {
      skillNames.add(name);
      addSkillRequest(skillRequests, name, '$');
    }
  }

  const taggedRe = /(?:^|[^\w`])@skill\/([A-Za-z0-9][A-Za-z0-9_.:-]{1,80})\b/g;
  while ((match = taggedRe.exec(text))) {
    const name = normalizeSkillName(match[1]);
    if (isLikelySkillName(name)) {
      skillNames.add(name);
      addSkillRequest(skillRequests, name, '@skill');
    }
  }

  const namedRe = /(?:^|[\s([{])skill:([A-Za-z0-9][A-Za-z0-9_.:-]{1,80})\b/gi;
  while ((match = namedRe.exec(text))) {
    const name = normalizeSkillName(match[1]);
    if (isLikelySkillName(name)) {
      skillNames.add(name);
      addSkillRequest(skillRequests, name, 'skill:');
    }
  }

  if (!slashSkillNames) return;
  const slashRe = /(^|[\s([{])\/([A-Za-z0-9][A-Za-z0-9_.:-]{1,80})\b(?!\/)/g;
  while ((match = slashRe.exec(text))) {
    const name = normalizeSkillName(match[2]);
    if (isLikelySkillName(name) && !BUILTIN_SLASH_COMMANDS.has(name)) {
      slashSkillNames.add(name);
      addSkillRequest(skillRequests, name, '/');
    }
  }
}

function collectEmbeddedSkillHints(text, skillNames, skillRequests = null) {
  const nameRe = /<skill>[\s\S]*?<name>\s*([^<\s]+)\s*<\/name>[\s\S]*?<\/skill>/gi;
  let match;
  while ((match = nameRe.exec(text))) {
    const name = normalizeSkillName(match[1]);
    if (name) {
      skillNames.add(name);
      addSkillRequest(skillRequests, name, '<skill>');
    }
  }
}

function collectMcpHints(text, servers, tools) {
  const namedAliasRe = /\bmcp__([A-Za-z0-9_-]+)__([A-Za-z0-9_/-]+)\b/g;
  let match;
  while ((match = namedAliasRe.exec(text))) {
    const server = normalizeMcpName(match[1]);
    const tool = normalizeMcpName(match[2]);
    if (server) servers.add(server);
    if (tool) tools.set(`${server}.${tool}`, { server, tool });
  }

  const explicitRe = /\bmcp[:/]([A-Za-z0-9_-]+)(?:[.:/]([A-Za-z0-9_/-]+))?\b/gi;
  while ((match = explicitRe.exec(text))) {
    const server = normalizeMcpName(match[1]);
    const tool = normalizeMcpName(match[2] || '');
    if (server) servers.add(server);
    if (server && tool) tools.set(`${server}.${tool}`, { server, tool });
  }

  const namedServerRe = /\b(?:use|with|from|via|call|query)\s+([A-Za-z0-9_-]+)\s+MCP\b/gi;
  while ((match = namedServerRe.exec(text))) {
    const server = normalizeMcpName(match[1]);
    if (server && server !== 'the') servers.add(server);
  }

  if (/\bwall[- ]?e\s+MCP\b/i.test(text)) servers.add('wall-e');
}

async function resolveLegacySkillHints(hints, { cwd, agentType, skillLoader, skillRunner, maxSkills }) {
  const explicitSkills = await resolveSkillHints(hints.skills, { cwd, agentType, skillLoader, skillRunner, maxSkills });
  const remainingSkillSlots = Math.max(0, maxSkills - explicitSkills.length);
  const slashSkills = remainingSkillSlots > 0
    ? await resolveSkillHints(hints.slashSkills || [], {
      cwd,
      agentType,
      skillLoader,
      skillRunner,
      maxSkills: remainingSkillSlots,
      includeMissing: false,
    })
    : [];
  return dedupeSkillResolutions([...explicitSkills, ...slashSkills]).slice(0, maxSkills);
}

async function resolveSkillRequests(requests, { cwd, agentType, skillLoader, skillRunner, maxSkills }) {
  const loader = skillLoader || safeRequire('../skills/skill-loader');
  const listCache = new Map();
  const out = [];
  for (const request of dedupeSkillRequests(requests).slice(0, maxSkills)) {
    const preferredAgent = request.agentType || agentType;
    if (!listCache.has(preferredAgent)) {
      listCache.set(preferredAgent, await listAvailableSkills({ cwd, agentType: preferredAgent, loader, skillRunner }));
    }
    const listed = listCache.get(preferredAgent);
    const found = findSkillForRequest(request, listed);
    out.push(publicSkillResolution(request.name, found, request));
  }
  return dedupeSkillResolutions(out).slice(0, maxSkills);
}

async function resolveSkillHints(names, { cwd, agentType, skillLoader, skillRunner, maxSkills, includeMissing = true }) {
  const loader = skillLoader || safeRequire('../skills/skill-loader');
  const listed = await listAvailableSkills({ cwd, agentType, loader, skillRunner });
  const byName = new Map(listed.map(skill => [skill.name, skill]));
  const out = [];
  for (const name of names.slice(0, maxSkills)) {
    const found = byName.get(name) || findSimilarSkill(name, listed);
    if (!found && !includeMissing) continue;
    out.push(publicSkillResolution(name, found));
  }
  return out;
}

async function listAvailableSkills({ cwd, agentType, loader, skillRunner }) {
  const byName = new Map();
  if (loader?.loadAllSkillEntries || loader?.loadAllSkills) {
    const raw = loader.loadAllSkillEntries
      ? loader.loadAllSkillEntries(cwd)
      : loader.loadAllSkills(cwd);
    const skills = loader?.filterSkillsForAgent
      ? loader.filterSkillsForAgent(raw, agentType)
      : raw;
    for (const skill of safeArray(skills)) {
      if (skill?.name && !byName.has(skill.name)) byName.set(skill.name, skill);
    }
  }
  if (loader?.loadProjectSkills) {
    const raw = loader.loadProjectSkills(cwd);
    const skills = loader?.filterSkillsForAgent
      ? loader.filterSkillsForAgent(raw, agentType)
      : raw;
    for (const skill of safeArray(skills)) {
      if (skill?.name && !byName.has(skill.name)) byName.set(skill.name, skill);
    }
  }
  try {
    const raw = typeof skillRunner?.listSkills === 'function'
      ? await skillRunner.listSkills()
      : typeof skillRunner?.getSkills === 'function'
        ? await skillRunner.getSkills()
        : [];
    for (const skill of safeArray(raw)) {
      const compatible = isSkillRunnerSkillCompatible(skill, loader, agentType);
      if (compatible && skill?.name && !byName.has(skill.name)) byName.set(skill.name, skill);
    }
  } catch {}
  return [...byName.values()].sort((a, b) => String(a.name).localeCompare(String(b.name)));
}

function findSkillForRequest(request, skills) {
  const exact = safeArray(skills).filter(skill => skill?.name === request.name);
  if (!exact.length) return findSimilarSkill(request.name, skills);
  if (request.sourceHint) {
    const sourceMatched = exact.find(skill => sourceMatchesHint(skill.source, request.sourceHint));
    if (sourceMatched) return sourceMatched;
  }
  return exact[0];
}

function sourceMatchesHint(source, hint) {
  const sourceText = String(source || '').toLowerCase();
  const hintText = String(hint || '').toLowerCase();
  return sourceText === hintText || sourceText.startsWith(`${hintText}-`);
}

function isSkillRunnerSkillCompatible(skill, loader, agentType) {
  if (!loader?.isSkillCompatibleWithAgent) return true;
  if (!hasSkillCompatibilityMetadata(skill)) return true;
  return loader.isSkillCompatibleWithAgent(skill, agentType);
}

function hasSkillCompatibilityMetadata(skill = {}) {
  return Boolean(
    skill.source ||
    skill.agents ||
    skill.agent ||
    skill.compatibleAgents ||
    skill.compatible_agents
  );
}

async function resolveMcpHints(hints, { mcpClient, maxMcpTools }) {
  const requestedServers = new Set(hints.mcpServers || []);
  const requestedTools = [...(hints.mcpTools || [])];
  const tools = [];
  if (mcpClient && typeof mcpClient.listTools === 'function' && (requestedServers.size || requestedTools.length)) {
    try {
      const listed = normalizeMcpTools(await mcpClient.listTools());
      for (const tool of listed) {
        if (tools.length >= maxMcpTools) break;
        const serverMatch = requestedServers.has(tool.server);
        const toolMatch = requestedTools.some(req => req.server === tool.server && req.tool === tool.name);
        if (serverMatch || toolMatch) {
          tools.push({
            server: tool.server,
            tool: tool.name,
            description: tool.description || '',
          });
        }
      }
    } catch {}
  }
  return {
    servers: [...requestedServers],
    tools: dedupeMcpTools([...requestedTools, ...tools]).slice(0, maxMcpTools),
  };
}

function normalizeMcpTools(raw) {
  if (!raw) return [];
  if (Array.isArray(raw)) {
    return raw.flatMap(item => normalizeMcpTools(item));
  }
  if (raw.tools && Array.isArray(raw.tools)) {
    const server = raw.server || raw.serverName || raw.name || '';
    return raw.tools.flatMap(tool => normalizeMcpTools({ ...tool, server: tool.server || server }));
  }
  if (raw.result?.tools) return normalizeMcpTools(raw.result.tools);
  if (typeof raw === 'object' && raw.name) {
    return [{
      server: normalizeMcpName(raw.server || raw.serverName || ''),
      name: normalizeMcpName(raw.name || raw.tool || ''),
      description: raw.description || '',
    }].filter(item => item.name);
  }
  return [];
}

function publicSkillResolution(requestedName, skill, request = {}) {
  if (!skill) {
    return {
      name: requestedName,
      available: false,
      description: '',
      source: '',
      path: '',
      prefix: request.prefix || '',
      agentType: request.agentType || '',
      sourceHint: request.sourceHint || '',
    };
  }
  return {
    name: skill.name,
    requestedName,
    available: true,
    description: skill.description || '',
    source: skill.source || '',
    path: skill.filePath || skill.path || '',
    execution: skill.execution || '',
    agents: Array.isArray(skill.agents) ? skill.agents : [],
    prefix: request.prefix || '',
    agentType: request.agentType || '',
    sourceHint: request.sourceHint || '',
  };
}

function formatSkillLine(skill) {
  const status = skill.available ? 'available' : 'not found';
  const source = skill.source ? `, source: ${skill.source}` : '';
  const prefix = skill.prefix ? `, requested with ${skill.prefix}${skill.agentType ? ` (${skill.agentType})` : ''}` : '';
  const desc = skill.description ? ` — ${oneLine(skill.description, 180)}` : '';
  const requested = skill.requestedName && skill.requestedName !== skill.name ? ` (requested as ${skill.requestedName})` : '';
  return `- \`${skill.name}\`${requested}: ${status}${source}${prefix}${desc}`;
}

function findSimilarSkill(name, skills) {
  const normalized = normalizeSkillName(name);
  return skills.find(skill => normalizeSkillName(skill.name) === normalized) || null;
}

function dedupeMcpTools(tools) {
  const seen = new Set();
  const out = [];
  for (const tool of tools || []) {
    if (!tool?.tool) continue;
    const key = `${tool.server || ''}.${tool.tool}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(tool);
  }
  return out;
}

function dedupeSkillResolutions(skills) {
  const seen = new Set();
  const out = [];
  for (const skill of skills || []) {
    const name = normalizeSkillName(skill?.name || skill?.requestedName || '');
    if (!name) continue;
    const key = `${skill?.prefix || ''}:${skill?.agentType || ''}:${name}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push(skill);
  }
  return out;
}

function normalizeSkillName(value) {
  return String(value || '').trim().replace(/^skill:/i, '').replace(/^@skill\//i, '');
}

function addSkillRequest(out, name, prefix) {
  if (!out || !name) return;
  const agentType = PREFIX_AGENT_HINTS[prefix] || '';
  out.push({
    name,
    prefix,
    agentType,
    sourceHint: agentType,
  });
}

function dedupeSkillRequests(requests) {
  const seen = new Set();
  const out = [];
  for (const request of safeArray(requests)) {
    const name = normalizeSkillName(request?.name);
    if (!name) continue;
    const prefix = request.prefix || '';
    const agentType = request.agentType || PREFIX_AGENT_HINTS[prefix] || '';
    const key = `${prefix}:${agentType}:${name}`;
    if (seen.has(key)) continue;
    seen.add(key);
    out.push({
      name,
      prefix,
      agentType,
      sourceHint: request.sourceHint || agentType,
    });
  }
  return out;
}

function loadSkillInstructionFromPath(skill, maxBytes) {
  const filePath = String(skill.path || '');
  if (!filePath) throw new Error(`Skill "${skill.name}" has no file path`);
  const raw = fs.readFileSync(filePath, 'utf8');
  let parsed = null;
  try {
    const loader = safeRequire('../skills/skill-loader');
    parsed = loader?.parseSkillMd?.(raw) || null;
  } catch {}
  const instructions = parsed?.body || stripLooseFrontmatter(raw);
  return {
    name: parsed?.meta?.name || skill.name,
    source: skill.source || '',
    path: filePath,
    instructions: oneLinePreservingBlocks(instructions, maxBytes),
    truncated: String(instructions || '').length > maxBytes,
  };
}

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).trim();
}

function oneLinePreservingBlocks(value, max) {
  const text = String(value || '');
  return text.length > max ? `${text.slice(0, max)}...[truncated]` : text;
}

function normalizeMcpName(value) {
  return String(value || '').trim().replace(/_/g, '-').replace(/^-+|-+$/g, '');
}

function isLikelySkillName(name) {
  if (!name || name.length < 2) return false;
  if (/^[A-Z0-9_]+$/.test(name)) return false; // shell/env vars such as $DEV_CTM_PORT
  if (/^\d+$/.test(name)) return false;
  return /[a-z]/.test(name);
}

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

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

function safeRequire(id) {
  try {
    return require(id);
  } catch {
    return null;
  }
}

module.exports = {
  parsePromptCapabilityHints,
  resolvePromptCapabilities,
  buildCapabilityContext,
  loadRequestedSkillInstructions,
  normalizeMcpTools,
  resolveSkillRequests,
};
