'use strict';

const path = require('node:path');
const { spawn } = require('node:child_process');

const DEFAULT_CACHE_TTL_MS = 30000;
const DEFAULT_BLOCKED_SKILLS = new Set(['coding-agent']);
const MAX_SKILL_OUTPUT_BYTES = 100 * 1024;
const MAX_SKILL_INSTRUCTION_BYTES = 60 * 1024;

function createCodingMcpClient({
  mcpModule = null,
  cacheTtlMs = DEFAULT_CACHE_TTL_MS,
  namedServers = ['wall-e'],
  includeAllServers = false,
  logger = console,
} = {}) {
  const moduleRef = mcpModule || require('../skills/mcp-client');
  let cachedTools = null;
  let cachedAt = 0;

  async function listTools() {
    const now = Date.now();
    if (cachedTools && now - cachedAt < cacheTtlMs) return cachedTools.slice();
    try {
      let raw = [];
      if (includeAllServers && typeof moduleRef.listAllTools === 'function') {
        raw = await moduleRef.listAllTools();
      } else if (typeof moduleRef.getConnection === 'function') {
        raw = await listNamedServerTools(moduleRef, namedServers);
      } else if (typeof moduleRef.listAllTools === 'function') {
        raw = await moduleRef.listAllTools();
      }
      cachedTools = Array.isArray(raw) ? raw : [];
      cachedAt = now;
      return cachedTools.slice();
    } catch (err) {
      if (logger?.warn) logger.warn(`[coding-capabilities] MCP tool discovery failed: ${err.message}`);
      cachedTools = [];
      cachedAt = now;
      return [];
    }
  }

  async function callTool(server, tool, args) {
    let serverName = server;
    let toolName = tool;
    let toolArgs = args;

    // Support the lightweight callTool(tool, args) shape used by older context
    // builders by resolving unique tool names back to their server.
    if (typeof tool === 'object' && args === undefined) {
      toolArgs = tool;
      toolName = server;
      serverName = '';
    }

    if (!serverName) {
      const tools = await listTools();
      const matches = tools.filter((item) => item.name === toolName);
      if (matches.length === 1) serverName = matches[0].server || '';
    }

    if (!serverName) throw new Error(`MCP server is required for tool "${toolName}"`);
    if (!toolName) throw new Error('MCP tool name is required');
    if (typeof moduleRef.callMcpTool !== 'function') {
      throw new Error('MCP module does not expose callMcpTool');
    }
    return moduleRef.callMcpTool(serverName, toolName, toolArgs || {});
  }

  return { listTools, callTool };
}

async function listNamedServerTools(moduleRef, namedServers = []) {
  const out = [];
  for (const server of namedServers || []) {
    try {
      const conn = await moduleRef.getConnection(server);
      const tools = await conn.listTools();
      for (const tool of tools || []) out.push({ ...tool, server });
    } catch {
      // Missing or unauthenticated optional servers should not remove the
      // generic mcp_call bridge from the coding agent.
    }
  }
  return out;
}

function createCodingSkillRunner({
  projectRoot = process.cwd(),
  brain = null,
  blockedSkills = DEFAULT_BLOCKED_SKILLS,
  skillLoader = null,
  scriptSkillRunner = null,
  skillExecutor = null,
  cacheTtlMs = DEFAULT_CACHE_TTL_MS,
} = {}) {
  const loader = skillLoader || require('../skills/skill-loader');
  const scriptRunner = scriptSkillRunner || require('../skills/script-skill-runner');
  const executor = skillExecutor || require('../skills/skill-executor');
  const blocked = blockedSkills instanceof Set ? blockedSkills : new Set(blockedSkills || []);
  let cachedSkills = null;
  let cachedAt = 0;

  async function listSkills() {
    const now = Date.now();
    if (cachedSkills && now - cachedAt < cacheTtlMs) return cachedSkills.map(publicSkillDefinition);
    const byName = new Map();
    for (const skill of loadFilesystemSkills(loader, projectRoot)) {
      if (!blocked.has(skill.name)) byName.set(skill.name, skill);
    }
    if (brain && typeof brain.listSkills === 'function') {
      for (const skill of brain.listSkills({ enabled: 1 }) || []) {
        if (skill?.name && !blocked.has(skill.name) && !byName.has(skill.name)) {
          byName.set(skill.name, { ...skill, source: 'brain' });
        }
      }
    }
    cachedSkills = [...byName.values()].sort((a, b) => String(a.name).localeCompare(String(b.name)));
    cachedAt = now;
    return cachedSkills.map(publicSkillDefinition);
  }

  async function run(name, args = {}, ctx = {}) {
    if (!name) throw new Error('Skill name is required');
    if (blocked.has(name)) throw new Error(`Skill "${name}" is blocked from recursive coding-agent execution`);

    const skill = findSkill(loader, projectRoot, brain, name);
    if (!skill) throw new Error(`Skill "${name}" not found`);

    if (skill.source === 'project' && skill._command) {
      return runProjectSkillCommand(skill, args, { projectRoot, ctx });
    }

    if (skill.execution === 'script') {
      const output = await scriptRunner.runScriptSkill(skill, {
        id: ctx.sessionId || `coding-${Date.now()}`,
        skill: name,
        title: name,
        skill_config: args || {},
      }, {
        cwd: projectRoot,
        log: () => {},
      });
      return { success: true, skill: name, output: truncate(output) };
    }

    if (typeof executor.runSkill !== 'function') {
      throw new Error('Skill executor does not expose runSkill');
    }
    const result = await executor.runSkill(skill, {
      dispatch_source: 'coding-agent',
      decision_reason: 'tool_call',
      arguments: args || {},
    });
    return { success: true, skill: name, result };
  }

  async function execute(call = {}, ctx = {}) {
    return run(call.name, call.arguments || {}, ctx);
  }

  async function loadSkill(name, args = {}) {
    if (!name) throw new Error('Skill name is required');
    const skill = findSkill(loader, projectRoot, brain, name);
    if (!skill) throw new Error(`Skill "${name}" not found`);
    return {
      success: true,
      skill: publicSkillInstruction(skill, args),
    };
  }

  return {
    listSkills,
    getSkills: listSkills,
    loadSkill,
    run,
    execute,
  };
}

function createCodingCapabilities({
  cwd = process.cwd(),
  brain = null,
  mcpClient,
  skillRunner,
  mcpNamedServers = ['wall-e'],
  mcpIncludeAllServers = false,
  enableMcpTools = true,
  enableSkillTools = true,
  logger = console,
} = {}) {
  const namedServers = normalizeNamedServers(mcpNamedServers);
  return {
    mcpClient: mcpClient !== undefined
      ? mcpClient
      : (enableMcpTools ? createCodingMcpClient({ logger, namedServers, includeAllServers: mcpIncludeAllServers }) : null),
    skillRunner: skillRunner !== undefined
      ? skillRunner
      : (enableSkillTools ? createCodingSkillRunner({ projectRoot: cwd, brain }) : null),
  };
}

function normalizeNamedServers(value) {
  const input = Array.isArray(value) ? value : [value];
  const seen = new Set();
  const out = [];
  for (const item of ['wall-e', ...input]) {
    const name = String(item || '').trim();
    if (!name || seen.has(name)) continue;
    seen.add(name);
    out.push(name);
  }
  return out;
}

function loadFilesystemSkills(loader, projectRoot) {
  const byName = new Map();
  for (const skill of safeArray(loader.loadAllSkills?.(projectRoot))) {
    if (skill?.name) byName.set(skill.name, skill);
  }
  for (const skill of safeArray(loader.loadProjectSkills?.(projectRoot))) {
    if (skill?.name) byName.set(skill.name, skill);
  }
  return [...byName.values()];
}

function findSkill(loader, projectRoot, brain, name) {
  const filesystem = loadFilesystemSkills(loader, projectRoot).find((skill) => skill.name === name);
  if (filesystem) return filesystem;
  if (brain && typeof brain.getSkillByName === 'function') {
    const dbSkill = brain.getSkillByName(name);
    if (dbSkill) return { ...dbSkill, source: 'brain' };
  }
  return null;
}

function publicSkillDefinition(skill = {}) {
  return {
    name: skill.name,
    description: skill.description || skill.instructions?.slice?.(0, 160) || '',
    input_schema: skillInputSchema(skill),
  };
}

function publicSkillInstruction(skill = {}, args = {}) {
  const requestedMax = Number(args.max_bytes || args.maxBytes || MAX_SKILL_INSTRUCTION_BYTES);
  const maxBytes = Math.max(1024, Math.min(
    Number.isFinite(requestedMax) ? requestedMax : MAX_SKILL_INSTRUCTION_BYTES,
    MAX_SKILL_INSTRUCTION_BYTES
  ));
  const instructions = skill.instructions || skill.prompt_template || '';
  return {
    name: skill.name,
    description: skill.description || '',
    source: skill.source || '',
    execution: skill.execution || '',
    path: skill.filePath || skill.path || '',
    tags: Array.isArray(skill.tags) ? skill.tags : [],
    instructions: truncate(instructions, maxBytes),
    truncated: String(instructions || '').length > maxBytes,
  };
}

function skillInputSchema(skill = {}) {
  if (skill.input_schema) return skill.input_schema;
  if (skill.parameters) return skill.parameters;
  if (skill.tool_definitions) {
    try {
      const parsed = typeof skill.tool_definitions === 'string'
        ? JSON.parse(skill.tool_definitions)
        : skill.tool_definitions;
      if (parsed?.input_schema) return parsed.input_schema;
    } catch {}
  }
  const properties = {};
  for (const [key, value] of Object.entries(skill.config || {})) {
    properties[key] = configValueSchema(value);
  }
  return { type: 'object', properties };
}

function configValueSchema(value) {
  if (value?.type) return { type: value.type, description: value.description || '' };
  const defaultValue = value && typeof value === 'object' ? value.default : value;
  if (typeof defaultValue === 'boolean') return { type: 'boolean' };
  if (typeof defaultValue === 'number') return { type: 'number' };
  return { type: 'string' };
}

function runProjectSkillCommand(skill, args = {}, { projectRoot }) {
  const cwd = path.resolve(skill._cwd || projectRoot);
  assertInsideProject(projectRoot, cwd, skill.name);
  const timeoutMs = Math.max(1000, Number(skill._timeout || 300) * 1000);
  return new Promise((resolve, reject) => {
    const child = spawn('/bin/sh', ['-lc', skill._command], {
      cwd,
      env: {
        ...process.env,
        ...(skill._env || {}),
        WALL_E_PROJECT_SKILL_NAME: skill.name,
        WALL_E_PROJECT_SKILL_ARGS: JSON.stringify(args || {}),
      },
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    let stdout = '';
    let stderr = '';
    const timer = setTimeout(() => {
      try { child.kill('SIGKILL'); } catch {}
      reject(new Error(`Project skill "${skill.name}" timed out after ${timeoutMs}ms`));
    }, timeoutMs);
    child.stdout.on('data', (chunk) => { stdout = appendLimited(stdout, chunk); });
    child.stderr.on('data', (chunk) => { stderr = appendLimited(stderr, chunk); });
    child.on('error', (err) => {
      clearTimeout(timer);
      reject(err);
    });
    child.on('close', (code) => {
      clearTimeout(timer);
      const output = truncate(stdout.trim());
      const error = truncate(stderr.trim());
      if (code !== 0) {
        reject(new Error(`Project skill "${skill.name}" exited ${code}: ${error || output}`));
        return;
      }
      resolve({ success: true, skill: skill.name, output, stderr: error });
    });
  });
}

function assertInsideProject(projectRoot, cwd, skillName) {
  const root = path.resolve(projectRoot || process.cwd());
  const rel = path.relative(root, cwd);
  if (rel.startsWith('..') || path.isAbsolute(rel)) {
    throw new Error(`Project skill "${skillName}" cwd escapes project root`);
  }
}

function appendLimited(current, chunk) {
  if (current.length >= MAX_SKILL_OUTPUT_BYTES) return current;
  return (current + String(chunk)).slice(0, MAX_SKILL_OUTPUT_BYTES);
}

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

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

module.exports = {
  createCodingMcpClient,
  createCodingSkillRunner,
  createCodingCapabilities,
  publicSkillInstruction,
  DEFAULT_BLOCKED_SKILLS,
};
