'use strict';

const crypto = require('node:crypto');
const { AgentCatalog } = require('./agent-catalog');
const { taskPacketFromToolArgs, validateTaskPacket } = require('../agent-runtime/task-packet');

class TaskTool {
  constructor({ agentCatalog = null, runner = null, agentRuntime = null, brain = null, transcript = null, lifecycleHooks = null } = {}) {
    this.agentCatalog = agentCatalog || new AgentCatalog();
    this.runner = runner || null;
    this.agentRuntime = agentRuntime || null;
    this.brain = brain || null;
    this.transcript = transcript || null;
    this.lifecycleHooks = lifecycleHooks || null;
  }

  definition() {
    return {
      name: 'task',
      description: `Create or resume a child Wall-E coding task with a selected agent.\nAvailable agents:\n${this.agentCatalog.describeForTool()}`,
      input_schema: {
        type: 'object',
        properties: {
          prompt: { type: 'string', description: 'Task prompt for the child agent' },
          agent: { type: 'string', description: 'Agent id, such as build, plan, review, or explore' },
          task_id: { type: 'string', description: 'Existing task id to resume' },
          action: { type: 'string', enum: ['run', 'create', 'status', 'resume', 'cancel'], description: 'Lifecycle action. run is default; create/defer queues without running.' },
          defer: { type: 'boolean', description: 'Create the child task but do not run it yet.' },
          async: { type: 'boolean', description: 'Alias for defer.' },
          resume: { type: 'boolean', description: 'Resume task_id instead of creating a new task' },
          packet: { type: 'object', description: 'Optional typed task packet with objective, scope, branchPolicy, acceptanceTests, reportingContract, and escalationPolicy.' },
          scope: { type: 'string', description: 'Task scope kind: workspace, module, single_file, or custom.' },
          scope_path: { type: 'string', description: 'Path required for non-workspace scopes.' },
          acceptance_tests: { type: 'array', items: { type: 'string' }, description: 'Commands that define acceptance for this child task.' },
        },
        required: [],
      },
    };
  }

  async execute(args = {}, ctx = {}) {
    const taskConfig = args.task_id ? this._loadTaskConfig(args.task_id) : {};
    const agentId = args.agent || taskConfig.agent || 'build';
    const agent = this.agentCatalog.get(agentId);
    if (!agent) return { error: `Unknown agent: ${agentId}`, invalid: true };
    const action = normalizeAction(args);
    if (!VALID_ACTIONS.has(action)) return { error: `Unknown task action: ${action}`, invalid: true };
    const packetValidation = validateTaskPacket(taskPacketFromToolArgs(args, ctx));
    const taskPacket = packetValidation.packet;
    if (!args.task_id && !packetValidation.ok) {
      return { error: `Invalid task packet: ${packetValidation.errors.join('; ')}`, invalid: true, taskPacket };
    }

    if (action === 'status') {
      if (!args.task_id) return { error: 'task_id is required for status', invalid: true };
      const task = this._loadTask(args.task_id);
      await this._appendPart(ctx, {
        task_id: args.task_id,
        parent_id: ctx.sessionId || '',
        agent: agentId,
        state: 'status',
        status: task?.status || 'unknown',
      });
      await this._emitTaskEvent('task_updated', {
        taskId: args.task_id,
        parentId: ctx.sessionId || '',
        agentId,
        state: 'status',
        status: task?.status || 'unknown',
      }, ctx);
      return { task_id: args.task_id, agent: agentId, status: task?.status || 'unknown', task: publicTask(task) };
    }

    if (action === 'cancel') {
      if (!args.task_id) return { error: 'task_id is required for cancel', invalid: true };
      this._cancelTask(args.task_id, args.reason || 'Canceled by parent agent');
      const task = this._loadTask(args.task_id);
      await this._appendPart(ctx, {
        task_id: args.task_id,
        parent_id: ctx.sessionId || '',
        agent: agentId,
        state: 'canceled',
        reason: args.reason || '',
      });
      await this._emitTaskEvent('task_finished', {
        taskId: args.task_id,
        parentId: ctx.sessionId || '',
        agentId,
        state: 'canceled',
        status: task?.status || 'canceled',
        reason: args.reason || '',
      }, ctx);
      return { task_id: args.task_id, agent: agentId, status: task?.status || 'canceled', task: publicTask(task) };
    }

    const taskId = args.task_id || this._createTask(args, agent, ctx, taskPacket);
    const prompt = args.prompt || taskConfig.prompt || taskPacket.objective || this._loadPrompt(taskId) || '';
    if (!prompt) return { error: 'prompt is required for new task', task_id: taskId };

    await this._appendPart(ctx, {
      task_id: taskId,
      parent_id: ctx.sessionId || '',
      agent: agentId,
      state: args.task_id || args.resume ? 'resumed' : 'created',
      prompt,
      taskPacket,
    });
    await this._emitTaskEvent('task_started', {
      taskId,
      parentId: ctx.sessionId || '',
      agentId,
      state: args.task_id || args.resume ? 'resumed' : 'created',
      prompt,
      taskPacket,
    }, ctx);

    if (action === 'create' || args.defer || args.async) {
      await this._appendPart(ctx, {
        task_id: taskId,
        parent_id: ctx.sessionId || '',
        agent: agentId,
        state: 'deferred',
        prompt,
        taskPacket,
      });
      await this._emitTaskEvent('task_updated', {
        taskId,
        parentId: ctx.sessionId || '',
        agentId,
        state: 'deferred',
        status: this._loadTask(taskId)?.status || 'pending',
        prompt,
        taskPacket,
      }, ctx);
      return {
        task_id: taskId,
        agent: agentId,
        status: this._loadTask(taskId)?.status || 'pending',
        taskPacket,
        result: { success: true, deferred: true, output: 'Task created and deferred.' },
      };
    }

    this._markRunning(taskId);
    await this._emitTaskEvent('task_updated', {
      taskId,
      parentId: ctx.sessionId || '',
      agentId,
      state: 'running',
      status: 'running',
      prompt,
      taskPacket,
    }, ctx);
    // Children answer questions; they don't dump transcripts into the
    // parent's context (Cognition: parallel children should return
    // condensed findings, not full traces). Hidden/internal agents
    // (summary, title, compaction) keep their exact prompts.
    const childPrompt = (agent.hidden || agent.mode === 'summary')
      ? prompt
      : `${prompt}${PARENT_REPORT_CONTRACT}`;
    let result;
    try {
      if (this.agentRuntime) {
        result = await this.agentRuntime.spawnAgentSession({
          parentRunId: ctx.runId || ctx.sessionId || '',
          runnerId: agent.runnerId || agent.id,
          providerType: agent.providerType,
          mode: agent.mode,
          task: childPrompt,
          cwd: ctx.cwd || '',
          sessionId: args.session_id || args.agent_session_id,
          persistent: Boolean(args.persistent),
          cleanupPolicy: args.cleanup_policy,
          metadata: { taskPacket },
        });
      } else {
        result = this.runner
          ? await this.runner({ prompt: childPrompt, agent, taskId, ctx, taskPacket })
          : { success: true, output: 'Task created but no runner is configured.', deferred: true };
      }
    } catch (err) {
      result = { success: false, error: err.message };
    }
    if (!result || typeof result !== 'object') result = { success: true, output: String(result ?? '') };

    this._updateTask(taskId, result);
    await this._appendPart(ctx, {
      task_id: taskId,
      parent_id: ctx.sessionId || '',
      agent: agentId,
      state: result.success === false ? 'error' : 'completed',
      result,
    });
    await this._emitTaskEvent(result.success === false ? 'task_failed' : 'task_finished', {
      taskId,
      parentId: ctx.sessionId || '',
      agentId,
      state: result.success === false ? 'error' : 'completed',
      status: this._loadTask(taskId)?.status || (result.success === false ? 'failed' : 'completed'),
      result,
    }, ctx);

    return { task_id: taskId, agent: agentId, taskPacket, result: capResultForParent(result) };
  }

  _createTask(args, agent, ctx, taskPacket = null) {
    if (!this.brain?.insertTask) return `task_${crypto.randomUUID().slice(0, 8)}`;
    const inserted = this.brain.insertTask({
      title: args.prompt ? args.prompt.slice(0, 80) : `Child task: ${agent.id}`,
      description: args.prompt || '',
      priority: 'normal',
      type: 'once',
      execution: 'skill',
      skill: 'coding-agent',
      skill_config: JSON.stringify({
        parent_id: ctx.sessionId || '',
        agent: agent.id,
        mode: agent.mode,
        prompt: args.prompt || '',
        cwd: ctx.cwd || '',
        taskPacket,
      }),
      source: 'walle-task-tool',
      source_ref: ctx.sessionId || '',
    });
    return inserted.id;
  }

  _loadTaskConfig(taskId) {
    if (!taskId || !this.brain?.getTask) return {};
    const task = this.brain.getTask(taskId);
    if (!task?.skill_config) return {};
    try {
      return JSON.parse(task.skill_config) || {};
    } catch {
      return {};
    }
  }

  _loadTask(taskId) {
    if (!taskId || !this.brain?.getTask) return null;
    return this.brain.getTask(taskId);
  }

  _loadPrompt(taskId) {
    if (!taskId || !this.brain?.getTask) return '';
    const task = this.brain.getTask(taskId);
    if (!task?.skill_config) return task?.description || '';
    try {
      return JSON.parse(task.skill_config).prompt || task.description || '';
    } catch {
      return task.description || '';
    }
  }

  _markRunning(taskId) {
    if (!taskId || !this.brain?.updateTask) return;
    const updates = {
      status: 'running',
      started_at: new Date().toISOString(),
      error: null,
    };
    this.brain.updateTask(taskId, updates);
  }

  _updateTask(taskId, result) {
    if (!taskId || !this.brain?.updateTask) return;
    const deferred = Boolean(result.deferred);
    const failed = result.success === false || Boolean(result.error);
    this.brain.updateTask(taskId, {
      status: deferred ? 'pending' : failed ? 'failed' : 'completed',
      result: JSON.stringify(result),
      error: failed ? String(result.error || 'Task failed') : null,
      completed_at: deferred ? null : new Date().toISOString(),
    });
  }

  _cancelTask(taskId, reason) {
    if (!taskId || !this.brain?.updateTask) return;
    this.brain.updateTask(taskId, {
      status: 'canceled',
      error: reason || null,
      completed_at: new Date().toISOString(),
    });
  }

  async _appendPart(ctx, data) {
    const target = ctx.transcript || this.transcript;
    if (target?.appendPart) {
      await target.appendPart({ sessionId: ctx.sessionId || '', cwd: ctx.cwd || '', partType: 'subtask', data });
    } else if (target?.append) {
      await target.append({ type: 'walle_part', sessionId: ctx.sessionId || '', cwd: ctx.cwd || '', partType: 'subtask', data });
    }
  }

  async _emitTaskEvent(type, data, ctx = {}) {
    const hooks = ctx.lifecycleHooks || this.lifecycleHooks;
    if (!hooks?.emit) return null;
    return hooks.emit(type, taskEventPayload(data), {
      sessionId: ctx.sessionId || data.parentId || '',
      cwd: ctx.cwd || '',
      actor: 'task-tool',
    });
  }
}

const PARENT_REPORT_CONTRACT = '\n\n[Parent contract] Your final message IS the report returned to the parent agent. Return condensed findings — what changed, where, and the verification evidence — not a transcript or play-by-play.';
const MAX_PARENT_OUTPUT_CHARS = 4000;

/** Bound what flows back into the parent's context window. */
function capResultForParent(result) {
  if (!result || typeof result !== 'object') return result;
  const output = typeof result.output === 'string' ? result.output : null;
  if (!output || output.length <= MAX_PARENT_OUTPUT_CHARS) return result;
  return {
    ...result,
    output: `${output.slice(0, MAX_PARENT_OUTPUT_CHARS)}\n[child output truncated: ${output.length - MAX_PARENT_OUTPUT_CHARS} chars omitted — resume the task with task_id if more detail is needed]`,
    outputTruncated: true,
  };
}

function normalizeAction(args = {}) {
  if (args.action) return String(args.action).toLowerCase();
  if (args.resume) return 'resume';
  if (args.defer || args.async) return 'create';
  return 'run';
}

const VALID_ACTIONS = new Set(['run', 'create', 'status', 'resume', 'cancel']);

function publicTask(task) {
  if (!task) return null;
  let result = task.result || null;
  if (typeof result === 'string' && result.trim()) {
    try {
      result = JSON.parse(result);
    } catch {}
  }
  return {
    id: task.id,
    title: task.title || '',
    status: task.status || '',
    error: task.error || null,
    started_at: task.started_at || null,
    completed_at: task.completed_at || null,
    result,
  };
}

function taskEventPayload(data = {}) {
  const payload = {
    taskId: data.taskId || '',
    parentId: data.parentId || '',
    agent: data.agentId || '',
    state: data.state || '',
  };
  if (data.status) payload.status = data.status;
  if (data.reason) payload.reason = data.reason;
  if (data.prompt) {
    payload.promptPreview = previewText(data.prompt, 240);
    payload.promptHash = hashValue(data.prompt);
  }
  if (data.taskPacket) {
    payload.taskPacket = {
      objectiveHash: hashValue(data.taskPacket.objective || ''),
      scope: data.taskPacket.scope || null,
      acceptanceTestCount: Array.isArray(data.taskPacket.acceptanceTests) ? data.taskPacket.acceptanceTests.length : 0,
    };
  }
  if (data.result) {
    payload.result = previewResult(data.result);
  }
  return payload;
}

function previewText(value, max) {
  const text = String(value || '').replace(/\s+/g, ' ').trim();
  return text.length <= max ? text : `${text.slice(0, Math.max(0, max - 16))} [truncated]`;
}

function previewResult(result) {
  if (!result || typeof result !== 'object') return result;
  const out = {
    success: result.success !== false,
    deferred: Boolean(result.deferred),
  };
  if (result.error) out.error = previewText(result.error, 240);
  if (result.output) {
    out.outputPreview = previewText(result.output, 320);
    out.outputHash = hashValue(result.output);
  }
  if (result.sessionId) out.sessionId = result.sessionId;
  if (result.exitCode !== undefined) out.exitCode = result.exitCode;
  return out;
}

function hashValue(value) {
  return crypto.createHash('sha256').update(String(value || '')).digest('hex');
}

module.exports = {
  TaskTool,
  taskEventPayload,
  capResultForParent,
  PARENT_REPORT_CONTRACT,
  MAX_PARENT_OUTPUT_CHARS,
};
