/**
 * Shared executor utilities for the Ion workflow engine.
 *
 * Pure functions for variable substitution, error classification,
 * message helpers, and completion signal detection.
 */

import type { IWorkflowPlatform } from './deps.js';

/** Well-known workflow variable names. */
const WORKFLOW_VARIABLES = [
  '$WORKFLOW_ID',
  '$USER_MESSAGE',
  '$ARGUMENTS',
  '$ARTIFACTS_DIR',
  '$BASE_BRANCH',
  '$DOCS_DIR',
  '$CONTEXT',
  '$EXTERNAL_CONTEXT',
  '$LOOP_USER_INPUT',
  '$REJECTION_REASON',
] as const;

export type WorkflowVariableName = (typeof WORKFLOW_VARIABLES)[number];

/** Context object providing values for workflow variable substitution. */
export interface VariableContext {
  WORKFLOW_ID?: string;
  USER_MESSAGE?: string;
  ARGUMENTS?: string;
  ARTIFACTS_DIR?: string;
  BASE_BRANCH?: string;
  DOCS_DIR?: string;
  CONTEXT?: string;
  EXTERNAL_CONTEXT?: string;
  LOOP_USER_INPUT?: string;
  REJECTION_REASON?: string;
}

/**
 * Substitute workflow variables in a string.
 *
 * Replaces known `$VARIABLE` tokens with values from the context.
 * Unknown `$VARIABLE` tokens are left as-is (not removed).
 */
export function substituteWorkflowVariables(
  template: string,
  context: VariableContext,
): string {
  let result = template;

  // Map variable names to their context keys.
  const mapping: Record<string, string | undefined> = {
    $WORKFLOW_ID: context.WORKFLOW_ID,
    $USER_MESSAGE: context.USER_MESSAGE,
    $ARGUMENTS: context.ARGUMENTS,
    $ARTIFACTS_DIR: context.ARTIFACTS_DIR,
    $BASE_BRANCH: context.BASE_BRANCH,
    $DOCS_DIR: context.DOCS_DIR,
    $CONTEXT: context.CONTEXT,
    $EXTERNAL_CONTEXT: context.EXTERNAL_CONTEXT,
    $LOOP_USER_INPUT: context.LOOP_USER_INPUT,
    $REJECTION_REASON: context.REJECTION_REASON,
  };

  for (const [variable, value] of Object.entries(mapping)) {
    if (value !== undefined) {
      // Replace all occurrences of the variable.
      result = result.split(variable).join(value);
    }
  }

  return result;
}

/**
 * Build a prompt by substituting workflow variables and appending
 * issue context if provided.
 */
export function buildPromptWithContext(
  template: string,
  context: VariableContext,
  issueContext?: string,
): string {
  let prompt = substituteWorkflowVariables(template, context);

  if (issueContext && issueContext.length > 0) {
    prompt += `\n\n---\nIssue Context:\n${issueContext}`;
  }

  return prompt;
}

export type ErrorClassification = 'FATAL' | 'TRANSIENT' | 'UNKNOWN';

/** Patterns that indicate a fatal (non-retryable) error. */
const FATAL_PATTERNS: readonly RegExp[] = [
  /auth(?:entication|orization)?\s*(?:fail|error|denied|invalid)/i,
  /permission\s*(?:denied|error|fail)/i,
  /insufficient\s*(?:credit|quota|permission)/i,
  /api\s*key\s*(?:invalid|expired|missing)/i,
  /forbidden/i,
  /unauthorized/i,
  /account\s*(?:suspended|disabled|deactivated)/i,
];

/** Patterns that indicate a transient (retryable) error. */
const TRANSIENT_PATTERNS: readonly RegExp[] = [
  /timeout/i,
  /timed?\s*out/i,
  /network\s*(?:error|fail|unreachable)/i,
  /connection\s*(?:reset|refused|dropped|lost)/i,
  /econnreset/i,
  /econnrefused/i,
  /rate\s*limit/i,
  /too\s*many\s*requests/i,
  /service\s*(?:unavailable|temporarily\s*unavailable)/i,
  /503/i,
  /502/i,
  /retry/i,
];

/**
 * Classify an error as FATAL, TRANSIENT, or UNKNOWN based on its message.
 *
 * FATAL errors should not be retried (auth, permission, credit issues).
 * TRANSIENT errors may succeed on retry (network, timeout, rate limit).
 * UNKNOWN errors have no recognized pattern.
 */
export function classifyError(error: Error | string): ErrorClassification {
  const message = typeof error === 'string' ? error : error.message;

  for (const pattern of FATAL_PATTERNS) {
    if (pattern.test(message)) {
      return 'FATAL';
    }
  }

  for (const pattern of TRANSIENT_PATTERNS) {
    if (pattern.test(message)) {
      return 'TRANSIENT';
    }
  }

  return 'UNKNOWN';
}

/**
 * Safely send a message via the platform interface.
 *
 * Returns `true` if the message was sent successfully, `false` otherwise.
 * Logs failures but does not throw.
 */
export async function safeSendMessage(
  platform: IWorkflowPlatform,
  conversationId: string,
  message: string,
  metadata?: Record<string, unknown>,
): Promise<boolean> {
  try {
    await platform.sendMessage(conversationId, message, metadata);
    return true;
  } catch {
    return false;
  }
}

/**
 * Detect whether an output contains the expected completion signal.
 *
 * The `until` string is the marker that signals the node has finished
 * producing its output. Returns `true` if the marker is found.
 */
export function detectCompletionSignal(
  output: string,
  until: string,
): boolean {
  if (!until || until.length === 0) {
    // No completion marker configured — always consider complete.
    return true;
  }
  return output.includes(until);
}

/**
 * Strip the completion marker from the output.
 *
 * Removes the `until` string from the output if present.
 * Does not trim or modify whitespace beyond removing the marker.
 */
export function stripCompletionTags(output: string, until: string): string {
  if (!until || until.length === 0) {
    return output;
  }
  return output.split(until).join('');
}

export interface SubprocessFailure {
  exitCode: number | null;
  stderr: string;
  command?: string;
}

/**
 * Format a subprocess failure into a human-readable error string.
 *
 * Includes the command (if provided), exit code, and stderr output.
 */
export function formatSubprocessFailure(failure: SubprocessFailure): string {
  const parts: string[] = [];

  if (failure.command) {
    parts.push(`Command: ${failure.command}`);
  }

  if (failure.exitCode !== null) {
    parts.push(`Exit code: ${failure.exitCode}`);
  }

  if (failure.stderr && failure.stderr.length > 0) {
    // Truncate very long stderr to keep error messages manageable.
    const maxStderr = 2048;
    const stderr =
      failure.stderr.length > maxStderr
        ? failure.stderr.slice(0, maxStderr) + '... (truncated)'
        : failure.stderr;
    parts.push(`Stderr:\n${stderr}`);
  }

  return parts.length > 0 ? parts.join('\n') : 'Subprocess failed with no details';
}