/**
 * Agent Error Detection (SSOT)
 *
 * Single source of truth for classifying agent errors from output text
 * and error strings. Used by both message-agent-executor and task-scheduler.
 */

// ── Banner / text detection ─────────────────────────────────────

export function isClaudeAuthError(text: string): boolean {
  const lower = text.toLowerCase();
  return (
    lower.includes('failed to authenticate') &&
    (lower.includes('401') || lower.includes('authentication_error'))
  );
}

export function isClaudeUsageExhaustedMessage(text: string): boolean {
  const normalized = text
    .trim()
    .toLowerCase()
    .replace(/['\u2018\u2019`]/g, "'")
    .replace(/\s+/g, ' ')
    .replace(/^error:\s*/i, '');
  const looksLikeBanner =
    normalized.startsWith("you're out of extra usage") ||
    normalized.startsWith('you are out of extra usage') ||
    normalized.startsWith("you've hit your limit") ||
    normalized.startsWith('you have hit your limit');
  const hasResetHint =
    normalized.includes('resets ') ||
    normalized.includes('reset at ') ||
    normalized.includes('try again');
  return looksLikeBanner && hasResetHint && normalized.length <= 160;
}

export function isClaudeAuthExpiredMessage(text: string): boolean {
  const normalized = text.trim().toLowerCase().replace(/\s+/g, ' ');
  const looksLikeAuthFailure = normalized.startsWith('failed to authenticate');
  const hasExpiredTokenMarker =
    normalized.includes('oauth token has expired') ||
    normalized.includes('authentication_error') ||
    normalized.includes('obtain a new token') ||
    normalized.includes('refresh your existing token') ||
    normalized.includes('invalid authentication credentials');
  const hasUnauthorizedMarker =
    normalized.includes('401') || normalized.includes('authentication error');
  const hasTerminatedMarker = normalized.includes('terminated');

  return (
    looksLikeAuthFailure &&
    hasUnauthorizedMarker &&
    (hasExpiredTokenMarker || hasTerminatedMarker)
  );
}

export function detectClaudeProviderFailureMessage(
  text: string,
): Extract<AgentTriggerReason, '429' | 'overloaded' | 'network-error'> | '' {
  const normalized = text.trim().toLowerCase().replace(/\s+/g, ' ');
  const looksLikeProviderError =
    normalized.startsWith('api error:') ||
    normalized.startsWith('error: api error:') ||
    normalized.startsWith('network error') ||
    normalized.startsWith('fetch failed');

  if (!looksLikeProviderError) {
    return '';
  }

  const classification = classifyAgentError(text);
  if (
    classification.category === 'rate-limit' ||
    classification.category === 'overloaded' ||
    classification.category === 'network-error'
  ) {
    return classification.reason;
  }

  return '';
}

export function isClaudeOrgAccessDeniedMessage(text: string): boolean {
  const normalized = text.trim().toLowerCase().replace(/\s+/g, ' ');
  const hasOrgAccessDeniedMarker = normalized.includes(
    'does not have access to claude',
  );
  const hasRecoveryHint =
    normalized.includes('please login again') ||
    normalized.includes('contact your administrator');

  return hasOrgAccessDeniedMarker && hasRecoveryHint;
}

// ── Rotation decision ───────────────────────────────────────────

export type AgentTriggerReason =
  | '429'
  | 'usage-exhausted'
  | 'auth-expired'
  | 'org-access-denied'
  | 'session-failure'
  | 'overloaded'
  | 'network-error'
  | 'success-null-result'
  | 'session-failure';

export type ClaudeRotationReason = Extract<
  AgentTriggerReason,
  '429' | 'usage-exhausted' | 'auth-expired' | 'org-access-denied'
>;

export type CodexRotationReason = Extract<
  AgentTriggerReason,
  '429' | 'auth-expired' | 'org-access-denied' | 'overloaded' | 'network-error'
>;

export function shouldRotateClaudeToken(
  reason: AgentTriggerReason,
): reason is ClaudeRotationReason {
  return (
    reason === '429' ||
    reason === 'usage-exhausted' ||
    reason === 'auth-expired' ||
    reason === 'org-access-denied'
  );
}

export function isCodexRotationReason(
  reason: AgentTriggerReason,
): reason is CodexRotationReason {
  return (
    reason === '429' ||
    reason === 'auth-expired' ||
    reason === 'org-access-denied' ||
    reason === 'overloaded' ||
    reason === 'network-error'
  );
}

// ── Rotation trigger classification ─────────────────────────────

export type RotationTriggerResult =
  | { shouldRetry: false; reason: '' }
  | {
      shouldRetry: true;
      reason: AgentTriggerReason;
      retryAfterMs?: number;
    };

/**
 * Classify an error string to determine if Claude token rotation
 * should be attempted.
 *
 * Priority: 429 > auth/org errors > 503/network.
 */
export function classifyRotationTrigger(
  error?: string | null,
): RotationTriggerResult {
  if (!error) return { shouldRetry: false, reason: '' };

  const common = classifyAgentError(error);

  // 429 rate-limit (highest priority)
  if (common.category === 'rate-limit') {
    return {
      shouldRetry: true,
      reason: common.reason,
      retryAfterMs: common.retryAfterMs,
    };
  }

  // Claude-specific auth checks (before 503/network)
  const auth = classifyClaudeAuthError(error);
  if (auth.category !== 'none') {
    return { shouldRetry: true, reason: auth.reason };
  }

  // 503 overloaded, network errors
  if (common.category !== 'none') {
    return { shouldRetry: true, reason: common.reason };
  }

  return { shouldRetry: false, reason: '' };
}

// ── Unified error classification ────────────────────────────────

export type ErrorCategory =
  | 'rate-limit'
  | 'auth-expired'
  | 'org-access-denied'
  | 'overloaded'
  | 'network-error'
  | 'none';

export type AgentErrorClassification =
  | {
      category: 'none';
      reason: '';
      retryAfterMs?: undefined;
    }
  | {
      category: 'rate-limit';
      reason: '429';
      retryAfterMs?: number;
    }
  | {
      category: 'auth-expired';
      reason: 'auth-expired';
      retryAfterMs?: undefined;
    }
  | {
      category: 'org-access-denied';
      reason: 'org-access-denied';
      retryAfterMs?: undefined;
    }
  | {
      category: 'overloaded';
      reason: 'overloaded';
      retryAfterMs?: undefined;
    }
  | {
      category: 'network-error';
      reason: 'network-error';
      retryAfterMs?: undefined;
    };

const NONE: AgentErrorClassification = {
  category: 'none',
  reason: '',
};

export function isCodexPoolUnavailableError(
  error: string | null | undefined,
): boolean {
  if (!error) return false;
  return (
    /all\s+codex(?:\s+rotation)?\s+accounts(?:\s+are)?\s+unavailable/i.test(
      error,
    ) || /codex\s+rotation\s+pool\s+unavailable/i.test(error)
  );
}

export function isTerminalCodexAccountFailure(
  error: string | null | undefined,
): boolean {
  if (!error) return false;
  if (isCodexPoolUnavailableError(error)) return true;
  if (classifyCodexAuthError(error).category !== 'none') return true;
  const lower = error.toLowerCase();
  if (
    classifyAgentError(error).category === 'rate-limit' &&
    (lower.includes('workspace out of credits') ||
      lower.includes('out of credits') ||
      lower.includes('codex'))
  ) {
    return true;
  }
  return false;
}

/**
 * Classify an agent error string into a category.
 * Handles patterns common to both Claude and Codex: 429, 503, network.
 * Auth errors are provider-specific — use classifyClaudeAuthError or
 * classifyCodexAuthError for those.
 */
export function classifyAgentError(
  error: string | null | undefined,
): AgentErrorClassification {
  if (!error) return NONE;

  const lower = error.toLowerCase();

  // 429 / Rate Limit
  if (
    lower.includes('429') ||
    lower.includes('rate limit') ||
    lower.includes('usage limit') ||
    lower.includes('out of credits') ||
    lower.includes('workspace out of credits') ||
    lower.includes('hit your limit') ||
    lower.includes('too many requests') ||
    lower.includes('rate_limit')
  ) {
    const retryMatch = error.match(/retry[\s_-]*after[:\s]*(\d+)/i);
    const retryAfterMs = retryMatch
      ? parseInt(retryMatch[1], 10) * 1000
      : undefined;
    return { category: 'rate-limit', reason: '429', retryAfterMs };
  }

  // 503 / Overloaded
  if (
    lower.includes('503') ||
    lower.includes('overloaded') ||
    lower.includes('selected model is at capacity') ||
    lower.includes('model is at capacity') ||
    ((lower.includes('502') || lower.includes('bad gateway')) &&
      (lower.includes('cloudflare') ||
        lower.includes('<html') ||
        lower.includes('api error')))
  ) {
    return { category: 'overloaded', reason: 'overloaded' };
  }

  // Network / connection errors
  if (
    lower.includes('econnrefused') ||
    lower.includes('econnreset') ||
    lower.includes('etimedout') ||
    lower.includes('enotfound') ||
    lower.includes('fetch failed') ||
    lower.includes('network error')
  ) {
    return { category: 'network-error', reason: 'network-error' };
  }

  return NONE;
}

// ── Provider-specific auth checks ───────────────────────────────

/** Claude auth: strict 3-condition AND (auth failure + 401 + specific marker). */
export function classifyClaudeAuthError(
  error: string | null | undefined,
): AgentErrorClassification {
  if (!error) return NONE;
  const lower = error.toLowerCase();

  const hasOrgAccessDeniedMarker =
    lower.includes('your organization does not have access to claude') ||
    (lower.includes('does not have access to claude') &&
      lower.includes('contact your administrator'));
  const hasTerminated403AuthFailure =
    lower.includes('failed to authenticate') &&
    lower.includes('403') &&
    lower.includes('terminated');

  if (hasOrgAccessDeniedMarker || hasTerminated403AuthFailure) {
    return { category: 'org-access-denied', reason: 'org-access-denied' };
  }

  if (
    (lower.includes('failed to authenticate') ||
      lower.includes('authentication_error')) &&
    (lower.includes('401') || lower.includes('unauthorized')) &&
    (lower.includes('oauth token has expired') ||
      lower.includes('obtain a new token') ||
      lower.includes('refresh your existing token') ||
      lower.includes('invalid authentication credentials') ||
      lower.includes('terminated'))
  ) {
    return { category: 'auth-expired', reason: 'auth-expired' };
  }

  return NONE;
}

/** Codex auth: loose OR check (any single auth indicator). */
export function classifyCodexAuthError(
  error: string | null | undefined,
): AgentErrorClassification {
  if (!error) return NONE;
  if (isCodexPoolUnavailableError(error)) return NONE;
  const lower = error.toLowerCase();

  if (
    lower.includes('auth-expired') ||
    lower.includes('auth expired') ||
    lower.includes('401') ||
    lower.includes('authentication_error') ||
    lower.includes('failed to authenticate') ||
    lower.includes('access token could not be refreshed') ||
    lower.includes('oauth token has expired') ||
    lower.includes('refresh token was already used') ||
    lower.includes('refresh your existing token') ||
    lower.includes('log out and sign in again') ||
    lower.includes('app_session_terminated') ||
    lower.includes('unauthorized')
  ) {
    return { category: 'auth-expired', reason: 'auth-expired' };
  }

  return NONE;
}
