/**
 * nexus-agents voter agents
 *
 * Real LLM-powered voter agents for consensus voting.
 * Replaces simulated voting with actual agent execution that
 * analyzes proposals.
 *
 * (Source: Issue #226, Sprint #229)
 * (Updated: Issue #280 - Fixed timeout handling, removed simulation fallback)
 * (Refactored: Issue #285 - Extracted response and execution utilities)
 *
 * File structure:
 * - voter-prompts.ts: System prompts for each voter role
 * - voter-response.ts: Response parsing and validation
 * - voter-execution.ts: Execution utilities (timeout, retry, result creation)
 * - voter-agents.ts: Main API (this file)
 */

import type { VoterRole, AgentVoteResult } from './vote-types.js';
import { VOTER_ROLES } from './vote-types.js';
import type { IModelAdapter, ILogger } from '../core/index.js';
import { createLogger, getTimeProvider, getErrorMessage } from '../core/index.js';
import { getGlobalRegistry } from '../adapters/unified-registry.js';
import { getAvailableClis } from '../cli-adapters/factory.js';
import type { CliName } from '../cli-adapters/types.js';
import { checkCodexConcurrency } from '../cli-adapters/codex-limits.js';

// Re-export prompts for backward compatibility
export { VOTER_SYSTEM_PROMPTS, SIMULATED_VOTE_REASONING } from './voter-prompts.js';

// Re-export response utilities for backward compatibility
export {
  VoteResponseSchema,
  type VoteResponse,
  buildVotePrompt,
  extractJsonFromResponse,
  parseVoteResponse,
  SyntheticVoteError,
  type ParsedVote,
  type ParsedVoteSource,
  type ParseVoteOptions,
} from './voter-response.js';

// Re-export execution utilities for backward compatibility
export {
  DEFAULT_VOTE_TIMEOUT_MS,
  MAX_VOTE_TIMEOUT_MS,
  MIN_VOTE_TIMEOUT_MS,
  DEFAULT_MAX_RETRIES,
  RATE_LIMIT_RETRY_DELAY_MS,
  createErrorVoteResult,
  createSimulationVoteResult,
  createSimulatedVotes,
  simulateVote,
  isRateLimitError,
  withTimeout,
  delay,
  extractTextFromResponse,
  executeSingleVoteAttempt,
  resolveVoteTimeout,
  type RetryOptions,
  executeWithRetries,
} from './voter-execution.js';

// Import from execution module for internal use
import {
  createErrorVoteResult,
  createSimulationVoteResult,
  createSimulatedVotes,
  executeWithRetries,
} from './voter-execution.js';
import { resolveVoteTimeout, VOTE_TIMEOUTS, getMcpSafeDeadlineMs } from '../config/timeouts.js';
import { launchVotesWithOverallDeadline } from './voter-agents-deadline.js';

/**
 * Computes an overall wall-clock deadline for a consensus vote call (#1871).
 *
 * Acts as a safety net above per-vote timeouts: even if executeAgentVote's
 * internal withTimeout race fails to resolve (e.g. subprocess adapter hang),
 * this deadline bounds total wall time and lets partial results return.
 *
 * Formula: worst-case legitimate completion (timeoutMs * (maxRetries+1))
 * plus staggered launch headroom, plus `VOTE_TIMEOUTS.overallDeadlineBufferMs`.
 */
export function computeOverallConsensusDeadlineMs(
  timeoutMs: number,
  maxRetries: number,
  roleCount: number,
  interDelayMs: number
): number {
  const perVoteBudget = timeoutMs * (maxRetries + 1);
  const staggerBudget = Math.max(0, roleCount - 1) * interDelayMs;
  return perVoteBudget + staggerBudget + VOTE_TIMEOUTS.overallDeadlineBufferMs;
}

// ============================================================================
// Agent Vote Execution
// ============================================================================

/**
 * Options for executing voter agents.
 */
/** Default inter-agent delay to prevent rate limiting (ms). Raised from 1s to 2s (#1802). */
export const DEFAULT_INTER_AGENT_DELAY_MS = 2000;

export interface VoterAgentOptions {
  /** Logger instance */
  readonly logger?: ILogger;
  /** Model adapter to use (auto-selected if not provided) */
  readonly adapter?: IModelAdapter;
  /** Timeout per vote in milliseconds (default: 120000, override via NEXUS_VOTE_TIMEOUT_MS) */
  readonly timeoutMs?: number;
  /** Maximum retries per vote (default: 2) */
  readonly maxRetries?: number;
  /** Whether to allow simulation fallback (default: false per Issue #280) */
  readonly allowSimulation?: boolean;
  /** Delay between launching each agent vote to prevent rate limiting (default: 1000ms). Set to 0 to disable. */
  readonly interAgentDelayMs?: number;
}

// Re-export AgentVoteResult for convenience
export type { AgentVoteResult };

const defaultLogger = createLogger({ component: 'voter-agents' });

/**
 * Executes a real LLM vote for a single role with timeout and retry support.
 *
 * Per Issue #280: No simulation fallback by default. Returns error result
 * instead of simulated vote when execution fails.
 */
export async function executeAgentVote(
  role: VoterRole,
  proposal: string,
  adapter: IModelAdapter,
  logger: ILogger,
  options?: { timeoutMs?: number; maxRetries?: number; allowSimulation?: boolean }
): Promise<AgentVoteResult> {
  const start = getTimeProvider().now();
  const timeoutMs = options?.timeoutMs ?? resolveVoteTimeout();
  const maxRetries = options?.maxRetries ?? VOTE_TIMEOUTS.maxRetries;
  const allowSimulation = options?.allowSimulation ?? false;

  logger.info('Executing vote', { role, model: adapter.modelId, provider: adapter.providerId });

  const result = await executeWithRetries({
    role,
    proposal,
    adapter,
    logger,
    timeoutMs,
    maxRetries,
  });
  const processingTimeMs = getTimeProvider().now() - start;

  if (result.ok) {
    logger.info('Vote completed', { role, model: adapter.modelId, decision: result.vote.decision });
    return { role, vote: result.vote, processingTimeMs, source: 'llm', cli: adapter.providerId };
  }

  // All retries exhausted
  logger.error('Vote execution failed after all retries', undefined, {
    role,
    model: adapter.modelId,
    errorMessage: result.error,
  });

  if (allowSimulation) {
    logger.warn('Falling back to simulation (allowSimulation=true)', { role });
    return createSimulationVoteResult(role, proposal, processingTimeMs, result.error);
  }

  return createErrorVoteResult(role, result.error, processingTimeMs);
}

// ============================================================================
// Batch Vote Collection
// ============================================================================

/**
 * Options for collecting votes from multiple agents.
 */
export interface CollectRealVotesOptions extends VoterAgentOptions {
  /** Voter roles to include */
  readonly roles: readonly VoterRole[];
  /** Proposal text */
  readonly proposal: string;
  /** Use simulation mode (explicit opt-in only) */
  readonly simulate?: boolean;
}

/**
 * Error thrown when no adapter is available and simulation is disabled.
 */
export class NoAdapterError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'NoAdapterError';
  }
}

/**
 * Resolves the model adapter, handling errors per Issue #280.
 */
function resolveAdapter(
  options: CollectRealVotesOptions,
  logger: ILogger
): { adapter: IModelAdapter } | { error: string } {
  try {
    if (options.adapter !== undefined) return { adapter: options.adapter };
    const registry = getGlobalRegistry({ logger });
    return { adapter: registry.getDefault() };
  } catch (error) {
    return { error: getErrorMessage(error) };
  }
}

/**
 * #2659 — warn (don't block) when more voter roles land on Codex than its
 * default `max_threads`, e.g. a single-CLI fallback with a full panel.
 */
function warnIfCodexConcurrencyExceeded(
  roleAdapters: ReadonlyMap<VoterRole, IModelAdapter>,
  logger: ILogger
): void {
  const codexBound = [...roleAdapters.values()].filter(
    (a) => (a as { name?: string }).name === 'codex'
  ).length;
  const warning = checkCodexConcurrency(codexBound);
  if (warning !== null) {
    logger.warn('Codex concurrency limit may be exceeded', { detail: warning });
  }
}

/** Assigns a single adapter to all roles (fallback path). */
function assignUniformAdapter(
  roles: readonly VoterRole[],
  adapter: IModelAdapter
): Map<VoterRole, IModelAdapter> {
  const adapters = new Map<VoterRole, IModelAdapter>();
  for (const role of roles) adapters.set(role, adapter);
  return adapters;
}

/** Creates CLI-specific adapters for available CLIs via the unified registry. */
function createCliAdapterMap(
  clis: readonly CliName[],
  logger: ILogger
): Map<CliName, IModelAdapter> {
  const registry = getGlobalRegistry({ logger });
  const result = new Map<CliName, IModelAdapter>();
  for (const cli of clis) {
    result.set(cli, registry.getAdapterForCli(cli));
  }
  return result;
}

/**
 * Creates diverse per-role adapters using all available CLIs (Issue #845).
 * Distributes roles across CLIs in round-robin fashion for model diversity.
 * Falls back to single adapter if only one CLI is available.
 */
async function resolveDiverseAdapters(
  roles: readonly VoterRole[],
  logger: ILogger,
  fallbackAdapter: IModelAdapter
): Promise<Map<VoterRole, IModelAdapter>> {
  let availableClis: CliName[];
  try {
    availableClis = await getAvailableClis();
  } catch (e: unknown) {
    logger.warn('Failed to resolve available CLIs; falling back to single adapter', {
      error: String(e),
    });
    availableClis = [];
  }

  if (availableClis.length <= 1) {
    logger.info('Using single adapter for all roles', { cliCount: availableClis.length });
    return assignUniformAdapter(roles, fallbackAdapter);
  }

  const cliAdapters = createCliAdapterMap(availableClis, logger);
  if (cliAdapters.size <= 1) return assignUniformAdapter(roles, fallbackAdapter);

  // Round-robin assign roles to diverse CLIs
  const cliList = [...cliAdapters.entries()];
  const adapters = new Map<VoterRole, IModelAdapter>();
  const assignments: Record<string, string> = {};
  for (let i = 0; i < roles.length; i++) {
    const role = roles[i];
    const entry = cliList[i % cliList.length];
    if (role === undefined || entry === undefined) continue;
    adapters.set(role, entry[1]);
    assignments[role] = entry[0];
  }

  logger.info('Diverse adapters assigned', {
    cliCount: cliAdapters.size,
    clis: [...cliAdapters.keys()],
    roleAssignments: assignments,
  });
  return adapters;
}

/** Options for staggered vote launching. */
interface StaggeredVoteInput {
  readonly roles: readonly VoterRole[];
  readonly proposal: string;
  readonly roleAdapters: Map<VoterRole, IModelAdapter>;
  readonly fallbackAdapter: IModelAdapter;
  readonly logger: ILogger;
  readonly voteOptions: { timeoutMs: number; maxRetries: number; allowSimulation: boolean };
  readonly interDelay: number;
}

/**
 * Launches votes with staggered delays to prevent rate limiting (Issue #1319)
 * and an overall wall-clock deadline to prevent indefinite hangs (Issue #1871).
 */
async function launchStaggeredVotes(
  input: StaggeredVoteInput
): Promise<readonly AgentVoteResult[]> {
  const { roles, proposal, roleAdapters, fallbackAdapter, logger, voteOptions, interDelay } = input;
  // Raw "worst legitimate completion" estimate — retained unchanged so the
  // formula still answers "how long could this vote take in principle?".
  const computedDeadlineMs = computeOverallConsensusDeadlineMs(
    voteOptions.timeoutMs,
    voteOptions.maxRetries,
    roles.length,
    interDelay
  );
  // Clamp below the outer MCP tool-wrapper timeout. Without this, the
  // middleware kills the promise chain before launchVotesWithOverallDeadline
  // can produce structured partial results — clients see a naked timeout
  // error instead of a `source: 'error' / error: 'overall consensus deadline
  // exceeded'` vote per stuck role. (Issue #2105)
  const overallDeadlineMs = getMcpSafeDeadlineMs(computedDeadlineMs, 'consensus_vote');
  if (overallDeadlineMs < computedDeadlineMs) {
    logger.debug('Consensus deadline clamped to MCP wrapper timeout', {
      computedDeadlineMs,
      overallDeadlineMs,
    });
  }
  return launchVotesWithOverallDeadline({
    roles,
    proposal,
    roleAdapters,
    fallbackAdapter,
    logger,
    voteOptions,
    interDelay,
    overallDeadlineMs,
    voteFn: executeAgentVote,
  });
}

/**
 * Collects votes from multiple voter agents.
 *
 * Per Issue #280: No automatic simulation fallback. If no adapter is
 * available and simulation is not explicitly enabled, throws NoAdapterError.
 * Per Issue #845: Uses diverse CLIs when multiple are available.
 */
export async function collectRealVotes(
  options: CollectRealVotesOptions
): Promise<readonly AgentVoteResult[]> {
  const logger = options.logger ?? defaultLogger;
  const { roles, proposal, simulate, allowSimulation } = options;
  const timeoutMs = options.timeoutMs ?? resolveVoteTimeout();
  const maxRetries = options.maxRetries ?? VOTE_TIMEOUTS.maxRetries;

  if (simulate === true) {
    logger.info('Using simulation mode (explicitly requested)');
    return createSimulatedVotes(roles, proposal);
  }

  const adapterResult = resolveAdapter(options, logger);

  if ('error' in adapterResult) {
    logger.error('No adapter available for voting', undefined, { error: adapterResult.error });

    if (allowSimulation === true) {
      logger.warn('Falling back to simulation (allowSimulation=true)');
      return createSimulatedVotes(roles, proposal, 'No adapter available');
    }

    throw new NoAdapterError(
      `No adapter available for voting: ${adapterResult.error}. ` +
        'Install a CLI (claude/gemini/codex) or set ANTHROPIC_API_KEY.'
    );
  }

  // Per Issue #845: Use diverse adapters when no explicit adapter is provided
  const roleAdapters =
    options.adapter !== undefined
      ? assignUniformAdapter(roles, adapterResult.adapter)
      : await resolveDiverseAdapters(roles, logger, adapterResult.adapter);

  warnIfCodexConcurrencyExceeded(roleAdapters, logger);

  const voteOptions = { timeoutMs, maxRetries, allowSimulation: allowSimulation ?? false };
  const interDelay = options.interAgentDelayMs ?? DEFAULT_INTER_AGENT_DELAY_MS;

  return launchStaggeredVotes({
    roles,
    proposal,
    roleAdapters,
    fallbackAdapter: adapterResult.adapter,
    logger,
    voteOptions,
    interDelay,
  });
}

/**
 * Gets a description for a voter role.
 */
export function getRoleDescription(role: VoterRole): string {
  return VOTER_ROLES[role];
}
