import { ARBITER_DEADLOCK_THRESHOLD } from './config.js';
import { getPairedWorkspace } from './db.js';
import { isTerminalCodexAccountFailure } from './agent-error-detection.js';
import { logger } from './logger.js';
import { requestArbiterOrEscalate } from './paired-arbiter-request.js';
import {
  applyPairedTaskPatch,
  transitionPairedTaskStatus,
} from './paired-task-status.js';
import {
  resolveReviewerCompletionSignal,
  resolveReviewerFailureSignal,
} from './paired-completion-signals.js';
import { resolveCanonicalSourceRef } from './paired-source-ref.js';
import { parseReviewerVerdict } from './paired-verdict.js';
import type { PairedTask } from './types.js';

/**
 * Maximum consecutive reviewer executions that may produce no verdict
 * before we stop the ping-pong loop. Mirrors the owner-side failure cap and
 * closes the "reviewer silently fails forever" hole: codex sometimes emits a
 * final result with no visible text, the runtime marks it failed, the
 * next-turn scheduler re-queues another reviewer-turn, and the original
 * round_trip_count never increments — so the existing round-trip / arbiter
 * caps never fire.
 */
const REVIEWER_FAILURE_ESCALATION_THRESHOLD = 2;

export function handleFailedReviewerExecution(args: {
  task: PairedTask;
  taskId: string;
  summary?: string | null;
}): void {
  const { task, taskId, summary } = args;
  const now = new Date().toISOString();

  if (isTerminalCodexAccountFailure(summary)) {
    transitionPairedTaskStatus({
      taskId,
      currentStatus: task.status,
      nextStatus: 'completed',
      expectedUpdatedAt: task.updated_at,
      updatedAt: now,
      patch: {
        arbiter_verdict: 'escalate',
        arbiter_requested_at: null,
        completion_reason: 'reviewer_codex_unavailable',
      },
    });
    logger.warn(
      {
        taskId,
        role: 'reviewer',
        status: task.status,
        summary: summary?.slice(0, 200),
      },
      'Completed reviewer task after terminal Codex account failure instead of preserving review loop',
    );
    return;
  }

  if (summary) {
    const verdict = parseReviewerVerdict(summary);
    const signal = resolveReviewerFailureSignal({
      visibleVerdict: verdict,
    });
    if (
      signal.kind === 'request_owner_finalize' ||
      signal.kind === 'complete' ||
      signal.kind === 'request_owner_changes'
    ) {
      const ownerWs =
        signal.kind === 'request_owner_finalize'
          ? getPairedWorkspace(taskId, 'owner')
          : null;
      const approvedSourceRef =
        signal.kind === 'request_owner_finalize' && ownerWs?.workspace_dir
          ? resolveCanonicalSourceRef(ownerWs.workspace_dir)
          : task.source_ref;
      transitionPairedTaskStatus({
        taskId,
        currentStatus: task.status,
        nextStatus:
          signal.kind === 'request_owner_finalize'
            ? 'merge_ready'
            : signal.kind === 'request_owner_changes'
              ? 'active'
              : 'completed',
        expectedUpdatedAt: task.updated_at,
        updatedAt: now,
        patch: {
          ...(signal.kind === 'request_owner_finalize'
            ? {
                source_ref: approvedSourceRef,
                owner_failure_count: 0,
                owner_step_done_streak: 0,
                finalize_step_done_count: 0,
                empty_step_done_streak: 0,
                arbiter_verdict: null,
                arbiter_requested_at: null,
              }
            : {}),
          ...(signal.kind === 'complete'
            ? { completion_reason: signal.completionReason }
            : {}),
        },
      });
      logger.info(
        {
          taskId,
          verdict,
          approvedSourceRef,
          summary: summary.slice(0, 100),
        },
        'Reviewer verdict detected from failed execution — stopping ping-pong',
      );
      return;
    }
  }

  // Reviewer produced no usable verdict and no recognised infrastructure
  // failure. Track consecutive flakes — after the threshold, stop the
  // ping-pong loop instead of preserving review_ready forever.
  const nextFailureCount = (task.reviewer_failure_count ?? 0) + 1;

  if (nextFailureCount >= REVIEWER_FAILURE_ESCALATION_THRESHOLD) {
    requestArbiterOrEscalate({
      taskId,
      currentStatus: task.status,
      expectedUpdatedAt: task.updated_at,
      now,
      arbiterLogMessage:
        'Reviewer failed repeatedly without a visible verdict — requesting arbiter',
      escalateLogMessage:
        'Reviewer failed repeatedly without a visible verdict — escalating to user',
      logContext: {
        taskId,
        role: 'reviewer',
        previousStatus: task.status,
        reviewerFailureCount: nextFailureCount,
        summary: summary?.slice(0, 160),
      },
      patch: {
        reviewer_failure_count: nextFailureCount,
      },
    });
    return;
  }

  const fallbackStatus =
    task.status === 'in_review' || task.status === 'review_ready'
      ? 'review_ready'
      : task.status;
  if (fallbackStatus !== task.status) {
    transitionPairedTaskStatus({
      taskId,
      currentStatus: task.status,
      nextStatus: fallbackStatus,
      expectedUpdatedAt: task.updated_at,
      updatedAt: now,
      patch: {
        reviewer_failure_count: nextFailureCount,
      },
    });
  } else {
    applyPairedTaskPatch({
      taskId,
      expectedUpdatedAt: task.updated_at,
      updatedAt: now,
      patch: {
        reviewer_failure_count: nextFailureCount,
      },
    });
  }
  logger.warn(
    {
      taskId,
      role: 'reviewer',
      previousStatus: task.status,
      reviewerFailureCount: nextFailureCount,
      summary: summary?.slice(0, 160),
    },
    'Preserved reviewer task in review-ready state after failed execution',
  );
}

export function handleReviewerCompletion(args: {
  task: PairedTask;
  taskId: string;
  summary?: string | null;
}): void {
  const { task, taskId, summary } = args;
  const now = new Date().toISOString();
  const verdict = parseReviewerVerdict(summary);
  const signal = resolveReviewerCompletionSignal({
    visibleVerdict: verdict,
    roundTripCount: task.round_trip_count,
    deadlockThreshold: ARBITER_DEADLOCK_THRESHOLD,
  });

  switch (signal.kind) {
    case 'request_owner_finalize': {
      const ownerWs = getPairedWorkspace(taskId, 'owner');
      const approvedSourceRef = ownerWs?.workspace_dir
        ? resolveCanonicalSourceRef(ownerWs.workspace_dir)
        : task.source_ref;
      transitionPairedTaskStatus({
        taskId,
        currentStatus: task.status,
        nextStatus: 'merge_ready',
        expectedUpdatedAt: task.updated_at,
        updatedAt: now,
        patch: {
          source_ref: approvedSourceRef,
          owner_failure_count: 0,
          reviewer_failure_count: 0,
          owner_step_done_streak: 0,
          finalize_step_done_count: 0,
          empty_step_done_streak: 0,
          arbiter_verdict: null,
          arbiter_requested_at: null,
        },
      });
      logger.info(
        {
          taskId,
          verdict,
          approvedSourceRef,
          summary: summary?.slice(0, 100),
        },
        'Reviewer approved — owner gets final turn to finalize',
      );
      return;
    }

    case 'request_arbiter': {
      const arbiterLogMessage =
        verdict === 'blocked' || verdict === 'needs_context'
          ? 'Reviewer blocked/needs_context — requesting arbiter before escalating'
          : 'Deadlock detected — requesting arbiter intervention';
      const escalateLogMessage =
        verdict === 'blocked' || verdict === 'needs_context'
          ? 'Reviewer escalated to user — ping-pong stopped'
          : 'Stopped ping-pong — escalating to user (arbiter not configured)';
      requestArbiterOrEscalate({
        taskId,
        currentStatus: task.status,
        expectedUpdatedAt: task.updated_at,
        now,
        arbiterLogMessage,
        escalateLogMessage,
        logContext: { taskId, verdict, summary: summary?.slice(0, 100) },
        arbiterInterventionCount: task.arbiter_intervention_count ?? 0,
        patch: { reviewer_failure_count: 0 },
      });
      return;
    }

    case 'request_owner_changes':
      transitionPairedTaskStatus({
        taskId,
        currentStatus: task.status,
        nextStatus: 'active',
        expectedUpdatedAt: task.updated_at,
        updatedAt: now,
        patch: { reviewer_failure_count: 0 },
      });
      logger.info(
        { taskId, verdict },
        'Reviewer has feedback, task set back to active for owner',
      );
      return;
    default:
      return;
  }
}
