import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';

// Node не разворачивает ~ в путях — разворачиваем сами, иначе создаётся
// буквальная папка "~" в cwd (реальный кейс: ~/Desktop уезжал в ./~/Desktop)
function expandPath(p) {
  if (typeof p !== 'string' || !p) return p;
  if (p === '~') return os.homedir();
  if (p.startsWith('~/')) return path.join(os.homedir(), p.slice(2));
  return p;
}

// Белый список для «Об опасного»: рутинная работа с файлами — молча.
// Установщики пакетов (npm, pip…) и сетевые загрузки (curl, wget) сюда НЕ входят:
// режим по описанию спрашивает перед установками — это зона заражения и перезаписи
const DEFAULT_SAFE_COMMANDS = [
  'ls', 'dir', 'cat', 'pwd', 'echo', 'grep', 'rg', 'find', 'git', 'node',
  'python', 'python3', 'mkdir', 'touch', 'cp', 'mv', 'head', 'tail', 'wc',
  'which', 'whoami', 'df', 'du', 'date', 'tar', 'unzip', 'cd', 'set',
  'export', 'diff', 'sort', 'uniq',
];

// Подмножество белого списка — команды, которые гарантированно ничего не меняют.
// Нужны режиму «Обо всём»: он спрашивает перед каждым изменением, но не перед чтением
const READONLY_COMMANDS = [
  'ls', 'dir', 'cat', 'pwd', 'echo', 'grep', 'rg', 'find', 'head', 'tail',
  'wc', 'which', 'whoami', 'df', 'du', 'date', 'diff', 'sort', 'uniq',
  'stat', 'file',
];

const DEFAULT_DANGEROUS_PATTERNS = [
  /\bsudo\b/,
  /\brm\s+(-[a-z]*[rf][a-z]*\s+)+(\/|~|\$HOME|\*)?/i,
  /\bdd\s+if=/,
  /\bmkfs\b/,
  /\bshutdown\b|\breboot\b/,
  /:\(\)\s*\{\s*:\|:\s*&\s*\}\s*;/,
  /\b(curl|wget)\b[^|;&]*\|\s*(ba|z|fi)?sh\b/,
  /\bchmod\s+777\s+\//,
];

const MAX_OUTPUT_CHARS = 20000;
const MAX_FILE_CHARS = 200000;

export function isCommandSafe(command, { safeCommands = DEFAULT_SAFE_COMMANDS, dangerousPatterns = DEFAULT_DANGEROUS_PATTERNS } = {}) {
  if (typeof command !== 'string' || !command.trim()) return false;
  for (const pattern of dangerousPatterns) {
    if (pattern.test(command)) return false;
  }
  const firstToken = command.trim().split(/\s+/)[0].split(/[;|&]/).pop();
  return safeCommands.includes(firstToken) || safeCommands.includes(path.basename(firstToken));
}

function truncate(text, limit = MAX_OUTPUT_CHARS) {
  if (text.length <= limit) return text;
  return `${text.slice(0, limit)}\n... [обрезано, всего ${text.length} символов]`;
}

function globToRegExp(pattern) {
  let re = '';
  for (let i = 0; i < pattern.length; i++) {
    const ch = pattern[i];
    if (ch === '*') {
      if (pattern[i + 1] === '*') {
        re += '.*';
        i++;
        if (pattern[i + 1] === '/') i++;
      } else {
        re += '[^/]*';
      }
    } else if (ch === '?') {
      re += '[^/]';
    } else if ('\\^$.|+()[]{}'.includes(ch)) {
      re += `\\${ch}`;
    } else {
      re += ch;
    }
  }
  return new RegExp(`^${re}$`, 'i');
}

async function walkFiles(root, { maxEntries = 20000, maxDepth = 12 } = {}) {
  const files = [];
  const roots = Array.isArray(root) ? root : [root];
  async function walk(dir, depth) {
    if (depth > maxDepth || files.length >= maxEntries) return;
    let entries;
    try {
      entries = await fs.readdir(dir, { withFileTypes: true });
    } catch {
      return;
    }
    for (const entry of entries) {
      if (files.length >= maxEntries) return;
      if (['node_modules', '.git', '__pycache__', '.venv', 'venv'].includes(entry.name)) continue;
      const full = path.join(dir, entry.name);
      if (entry.isDirectory()) {
        await walk(full, depth + 1);
      } else if (entry.isFile()) {
        files.push(full);
      }
    }
  }
  for (const r of roots) {
    const stat = await fs.stat(r).catch(() => null);
    if (stat?.isDirectory()) await walk(r, 0);
    else if (stat?.isFile()) files.push(r);
  }
  return files;
}

function runCommand(command, timeoutMs) {
  return new Promise((resolve) => {
    const isWin = process.platform === 'win32';
    const shell = isWin ? 'cmd' : 'bash';
    const shellArgs = isWin ? ['/c', command] : ['-lc', command];
    const started = Date.now();
    const child = spawn(shell, shellArgs, {
      cwd: process.cwd(),
      env: process.env,
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    let stdout = '';
    let stderr = '';
    let settled = false;

    const timer = setTimeout(() => {
      if (!settled) {
        child.kill('SIGKILL');
        settled = true;
        resolve({
          exitCode: null,
          stdout: truncate(stdout),
          stderr: truncate(stderr) + '\n[превышен таймаут команды]',
          durationMs: Date.now() - started,
          timedOut: true,
        });
      }
    }, timeoutMs);

    child.stdout.on('data', (d) => { stdout += d.toString(); });
    child.stderr.on('data', (d) => { stderr += d.toString(); });
    child.on('error', (err) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({ exitCode: null, stdout: '', stderr: String(err), durationMs: Date.now() - started });
    });
    child.on('close', (code) => {
      if (settled) return;
      settled = true;
      clearTimeout(timer);
      resolve({
        exitCode: code,
        stdout: truncate(stdout),
        stderr: truncate(stderr),
        durationMs: Date.now() - started,
      });
    });
  });
}

export function buildTools({
  safeCommands = DEFAULT_SAFE_COMMANDS,
  dangerousPatterns = DEFAULT_DANGEROUS_PATTERNS,
  commandTimeoutMs = 120000,
  customTools = [],
} = {}) {
  const tools = [
    {
      name: 'run_command',
      meta: {
        readOnly: false,
        safe: (args) => isCommandSafe(args?.command, { safeCommands, dangerousPatterns }),
        readOnlyCommand: (args) => isCommandSafe(args?.command, { safeCommands: READONLY_COMMANDS, dangerousPatterns }),
      },
      definition: {
        type: 'function',
        function: {
          name: 'run_command',
          description: 'Выполнить shell-команду на компьютере пользователя и вернуть stdout/stderr. Для составных действий используй && или ;',
          parameters: {
            type: 'object',
            properties: {
              command: { type: 'string', description: 'Команда для выполнения' },
              timeout_ms: { type: 'integer', description: 'Таймаут в мс (по умолчанию 120000, максимум 600000)' },
            },
            required: ['command'],
          },
        },
      },
      async run(args) {
        const timeout = Math.min(Math.max(args.timeout_ms ?? commandTimeoutMs, 1000), 600000);
        const result = await runCommand(args.command, timeout);
        return { ...result, command: args.command };
      },
    },
    {
      name: 'read_file',
      meta: { readOnly: true },
      definition: {
        type: 'function',
        function: {
          name: 'read_file',
          description: 'Прочитать текстовый файл целиком или диапазон строк',
          parameters: {
            type: 'object',
            properties: {
              path: { type: 'string', description: 'Путь к файлу' },
              offset: { type: 'integer', description: 'Начать со строки (1-based)' },
              limit: { type: 'integer', description: 'Сколько строк прочитать' },
            },
            required: ['path'],
          },
        },
      },
      async run(args) {
        args.path = expandPath(args.path);
        let content = await fs.readFile(args.path, 'utf8');
        let lines = content.split('\n');
        const offset = args.offset ? Math.max(1, args.offset) : 1;
        if (args.offset || args.limit) {
          lines = lines.slice(offset - 1, args.limit ? offset - 1 + args.limit : undefined);
          content = lines.map((l, i) => `${offset + i}: ${l}`).join('\n');
        } else {
          content = lines.map((l, i) => `${i + 1}: ${l}`).join('\n');
        }
        if (content.length > MAX_FILE_CHARS) content = truncate(content, MAX_FILE_CHARS);
        return { path: args.path, content, lines: lines.length };
      },
    },
    {
      name: 'write_file',
      meta: { readOnly: false },
      definition: {
        type: 'function',
        function: {
          name: 'write_file',
          description: 'Создать файл (и недостающие директории) или полностью перезаписать существующий',
          parameters: {
            type: 'object',
            properties: {
              path: { type: 'string' },
              content: { type: 'string' },
            },
            required: ['path', 'content'],
          },
        },
      },
      async run(args) {
        args.path = expandPath(args.path);
        await fs.mkdir(path.dirname(args.path), { recursive: true });
        const existed = await fs.access(args.path).then(() => true, () => false);
        const oldContent = existed ? await fs.readFile(args.path, 'utf8').catch(() => '') : '';
        await fs.writeFile(args.path, args.content, 'utf8');
        const countLines = (t) => (t ? t.split('\n').length : 0);
        return {
          path: args.path,
          bytes: args.content.length,
          overwritten: existed,
          addedLines: countLines(args.content),
          removedLines: existed ? countLines(oldContent) : 0,
        };
      },
    },
    {
      name: 'edit_file',
      meta: { readOnly: false },
      definition: {
        type: 'function',
        function: {
          name: 'edit_file',
          description: 'Заменить в файле первое (или все) вхождение строки find на replace. Ошибка, если find не найден',
          parameters: {
            type: 'object',
            properties: {
              path: { type: 'string' },
              find: { type: 'string', description: 'Точное вхождение для замены' },
              replace: { type: 'string' },
              all: { type: 'boolean', description: 'Заменить все вхождения (по умолчанию false)' },
            },
            required: ['path', 'find', 'replace'],
          },
        },
      },
      async run(args) {
        args.path = expandPath(args.path);
        const content = await fs.readFile(args.path, 'utf8');
        if (!content.includes(args.find)) {
          return { error: `вхождение не найдено в ${args.path}` };
        }
        const occurrences = args.all ? content.split(args.find).length - 1 : 1;
        const countLines = (t) => (t ? t.split('\n').length : 0);
        const addedLines = countLines(args.replace) * occurrences;
        const removedLines = countLines(args.find) * occurrences;
        const updated = args.all
          ? content.split(args.find).join(args.replace)
          : content.replace(args.find, args.replace);
        await fs.writeFile(args.path, updated, 'utf8');
        return { path: args.path, replaced: occurrences, addedLines, removedLines };
      },
    },
    {
      name: 'list_dir',
      meta: { readOnly: true },
      definition: {
        type: 'function',
        function: {
          name: 'list_dir',
          description: 'Показать содержимое директории (файлы и подпапки)',
          parameters: {
            type: 'object',
            properties: {
              path: { type: 'string', description: 'Путь (по умолчанию текущая директория)' },
            },
            required: [],
          },
        },
      },
      async run(args) {
        const dir = expandPath(args.path || '.');
        const entries = await fs.readdir(dir, { withFileTypes: true });
        const listed = entries
          .slice(0, 500)
          .map((e) => `${e.isDirectory() ? '[dir] ' : '      '}${e.name}`);
        return { path: path.resolve(dir), entries: listed };
      },
    },
    {
      name: 'search_files',
      meta: { readOnly: true },
      definition: {
        type: 'function',
        function: {
          name: 'search_files',
          description: 'Найти файлы по glob-шаблону, например "**/*.ts" или "src/**/*.json"',
          parameters: {
            type: 'object',
            properties: {
              pattern: { type: 'string' },
              path: { type: 'string', description: 'Где искать (по умолчанию текущая директория)' },
            },
            required: ['pattern'],
          },
        },
      },
      async run(args) {
        const root = expandPath(args.path || '.');
        const re = globToRegExp(args.pattern);
        const files = await walkFiles(root);
        const matches = files
          .map((f) => path.relative(root, f))
          .filter((rel) => re.test(rel) || re.test(rel.split(path.sep).join('/')))
          .slice(0, 200);
        return { pattern: args.pattern, matches };
      },
    },
    {
      name: 'search_code',
      meta: { readOnly: true },
      definition: {
        type: 'function',
        function: {
          name: 'search_code',
          description: 'Поиск по содержимому файлов: regex-запрос, как grep. Возвращает файл:строка:совпадение',
          parameters: {
            type: 'object',
            properties: {
              query: { type: 'string', description: 'Регулярное выражение' },
              path: { type: 'string', description: 'Где искать (файл или директория)' },
              include: { type: 'string', description: 'Glob-фильтр файлов, например "*.py"' },
            },
            required: ['query'],
          },
        },
      },
      async run(args) {
        const root = expandPath(args.path || '.');
        let re;
        try {
          re = new RegExp(args.query, 'i');
        } catch (err) {
          return { error: `некорректный regex: ${err.message}` };
        }
        const includeRe = args.include ? globToRegExp(`**/${args.include}`) : null;
        const files = await walkFiles(root, { maxEntries: 5000 });
        const results = [];
        for (const file of files) {
          if (includeRe) {
            const rel = file.split(path.sep).join('/');
            if (!includeRe.test(rel)) continue;
          }
          const stat = await fs.stat(file).catch(() => null);
          if (!stat || stat.size > 1024 * 1024) continue;
          const content = await fs.readFile(file, 'utf8').catch(() => null);
          if (content === null) continue;
          const lines = content.split('\n');
          for (let i = 0; i < lines.length; i++) {
            if (re.test(lines[i])) {
              results.push(`${file}:${i + 1}: ${lines[i].trim().slice(0, 200)}`);
              if (results.length >= 200) return { query: args.query, results, truncated: true };
            }
          }
        }
        return { query: args.query, results };
      },
    },
    ...customTools,
  ];

  return {
    definitions: tools.map((t) => t.definition),
    byName: Object.fromEntries(tools.map((t) => [t.name, t])),
  };
}
