/**
 * Node output reference resolution for the Ion workflow engine.
 *
 * Resolves `$nodeId.field` references in workflow conditions and prompts,
 * with strict schema-aware validation and descriptive errors.
 */

export type OutputRefKind = 'value' | 'empty';

export interface OutputRefResult {
  /** Whether the field had a value or was empty. */
  kind: OutputRefKind;
  /** The resolved value (empty string for missing optional fields). */
  value: string;
}

export class OutputRefError extends Error {
  public readonly nodeId: string;
  public readonly field: string;

  constructor(nodeId: string, field: string, message: string) {
    super(`Output reference error for node "${nodeId}".${field}: ${message}`);
    this.name = 'OutputRefError';
    this.nodeId = nodeId;
    this.field = field;
  }
}

/**
 * Extract declared field names from an output_format schema.
 *
 * The output_format can be:
 * - A JSON Schema object with `properties` (standard)
 * - A string describing the format (treated as having no declared fields)
 * - Undefined (no schema)
 *
 * Returns a Set of field names that are declared in the schema.
 */
export function declaredFieldsFromSchema(
  outputFormat: Record<string, unknown> | string | undefined,
): Set<string> {
  if (!outputFormat || typeof outputFormat === 'string') {
    return new Set();
  }

  const properties = outputFormat['properties'];
  if (properties && typeof properties === 'object' && properties !== null) {
    return new Set(Object.keys(properties as Record<string, unknown>));
  }

  return new Set();
}

/**
 * Resolve a specific field from a node's output.
 *
 * Behavior:
 * - If the field is present in the output, returns `{ kind: 'value', value }`.
 * - If the field is declared in the schema but not present in the output,
 *   returns `{ kind: 'empty', value: '' }` (optional field not set).
 * - If the field is NOT declared in the schema AND not in the output,
 *   throws `OutputRefError` (undeclared reference).
 * - If the field is NOT declared in the schema but IS in the output,
 *   returns `{ kind: 'value', value }` (dynamic output).
 *
 * The `declaredFields` parameter should come from `declaredFieldsFromSchema()`.
 */
export function resolveNodeOutputField(
  nodeOutput: Record<string, unknown>,
  nodeId: string,
  field: string,
  declaredFields?: Set<string>,
): OutputRefResult {
  // Check if the field exists in the output.
  if (field in nodeOutput) {
    const rawValue = nodeOutput[field];

    // Convert the value to a string.
    if (rawValue === null || rawValue === undefined) {
      // Field key exists but value is nullish — treat as empty.
      return { kind: 'empty', value: '' };
    }

    if (typeof rawValue === 'string') {
      return { kind: 'value', value: rawValue };
    }

    // Non-string values are JSON-serialized.
    return { kind: 'value', value: JSON.stringify(rawValue) };
  }

  // Field is not in the output. Check if it was declared in the schema.
  const isDeclared = declaredFields?.has(field) ?? false;

  if (isDeclared) {
    // Declared but not present — optional field not set.
    return { kind: 'empty', value: '' };
  }

  // Not declared and not present — this is an error.
  throw new OutputRefError(
    nodeId,
    field,
    `Field "${field}" is not declared in the output schema and is not present in the node output. Available fields: ${Object.keys(nodeOutput).join(', ') || '(none)'}`,
  );
}