'use strict';

const fs = require('node:fs');
const path = require('node:path');
const { spawn } = require('node:child_process');
const { findSkill } = require('./skill-loader');

const WALL_E_DIR = path.join(__dirname, '..');

async function runScriptSkillByName(skillName, task = {}, opts = {}) {
  const skill = opts.skill || findSkill(skillName);
  if (!skill) throw new Error(`Skill not found: ${skillName}`);
  return runScriptSkill(skill, task, opts);
}

function runScriptSkill(skill, task = {}, opts = {}) {
  if (!skill || skill.execution !== 'script') {
    throw new Error(`Skill "${skill?.name || 'unknown'}" is not script-based`);
  }
  const entryPath = resolveSkillEntry(skill);
  const skillConfig = buildSkillConfig(skill, task);
  const args = buildSkillArgs(skill, skillConfig);
  const log = typeof opts.log === 'function' ? opts.log : () => {};
  log(`Running: node ${entryPath}${args.length ? ' ' + args.join(' ') : ''}`);

  return new Promise((resolve, reject) => {
    const env = {
      ...process.env,
      HOME: process.env.HOME,
      WALL_E_SKILL_CONFIG: JSON.stringify(skillConfig),
      WALL_E_SKILL_DIR: skill.dir,
      WALL_E_SRC_DIR: WALL_E_DIR,
      // Script skills run as dedicated short-lived subprocesses that open their own
      // brain.db connection (brain.initDb()). The daemon's brain lock defaults to a 0ms
      // fail-fast (correct for the daemon event loop, which must never Atomics.wait), but
      // inherited by a subprocess that means it ABORTS the instant the daemon holds the
      // lock for a single statement — observed as ~272 "SQLite write lock busy" aborts
      // (slack-mentions Exit code 1, dropped calendar/email syncs). A subprocess can
      // safely block-wait briefly and retry, so give it a bounded timeout. In practice
      // the lock frees in <100ms; 5s is just the ceiling. Caller env still wins.
      WALL_E_SQLITE_WRITE_LOCK_TIMEOUT_MS:
        process.env.WALL_E_SQLITE_WRITE_LOCK_TIMEOUT_MS || '5000',
      ...(opts.env || {}),
    };
    if (task.checkpoint) env.WALL_E_CHECKPOINT = task.checkpoint;

    const child = spawn(process.execPath, [entryPath, ...args], {
      cwd: opts.cwd || WALL_E_DIR,
      env,
      stdio: ['ignore', 'pipe', 'pipe'],
    });
    if (typeof opts.onProcess === 'function') opts.onProcess(child);

    let stdout = '';
    let stderr = '';
    child.stdout.setEncoding('utf8');
    child.stderr.setEncoding('utf8');
    child.stdout.on('data', (chunk) => {
      stdout += chunk;
      for (const line of chunk.split('\n').filter(Boolean)) {
        log(line);
        if (line.startsWith('CHECKPOINT:') && typeof opts.onCheckpoint === 'function') {
          opts.onCheckpoint(line.slice(11).trim());
        }
      }
    });
    child.stderr.on('data', (chunk) => {
      stderr += chunk;
      for (const line of chunk.split('\n').filter(Boolean)) log(`[stderr] ${line}`);
    });
    child.on('close', (code) => {
      if (typeof opts.onClose === 'function') opts.onClose(child, code);
      if (code !== 0 && code !== null) {
        reject(new Error(`Exit code ${code}: ${stderr.slice(0, 500)}`));
        return;
      }
      let output = stdout.trim();
      if (stderr.trim()) output += '\n[stderr] ' + stderr.trim();
      resolve(output);
    });
    child.on('error', (err) => {
      if (typeof opts.onClose === 'function') opts.onClose(child, null);
      reject(err);
    });
  });
}

function resolveSkillEntry(skill) {
  const entry = skill.entry || 'run.js';
  const entryPath = path.resolve(skill.dir, entry);
  const lexicalSkillRel = path.relative(path.resolve(skill.dir), entryPath);
  const lexicalWallERel = path.relative(WALL_E_DIR, entryPath);
  const lexicallyInsideSkill = !lexicalSkillRel.startsWith('..') && !path.isAbsolute(lexicalSkillRel);
  const lexicallyInsideWallE = !lexicalWallERel.startsWith('..') && !path.isAbsolute(lexicalWallERel);
  if (!lexicallyInsideSkill && !lexicallyInsideWallE) {
    throw new Error(`Script skill ${skill.name} entry escapes skill and Wall-E directories`);
  }
  if (!fs.existsSync(entryPath)) {
    throw new Error(`Skill entry point not found: ${entryPath}`);
  }
  const realEntryPath = fs.realpathSync(entryPath);
  const realSkillDir = fs.realpathSync(skill.dir);
  const realWallEDir = fs.realpathSync(WALL_E_DIR);
  const skillRel = path.relative(realSkillDir, realEntryPath);
  const wallERel = path.relative(realWallEDir, realEntryPath);
  const insideSkillDir = !skillRel.startsWith('..') && !path.isAbsolute(skillRel);
  const insideWallE = !wallERel.startsWith('..') && !path.isAbsolute(wallERel);
  if (!insideSkillDir && !insideWallE) {
    throw new Error(`Script skill ${skill.name} entry escapes skill and Wall-E directories`);
  }
  return realEntryPath;
}

function buildSkillConfig(skill, task = {}) {
  const skillConfig = {};
  if (skill.config) {
    for (const [key, value] of Object.entries(skill.config)) {
      if (value && typeof value === 'object' && value.default !== undefined) {
        skillConfig[key] = value.default;
      }
    }
  }
  let taskConfig = {};
  if (task.skill_config) {
    if (typeof task.skill_config === 'string') {
      try {
        taskConfig = JSON.parse(task.skill_config);
      } catch {
        taskConfig = {};
      }
    } else {
      taskConfig = task.skill_config;
    }
  }
  return { ...skillConfig, ...taskConfig };
}

function buildSkillArgs(skill, skillConfig = {}) {
  const args = [...(skill.args || [])].map(arg => String(arg));
  if (skillConfig.mode && !args.includes(skillConfig.mode) && skill.name === 'slack-backfill') {
    args.push(String(skillConfig.mode));
    if (skillConfig.month) args.push(String(skillConfig.month));
  }
  return args;
}

module.exports = {
  buildSkillArgs,
  buildSkillConfig,
  resolveSkillEntry,
  runScriptSkill,
  runScriptSkillByName,
};
