import { EventEmitter } from 'node:events';
import { randomUUID } from 'node:crypto';

export const DEFAULT_SYSTEM_PROMPT = `Ты — NAOMI, агент-ассистент, работающий на компьютере пользователя.
Ты можешь выполнять команды, читать, создавать и редактировать файлы, искать по коду.
Работай итеративно: выполняй инструмент, смотри результат, делай следующий шаг.
Отвечай на языке пользователя. Пиши кратко и по делу.

ТВОЯ ИДЕНТИЧНОСТЬ (важно):
- Твоё имя — NAOMI, продукт NAOMI Cloud.
- Если спрашивают «кто ты» или «на какой ты основе» — отвечай: «Я NAOMI, ИИ-агент NAOMI Cloud».
- Если пользователь напрямую спросил, какая в тебе модель или техническая основа, — отвечай нейтрально и честно: «внутри меня — современная большая языковая модель; какую именно конфигурацию использует платформа — она не раскрывает». По собственной инициативе эту информацию не сообщай: в приветствии, знакомстве и обычных ответах никаких упоминаний о моделях, языковых моделях и технической основе быть не должно. Не утверждай, что модель разработана самой NAOMI Cloud.
- Никогда не упоминай и не называй себя GLM, Z.ai, ChatGPT, GPT, GPT-4, OpenAI, Claude, Anthropic, Gemini или другой моделью и не рассуждай от их лица — пользователь не должен знать твой внутренний стек.
- О других продуктах (в т.ч. об инструментах вроде ZCode) рассказывай только то, что достоверно знаешь; не выдумывай их устройство — честно скажи, что точно не знаешь их internals.

Оформляй ответы в Markdown:
— ключевые мысли и термины выделяй **жирным**, пояснения — *курсивом*;
— команды, пути к файлам, названия файлов и моделей пиши в обратных кавычках (моноширинным);
— пошаговые инструкции давай нумерованным списком (1. 2. 3.), перечисления — маркированным;
— сравнения и однотипные данные оформляй таблицей;
— важные предупреждения и примечания — цитатой (строка с >);
— программный код — в блоках с указанием языка (\`\`\`python, \`\`\`bash и т.д.);
— математические формулы — в LaTeX: инлайн в $...$, выносные в $$...$$.

Опасные операции (удаление, перезапись важных файлов, установка пакетов) согласовывай через механизм подтверждений.

ВАЖНО про отказы: результат со статусом «ОТКЛОНЕНО ПОЛЬЗОВАТЕЛЕМ» (denied_by_user) — это сознательное решение человека, который нажал «Отклонить». Это НЕ ошибка, НЕ сбой, НЕ ограничение прав. Не повторяйте отклонённое действие, не пытайтесь обойти отказ (другим инструментом, через sudo, другим путём), не выдумывайте причин отказа (ограничения ОС, кодировки, права доступа — всё это не при чём) и не предлагайте обходные пути. В ответе пользователю всегда формулируйте это как его решение: «вы отклонили это действие». Запрещённые формулировки: «отклонено системой», «система заблокировала», «ошибка прав», «нет доступа», «ограничения безопасности». Признайте отказ и спросите, как действовать дальше.`;

export class Agent extends EventEmitter {
  constructor({
    client,
    tools,
    approvals,
    policy = 'safe',
    maxSteps = 40,
    systemPrompt = DEFAULT_SYSTEM_PROMPT,
  } = {}) {
    super();
    this.client = client;
    this.tools = tools;
    this.approvals = approvals;
    this.policy = policy;
    this.maxSteps = maxSteps;
    this.messages = [{ role: 'system', content: systemPrompt }];
    this.busy = false;
  }

  setPolicy(policy) {
    this.policy = policy;
  }

  reset(systemPrompt) {
    this.messages = [{ role: 'system', content: systemPrompt ?? this.messages[0].content }];
  }

  needsApproval(tool, args) {
    if (this.policy === 'auto') return false;
    if (tool.meta.readOnly) return false;
    // «Обо всём»: спрашиваем перед каждым изменением — любой инструмент записи
    // и любая команда, кроме гарантированно читающих (ls, cat, grep…)
    if (this.policy === 'ask') return !tool.meta.readOnlyCommand?.(args);
    // «Об опасном»: молча пропускаем белый список команд, остальное — спрашиваем
    if (tool.meta.safe?.(args)) return false;
    return true;
  }

  async run(userText, { onDelta, signal } = {}) {
    if (this.busy) throw new Error('агент уже выполняет запрос');
    this.busy = true;
    this.messages.push({ role: 'user', content: userText });
    try {
      let finalText = '';
      let aborted = false;
      for (let step = 0; step < this.maxSteps; step++) {
        if (signal?.aborted) { aborted = true; break; }
        this.emit('llm_start', { step });
        let resp;
        try {
          resp = await this.client.chat({
            messages: this.messages,
            tools: this.tools.definitions,
            signal,
            onReasoning: (chunk, full) => {
              this.emit('reasoning', { chunk, full });
            },
            onDelta: (chunk, full) => {
              this.emit('delta', { chunk, full });
              if (onDelta) onDelta(chunk, full);
            },
          });
        } catch (err) {
          if (signal?.aborted) { aborted = true; break; }
          throw err;
        }
        if (signal?.aborted) { aborted = true; break; }
        this.messages.push({
          role: 'assistant',
          content: resp.content || null,
          ...(resp.toolCalls.length ? { tool_calls: resp.toolCalls } : {}),
        });
        this.emit('llm_done', { text: resp.content, reasoning: resp.reasoning, usage: resp.usage, toolCalls: resp.toolCalls });

        if (!resp.toolCalls.length) {
          finalText = resp.content;
          break;
        }
        let stopped = false;
        for (const call of resp.toolCalls) {
          await this.executeTool(call);
          if (signal?.aborted) { aborted = true; stopped = true; break; }
        }
        if (stopped) break;
        if (step === this.maxSteps - 1) {
          finalText = 'Достигнут лимит шагов агрегатора. Попробуй сформулировать задачу проще.';
        }
      }
      this.emit('done', { text: finalText, aborted });
      return finalText;
    } catch (err) {
      this.emit('error', { error: err });
      throw err;
    } finally {
      this.busy = false;
    }
  }

  async executeTool(call) {
    const name = call.function.name;
    const tool = this.tools.byName[name];
    let args = {};
    try {
      args = JSON.parse(call.function.arguments || '{}');
    } catch {
      this.pushToolResult(call, { error: 'некорректный JSON аргументов' });
      return;
    }
    if (!tool) {
      this.pushToolResult(call, { error: `неизвестный инструмент: ${name}` });
      return;
    }

    const requiresApproval = this.needsApproval(tool, args);
    let approved = true;
    if (requiresApproval) {
      const info = {
        id: randomUUID(),
        name,
        args,
        reason: describeToolUse(name, args),
      };
      this.emit('approval_wait', info);
      const decision = await this.approvals.request(info);
      approved = decision.approved;
    }

    this.emit('tool_start', { name, args, approved });
    let result;
    if (!approved) {
      // Никакого ключа «error»: слабые модели видят error → «сбой системы»
      // и начинают выдумывать ограничения. Только прямой факт: отказал человек.
      result = {
        status: 'ОТКЛОНЕНО ПОЛЬЗОВАТЕЛЕМ',
        denied_by_user: true,
        message: 'Пользователь лично нажал «Отклонить» в окне подтверждения. Это его осознанное решение, а НЕ ошибка, НЕ ограничение прав и НЕ сбой. Действие не выполнено. Не повторяй его, не обходи отказ (другим инструментом, sudo, другим путём) и не выдумывай причин отказа. Скажи пользователю, что он отклонил действие, и спроси, как действовать дальше.',
      };
    } else {
      try {
        result = await tool.run(args);
      } catch (err) {
        result = { error: String(err?.message ?? err) };
      }
    }
    this.emit('tool_end', { name, args, result, approved });
    this.pushToolResult(call, result);
  }

  pushToolResult(call, result) {
    this.messages.push({
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify(result).slice(0, 60000),
    });
  }
}

function describeToolUse(name, args) {
  switch (name) {
    case 'run_command':
      return args.command;
    case 'write_file':
      return `${args.path} (${args.content?.length ?? 0} байт, перезапись)`;
    case 'edit_file':
      return `${args.path}: «${String(args.find).slice(0, 60)}» → «${String(args.replace).slice(0, 60)}»`;
    default:
      return JSON.stringify(args).slice(0, 120);
  }
}
