import { randomUUID } from 'node:crypto';
import * as runTracker from '../runs/tracker.js';
import * as agentRepo from './repository.js';
import {
  getToolsForBindings,
  type ToolExecutionContext,
  type ToolExecutionResult,
} from './tool-registry.js';
import { bus } from '../../lib/event-bus.js';

// ── Types ────────────────────────────────────────────────────

export interface ExecuteAgentInput {
  agentId: string;
  productId: string;
  userId: string;
  input: Record<string, unknown>;
  parentRunId?: string;
  triggeredBy?: string;
}

export interface ExecuteAgentResult {
  runId: string;
  status: 'succeeded' | 'failed' | 'cancelled' | 'review_required';
  output?: Record<string, unknown>;
  error?: string;
  tokenUsage: { prompt: number; completion: number; total: number };
  stepsCompleted: number;
  durationMs: number;
}

export interface StepEvent {
  runId: string;
  stepName: string;
  status: string;
  output?: unknown;
  error?: string;
  tokenUsage?: { prompt: number; completion: number; total: number };
}

type StepListener = (event: StepEvent) => void;

// ── Active Runs ──────────────────────────────────────────────

const activeRuns = new Map<string, AbortController>();
const stepListeners = new Map<string, Set<StepListener>>();

export function getActiveAgentRuns(): string[] {
  return Array.from(activeRuns.keys());
}

export function cancelAgentRun(runId: string): boolean {
  const controller = activeRuns.get(runId);
  if (!controller) return false;
  controller.abort();
  return true;
}

export function onStepEvent(runId: string, listener: StepListener): () => void {
  const listeners = stepListeners.get(runId) ?? new Set();
  listeners.add(listener);
  stepListeners.set(runId, listeners);
  return () => {
    listeners.delete(listener);
    if (listeners.size === 0) stepListeners.delete(runId);
  };
}

function emitStepEvent(event: StepEvent): void {
  const listeners = stepListeners.get(event.runId);
  if (listeners) {
    for (const listener of listeners) {
      try {
        listener(event);
      } catch {
        // best-effort
      }
    }
  }
}

// ── Executor ─────────────────────────────────────────────────

export async function executeAgent(input: ExecuteAgentInput): Promise<ExecuteAgentResult> {
  const runId = `run_${randomUUID()}`;
  const startTime = Date.now();
  const controller = new AbortController();
  activeRuns.set(runId, controller);

  const tokenUsage = { prompt: 0, completion: 0, total: 0 };
  let stepsCompleted = 0;

  try {
    // Get agent + published version
    const agent = await agentRepo.getAgent(input.agentId, input.productId);
    const version = await agentRepo.getPublishedVersion(input.agentId);
    if (!version) {
      throw new Error(`No published version found for agent '${input.agentId}'`);
    }

    // Start run tracking
    await runTracker.startRun({
      id: runId,
      productId: input.productId,
      kind: 'agent',
      name: agent.name,
      source: `platform.agents.${agent.key}`,
      triggeredBy: input.triggeredBy ?? input.userId,
      parentRunId: input.parentRunId,
      input: input.input,
      metadata: {
        agentId: input.agentId,
        agentKey: agent.key,
        versionId: version.id,
        versionNumber: version.version,
      },
    });

    // Resolve tool bindings
    const boundTools = getToolsForBindings(version.toolBindings);
    const toolContext: ToolExecutionContext = {
      runId,
      productId: input.productId,
      agentId: input.agentId,
      userId: input.userId,
    };

    // Step 1: Prompt assembly
    if (controller.signal.aborted) throw new Error('Run cancelled');
    await runTracker.startRunStep({
      runId,
      productId: input.productId,
      stepName: 'prompt_assembly',
      order: 1,
      input: { toolCount: boundTools.length },
    });

    const assembledPrompt = assemblePrompt(
      version,
      input.input,
      boundTools.map(t => t.definition)
    );
    await runTracker.completeRunStep(runId, input.productId, 'prompt_assembly', {
      promptLength: assembledPrompt.length,
      toolsBound: boundTools.map(t => t.definition.name),
    });
    stepsCompleted++;
    emitStepEvent({ runId, stepName: 'prompt_assembly', status: 'succeeded' });

    // Step 2: Tool execution (if tools are bound)
    if (boundTools.length > 0 && controller.signal.aborted === false) {
      await runTracker.startRunStep({
        runId,
        productId: input.productId,
        stepName: 'tool_execution',
        order: 2,
      });

      const toolResults: Array<{ tool: string; result: ToolExecutionResult }> = [];
      for (const tool of boundTools) {
        if (controller.signal.aborted) break;

        // Check if tool requires review
        if (tool.definition.requiresReview) {
          emitStepEvent({
            runId,
            stepName: 'tool_execution',
            status: 'review_required',
            output: { tool: tool.definition.name },
          });

          // Mark run as needing review — caller should handle creating a review item
          await runTracker.failRunStep(
            runId,
            input.productId,
            'tool_execution',
            `Tool '${tool.definition.name}' requires human review`
          );
          await runTracker.failRun(
            runId,
            input.productId,
            `Paused: tool '${tool.definition.name}' requires review`,
            { status: 'review_required' }
          );

          activeRuns.delete(runId);
          return {
            runId,
            status: 'review_required',
            error: `Tool '${tool.definition.name}' requires human review`,
            tokenUsage,
            stepsCompleted,
            durationMs: Date.now() - startTime,
          };
        }

        try {
          const result = await tool.execute(input.input, toolContext);
          toolResults.push({ tool: tool.definition.name, result });
          if (result.tokenUsage) {
            tokenUsage.prompt += result.tokenUsage.prompt;
            tokenUsage.completion += result.tokenUsage.completion;
            tokenUsage.total += result.tokenUsage.total;
          }
        } catch (err: unknown) {
          toolResults.push({
            tool: tool.definition.name,
            result: {
              success: false,
              error: err instanceof Error ? err.message : String(err),
            },
          });
        }
      }

      const failedTools = toolResults.filter(r => !r.result.success);
      if (failedTools.length > 0 && !controller.signal.aborted) {
        await runTracker.failRunStep(
          runId,
          input.productId,
          'tool_execution',
          `${failedTools.length} tool(s) failed: ${failedTools.map(t => t.tool).join(', ')}`
        );
      } else if (!controller.signal.aborted) {
        await runTracker.completeRunStep(runId, input.productId, 'tool_execution', {
          toolResults: toolResults.map(r => ({
            tool: r.tool,
            success: r.result.success,
          })),
        });
      }

      stepsCompleted++;
      emitStepEvent({
        runId,
        stepName: 'tool_execution',
        status: failedTools.length > 0 ? 'partial_failure' : 'succeeded',
        tokenUsage: { ...tokenUsage },
      });
    }

    // Step 3: Finalize
    if (controller.signal.aborted) throw new Error('Run cancelled');
    await runTracker.startRunStep({
      runId,
      productId: input.productId,
      stepName: 'finalize',
      order: 3,
    });

    const output = {
      agentKey: agent.key,
      versionNumber: version.version,
      tokenUsage,
      stepsCompleted: stepsCompleted + 1,
    };

    await runTracker.completeRunStep(runId, input.productId, 'finalize', output);
    stepsCompleted++;
    emitStepEvent({ runId, stepName: 'finalize', status: 'succeeded', tokenUsage });

    // Complete the run
    await runTracker.completeRun(runId, input.productId, {
      ...output,
      durationMs: Date.now() - startTime,
    });

    // Emit event for downstream consumption
    try {
      await bus.emit('job.completed', {
        jobName: `agent:${agent.key}`,
        runId,
        durationMs: Date.now() - startTime,
        productId: input.productId,
      });
    } catch {
      // best-effort
    }

    activeRuns.delete(runId);
    return {
      runId,
      status: 'succeeded',
      output,
      tokenUsage,
      stepsCompleted,
      durationMs: Date.now() - startTime,
    };
  } catch (err: unknown) {
    const errorMsg = err instanceof Error ? err.message : String(err);
    const isCancelled = controller.signal.aborted;

    try {
      if (isCancelled) {
        await runTracker.failRun(runId, input.productId, 'Run cancelled by user', {
          cancelledAt: new Date().toISOString(),
        });
      } else {
        await runTracker.failRun(runId, input.productId, errorMsg);
      }
    } catch {
      // best-effort
    }

    try {
      await bus.emit('job.failed', {
        jobName: `agent:${input.agentId}`,
        runId,
        error: errorMsg,
        productId: input.productId,
      });
    } catch {
      // best-effort
    }

    activeRuns.delete(runId);
    return {
      runId,
      status: isCancelled ? 'cancelled' : 'failed',
      error: errorMsg,
      tokenUsage,
      stepsCompleted,
      durationMs: Date.now() - startTime,
    };
  }
}

// ── Prompt Assembly ──────────────────────────────────────────

function assemblePrompt(
  version: {
    prompt: string;
    systemInstructions?: string;
    modelPolicy?: { maxTokens?: number } | null;
  },
  input: Record<string, unknown>,
  tools: Array<{ name: string; description: string }>
): string {
  const parts: string[] = [];

  if (version.systemInstructions) {
    parts.push(`[System]\n${version.systemInstructions}`);
  }

  parts.push(`[Prompt]\n${version.prompt}`);

  if (tools.length > 0) {
    const toolList = tools.map(t => `- ${t.name}: ${t.description}`).join('\n');
    parts.push(`[Available Tools]\n${toolList}`);
  }

  if (Object.keys(input).length > 0) {
    parts.push(`[Input]\n${JSON.stringify(input, null, 2)}`);
  }

  return parts.join('\n\n');
}
