/**
 * Mock Agent Manager Adapter
 *
 * Implementation of AgentManager port for test scenarios.
 * Simulates configurable agent behaviors (success, crash, waiting_for_input)
 * without spawning real Claude agents.
 */

import { randomUUID } from 'crypto';
import type {
  AgentManager,
  AgentInfo,
  AgentMode,
  SpawnAgentOptions,
  AgentResult,
  AgentStatus,
  PendingQuestions,
  QuestionItem,
} from './types.js';
import type {
  EventBus,
  AgentSpawnedEvent,
  AgentStoppedEvent,
  AgentCrashedEvent,
  AgentResumedEvent,
  AgentDeletedEvent,
  AgentWaitingEvent,
} from '../events/index.js';
import type { AgentRepository } from '../db/repositories/agent-repository.js';

/**
 * Scenario configuration for mock agent behavior.
 * Matches the simplified agent signal schema: done, questions, or error.
 * Mode-specific stopped reasons are derived from the agent's mode.
 */
export type MockAgentScenario =
  | {
      status: 'done';
      result?: string;
      filesModified?: string[];
      delay?: number;
    }
  | {
      status: 'questions';
      questions: QuestionItem[];
      delay?: number;
    }
  | {
      status: 'error';
      error: string;
      delay?: number;
    };

/**
 * Internal agent record with scenario and timer tracking.
 */
interface MockAgentRecord {
  info: AgentInfo;
  scenario: MockAgentScenario;
  result?: AgentResult;
  pendingQuestions?: PendingQuestions;
  completionTimer?: ReturnType<typeof setTimeout>;
}

/**
 * Default scenario: immediate success with generic message.
 */
const DEFAULT_SCENARIO: MockAgentScenario = {
  status: 'done',
  result: 'Task completed successfully',
  filesModified: [],
  delay: 0,
};

/**
 * MockAgentManager - Adapter implementing AgentManager port for testing.
 *
 * Enables E2E testing of dispatch/coordination flows without spawning
 * real Claude agents. Simulates configurable agent behaviors and
 * emits proper lifecycle events.
 */
export class MockAgentManager implements AgentManager {
  private agents: Map<string, MockAgentRecord> = new Map();
  private scenarioOverrides: Map<string, MockAgentScenario> = new Map();
  private defaultScenario: MockAgentScenario;
  private eventBus?: EventBus;
  private agentRepository?: AgentRepository;

  constructor(options?: { eventBus?: EventBus; defaultScenario?: MockAgentScenario; agentRepository?: AgentRepository }) {
    this.eventBus = options?.eventBus;
    this.defaultScenario = options?.defaultScenario ?? DEFAULT_SCENARIO;
    this.agentRepository = options?.agentRepository;
  }

  /**
   * Set scenario override for a specific agent name.
   * When spawn() is called with this name, the override takes precedence.
   */
  setScenario(agentName: string, scenario: MockAgentScenario): void {
    this.scenarioOverrides.set(agentName, scenario);
  }

  /**
   * Clear scenario override for a specific agent name.
   */
  clearScenario(agentName: string): void {
    this.scenarioOverrides.delete(agentName);
  }

  /**
   * Spawn a new mock agent.
   *
   * Creates agent record in internal Map, schedules completion based on scenario.
   * Completion happens async via setTimeout (even if delay=0).
   */
  async spawn(options: SpawnAgentOptions): Promise<AgentInfo> {
    const { taskId } = options;
    const name = options.name ?? `agent-${taskId?.slice(0, 6) ?? 'noTask'}`;

    // Check name uniqueness
    for (const record of this.agents.values()) {
      if (record.info.name === name) {
        throw new Error(`Agent with name '${name}' already exists`);
      }
    }

    const sessionId = randomUUID();
    const worktreeId = randomUUID();
    const now = new Date();

    // Persist to agentRepository when provided (required for FK constraints in tests)
    let agentId: string;
    if (this.agentRepository) {
      const dbAgent = await this.agentRepository.create({
        name,
        worktreeId,
        taskId: taskId ?? null,
        initiativeId: options.initiativeId ?? null,
        sessionId,
        status: 'running',
        mode: options.mode ?? 'execute',
        provider: options.provider ?? 'claude',
        accountId: null,
      });
      agentId = dbAgent.id;
    } else {
      agentId = randomUUID();
    }

    // Determine scenario (override takes precedence — use original name or generated)
    const scenario = this.scenarioOverrides.get(name) ?? this.defaultScenario;

    const info: AgentInfo = {
      id: agentId,
      name: name ?? `mock-${agentId.slice(0, 6)}`,
      taskId: taskId ?? null,
      initiativeId: options.initiativeId ?? null,
      sessionId,
      worktreeId,
      status: 'running',
      mode: options.mode ?? 'execute',
      provider: options.provider ?? 'claude',
      accountId: null,
      createdAt: now,
      updatedAt: now,
    };

    const record: MockAgentRecord = {
      info,
      scenario,
    };

    this.agents.set(agentId, record);

    // Emit spawned event
    if (this.eventBus) {
      const event: AgentSpawnedEvent = {
        type: 'agent:spawned',
        timestamp: new Date(),
        payload: {
          agentId,
          name,
          taskId: taskId ?? null,
          worktreeId,
          provider: options.provider ?? 'claude',
        },
      };
      this.eventBus.emit(event);
    }

    // Schedule completion async (even with delay=0, uses setTimeout for async behavior)
    this.scheduleCompletion(agentId, scenario);

    return info;
  }

  /**
   * Schedule agent completion based on scenario.
   */
  private scheduleCompletion(agentId: string, scenario: MockAgentScenario): void {
    const delay = scenario.delay ?? 0;

    const timer = setTimeout(() => {
      this.completeAgent(agentId, scenario);
    }, delay);

    const record = this.agents.get(agentId);
    if (record) {
      record.completionTimer = timer;
    }
  }

  /**
   * Map agent mode to stopped event reason.
   */
  private getStoppedReason(mode: AgentMode): AgentStoppedEvent['payload']['reason'] {
    switch (mode) {
      case 'discuss': return 'context_complete';
      case 'plan': return 'plan_complete';
      case 'detail': return 'detail_complete';
      case 'refine': return 'refine_complete';
      default: return 'task_complete';
    }
  }

  /**
   * Complete agent based on scenario status.
   */
  private completeAgent(agentId: string, scenario: MockAgentScenario): void {
    const record = this.agents.get(agentId);
    if (!record) return;

    const { info } = record;

    switch (scenario.status) {
      case 'done':
        record.result = {
          success: true,
          message: scenario.result ?? 'Task completed successfully',
          filesModified: scenario.filesModified,
        };
        record.info.status = 'idle';
        record.info.updatedAt = new Date();

        if (this.eventBus) {
          const reason = this.getStoppedReason(info.mode);
          const event: AgentStoppedEvent = {
            type: 'agent:stopped',
            timestamp: new Date(),
            payload: {
              agentId,
              name: info.name,
              taskId: info.taskId,
              reason,
            },
          };
          this.eventBus.emit(event);
        }
        break;

      case 'error':
        record.result = {
          success: false,
          message: scenario.error,
        };
        record.info.status = 'crashed';
        record.info.updatedAt = new Date();

        if (this.eventBus) {
          const event: AgentCrashedEvent = {
            type: 'agent:crashed',
            timestamp: new Date(),
            payload: {
              agentId,
              name: info.name,
              taskId: info.taskId,
              error: scenario.error,
            },
          };
          this.eventBus.emit(event);
        }
        break;

      case 'questions':
        record.info.status = 'waiting_for_input';
        record.info.updatedAt = new Date();
        record.pendingQuestions = {
          questions: scenario.questions,
        };

        if (this.eventBus) {
          const event: AgentWaitingEvent = {
            type: 'agent:waiting',
            timestamp: new Date(),
            payload: {
              agentId,
              name: info.name,
              taskId: info.taskId,
              sessionId: info.sessionId ?? '',
              questions: scenario.questions,
            },
          };
          this.eventBus.emit(event);
        }
        break;
    }
  }

  /**
   * Stop a running agent.
   *
   * Cancels scheduled completion, marks agent stopped, emits agent:stopped event.
   */
  async stop(agentId: string): Promise<void> {
    const record = this.agents.get(agentId);
    if (!record) {
      throw new Error(`Agent '${agentId}' not found`);
    }

    // Cancel any pending completion
    if (record.completionTimer) {
      clearTimeout(record.completionTimer);
      record.completionTimer = undefined;
    }

    record.info.status = 'stopped';
    record.info.updatedAt = new Date();

    if (this.eventBus) {
      const event: AgentStoppedEvent = {
        type: 'agent:stopped',
        timestamp: new Date(),
        payload: {
          agentId,
          name: record.info.name,
          taskId: record.info.taskId,
          reason: 'user_requested',
        },
      };
      this.eventBus.emit(event);
    }
  }

  /**
   * Delete an agent and clean up.
   * Removes from internal map and emits agent:deleted event.
   */
  async delete(agentId: string): Promise<void> {
    const record = this.agents.get(agentId);
    if (!record) {
      throw new Error(`Agent '${agentId}' not found`);
    }

    // Cancel any pending completion
    if (record.completionTimer) {
      clearTimeout(record.completionTimer);
      record.completionTimer = undefined;
    }

    const name = record.info.name;
    this.agents.delete(agentId);

    if (this.eventBus) {
      const event: AgentDeletedEvent = {
        type: 'agent:deleted',
        timestamp: new Date(),
        payload: {
          agentId,
          name,
        },
      };
      this.eventBus.emit(event);
    }
  }

  /**
   * List all agents with their current status.
   */
  async list(): Promise<AgentInfo[]> {
    return Array.from(this.agents.values()).map((record) => record.info);
  }

  /**
   * Get a specific agent by ID.
   */
  async get(agentId: string): Promise<AgentInfo | null> {
    const record = this.agents.get(agentId);
    return record ? record.info : null;
  }

  /**
   * Get a specific agent by name.
   */
  async getByName(name: string): Promise<AgentInfo | null> {
    for (const record of this.agents.values()) {
      if (record.info.name === name) {
        return record.info;
      }
    }
    return null;
  }

  /**
   * Resume an agent that's waiting for input.
   *
   * Re-runs the scenario for the resumed agent. Emits agent:resumed event.
   * Agent must be in 'waiting_for_input' status.
   *
   * @param agentId - Agent to resume
   * @param answers - Map of question ID to user's answer
   */
  async resume(agentId: string, answers: Record<string, string>): Promise<void> {
    const record = this.agents.get(agentId);
    if (!record) {
      throw new Error(`Agent '${agentId}' not found`);
    }

    if (record.info.status !== 'waiting_for_input') {
      throw new Error(
        `Agent '${record.info.name}' is not waiting for input (status: ${record.info.status})`
      );
    }

    if (!record.info.sessionId) {
      throw new Error(`Agent '${record.info.name}' has no session to resume`);
    }

    // Update status to running, clear pending questions
    record.info.status = 'running';
    record.info.updatedAt = new Date();
    record.pendingQuestions = undefined;

    // Emit resumed event
    if (this.eventBus) {
      const event: AgentResumedEvent = {
        type: 'agent:resumed',
        timestamp: new Date(),
        payload: {
          agentId,
          name: record.info.name,
          taskId: record.info.taskId,
          sessionId: record.info.sessionId,
        },
      };
      this.eventBus.emit(event);
    }

    // Re-run scenario (after resume, typically completes successfully)
    // For testing, we use a new scenario that defaults to success
    // Extract filesModified from original scenario if it was a 'done' type
    const originalFilesModified =
      record.scenario.status === 'done' ? record.scenario.filesModified : undefined;
    const resumeScenario: MockAgentScenario = {
      status: 'done',
      delay: record.scenario.delay ?? 0,
      result: 'Resumed and completed successfully',
      filesModified: originalFilesModified,
    };

    this.scheduleCompletion(agentId, resumeScenario);
  }

  /**
   * Get the result of an agent's work.
   *
   * Only available after agent completes or crashes.
   */
  async getResult(agentId: string): Promise<AgentResult | null> {
    const record = this.agents.get(agentId);
    return record?.result ?? null;
  }

  /**
   * Get pending questions for an agent waiting for input.
   */
  async getPendingQuestions(agentId: string): Promise<PendingQuestions | null> {
    const record = this.agents.get(agentId);
    return record?.pendingQuestions ?? null;
  }

  /**
   * Dismiss an agent.
   * Mock implementation just marks the agent as dismissed.
   */
  async dismiss(agentId: string): Promise<void> {
    const record = this.agents.get(agentId);
    if (!record) {
      throw new Error(`Agent '${agentId}' not found`);
    }
    const now = new Date();
    record.info.userDismissedAt = now;
    record.info.updatedAt = now;
  }

  /**
   * Resume an idle agent to answer an inter-agent conversation.
   * Mock implementation: marks agent as running and schedules immediate completion.
   */
  async resumeForConversation(
    agentId: string,
    conversationId: string,
    question: string,
    fromAgentId: string,
  ): Promise<boolean> {
    const record = this.agents.get(agentId);
    if (!record || record.info.status !== 'idle' || !record.info.sessionId) {
      return false;
    }

    record.info.status = 'running';
    record.info.updatedAt = new Date();

    if (this.eventBus) {
      const event: AgentResumedEvent = {
        type: 'agent:resumed',
        timestamp: new Date(),
        payload: {
          agentId,
          name: record.info.name,
          taskId: record.info.taskId,
          sessionId: record.info.sessionId,
        },
      };
      this.eventBus.emit(event);
    }

    this.scheduleCompletion(agentId, { status: 'done', delay: 0, result: 'Answered conversation' });
    return true;
  }

  /**
   * Deliver a user message to a running errand agent.
   * Mock implementation: no-op (simulates message delivery without actual process interaction).
   */
  async sendUserMessage(agentId: string, _message: string): Promise<void> {
    const record = this.agents.get(agentId);
    if (!record) {
      throw new Error(`Agent '${agentId}' not found`);
    }
    // Mock: succeed silently — message delivery is a no-op in tests
  }

  /**
   * Clear all agents and pending timers.
   * Useful for test cleanup.
   */
  clear(): void {
    for (const record of this.agents.values()) {
      if (record.completionTimer) {
        clearTimeout(record.completionTimer);
      }
    }
    this.agents.clear();
    this.scenarioOverrides.clear();
  }
}
