// Copyright 2026 CRMy Contributors
// SPDX-License-Identifier: Apache-2.0

/**
 * Sequence execution engine.
 *
 * Runs on the 60-second background tick. Fetches due enrollments, dispatches
 * each step by type, logs the result, and advances or completes the enrollment.
 *
 * Also handles:
 *   - Reply detection  → exit enrollment on inbound email reply
 *   - Goal-event exit  → complete enrollment when a goal event fires
 *   - AI generation    → LLM-drafted step content with optional HITL gate
 *   - Branch steps     → conditional jumps based on triggers
 */

import type { DbPool } from '../db/pool.js';
import type { ActionContext, ActorContext, UUID } from '@crmy/shared';
import { emitEvent } from '../events/emitter.js';
import * as seqRepo from '../db/repos/email-sequences.js';
import * as emailRepo from '../db/repos/emails.js';
import * as contactRepo from '../db/repos/contacts.js';
import * as assignmentRepo from '../db/repos/assignments.js';
import * as activityRepo from '../db/repos/activities.js';
import { deliverEmail } from '../email/delivery.js';
import { interpolate, buildVariableContext } from '../workflows/variables.js';
import { callLLM, requireTenantLLMConfig } from '../agent/providers/llm.js';
import { triggerExtraction } from '../agent/extraction.js';
import { getActionContext } from './action-context.js';

// ── Types ──────────────────────────────────────────────────────────────────────

interface EmailStep {
  type?: 'email';
  delay_days: number;
  delay_hours?: number;
  subject: string;
  body_text?: string;
  body_html?: string;
  require_approval?: boolean;
  ai_generate?: boolean;
  ai_prompt?: string;
}

interface NotificationStep {
  type: 'notification';
  delay_days: number;
  channel_id?: string;
  message: string;
}

interface TaskStep {
  type: 'task';
  delay_days: number;
  title: string;
  description?: string;
  assign_to?: string;
  priority?: 'low' | 'normal' | 'high';
}

interface WebhookStep {
  type: 'webhook';
  delay_days: number;
  url: string;
  method?: 'POST' | 'GET';
  headers?: Record<string, string>;
  body_template?: string;
}

interface WaitStep {
  type: 'wait';
  delay_days: number;
  condition?: {
    event: string;
    timeout_days: number;
    timeout_branch?: number;
  };
}

interface BranchStep {
  type: 'branch';
  delay_days?: number;
  conditions: Array<{
    trigger: 'replied' | 'opened' | 'clicked' | 'goal_met' | 'custom_event';
    event?: string;
    jump_to_step?: number;
    exit?: boolean;
  }>;
}

interface AiActionStep {
  type: 'ai_action';
  delay_days: number;
  prompt: string;
  tool_names?: string[];
  require_approval?: boolean;
}

type SequenceStep = EmailStep | NotificationStep | TaskStep | WebhookStep | WaitStep | BranchStep | AiActionStep;

function sequenceActionActor(tenantId: UUID): ActorContext {
  return {
    tenant_id: tenantId,
    actor_id: 'sequence',
    actor_type: 'system',
    role: 'owner',
    scopes: ['read', 'write', 'context:read', 'activities:write'],
  };
}

function summarizeSequenceActionContext(actionContext: ActionContext) {
  return {
    subject_type: actionContext.subject_type,
    subject_id: actionContext.subject_id,
    operating_mode: actionContext.operating_mode,
    readiness_status: actionContext.readiness.status,
    risk_level: actionContext.readiness.risk_level,
    review_required: actionContext.readiness.review_required,
    guidance_summary: actionContext.guidance.summary,
    warning_reasons: actionContext.guidance.warning_reasons,
    review_reasons: actionContext.guidance.review_reasons,
    proof: actionContext.proof,
  };
}

async function getSequenceEmailActionContext(
  db: DbPool,
  tenantId: UUID,
  enrollment: { contact_id: UUID; id: UUID; sequence_id: UUID },
  sequence: { name: string },
  stepIndex: number,
  subject: string,
  toEmail: string,
): Promise<Record<string, unknown> | undefined> {
  const actionContext = await getActionContext(db, sequenceActionActor(tenantId), {
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    context_radius: 'adjacent',
    proposed_action: {
      action_type: 'customer_outreach',
      object_type: 'contact',
      payload: {
        to_address: toEmail,
        subject,
        sequence_name: sequence.name,
        enrollment_id: enrollment.id,
        sequence_id: enrollment.sequence_id,
        step_index: stepIndex,
      },
    },
  }).catch(() => null);
  return actionContext ? summarizeSequenceActionContext(actionContext) : undefined;
}

async function getSequenceStepActionContext(
  db: DbPool,
  tenantId: UUID,
  enrollment: { contact_id: UUID; id: UUID; sequence_id: UUID },
  sequence: { name: string },
  stepIndex: number,
  stepType: string,
  payload: Record<string, unknown> = {},
): Promise<Record<string, unknown> | undefined> {
  const actionContext = await getActionContext(db, sequenceActionActor(tenantId), {
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    context_radius: 'adjacent',
    proposed_action: {
      action_type: stepType === 'task' ? 'assignment_create' : 'sequence_step',
      object_type: 'contact',
      payload: {
        sequence_name: sequence.name,
        enrollment_id: enrollment.id,
        sequence_id: enrollment.sequence_id,
        step_index: stepIndex,
        step_type: stepType,
        ...payload,
      },
    },
  }).catch(() => null);
  return actionContext ? summarizeSequenceActionContext(actionContext) : undefined;
}

// ── Main pump ──────────────────────────────────────────────────────────────────

const inFlightEnrollments = new Set<string>();

/** Called from the 60-second background tick in index.ts */
export async function processSequenceDue(db: DbPool): Promise<void> {
  const enrollments = await seqRepo.getDueEnrollments(db, 50);
  for (const enrollment of enrollments) {
    if (inFlightEnrollments.has(enrollment.id)) continue;
    inFlightEnrollments.add(enrollment.id);
    try {
      await processEnrollment(db, enrollment);
    } catch (err) {
      console.error(`[sequences] enrollment ${enrollment.id} failed:`, err);
      // Log failure but keep processing other enrollments
      await seqRepo.logStepExecution(db, {
        enrollment_id: enrollment.id,
        tenant_id: enrollment.tenant_id,
        step_index: enrollment.current_step,
        step_type: 'unknown',
        status: 'failed',
        error: err instanceof Error ? err.message : String(err),
      });
    } finally {
      inFlightEnrollments.delete(enrollment.id);
    }
  }
}

async function processEnrollment(db: DbPool, enrollment: Awaited<ReturnType<typeof seqRepo.getDueEnrollments>>[number]): Promise<void> {
  const sequence = await seqRepo.getSequence(db, enrollment.tenant_id, enrollment.sequence_id);
  if (!sequence || !sequence.is_active) {
    await seqRepo.cancelEnrollment(db, enrollment.id, 'sequence_inactive');
    return;
  }

  const steps = (sequence.steps as SequenceStep[]);
  const stepIndex = enrollment.current_step;

  if (stepIndex >= steps.length) {
    await seqRepo.completeEnrollment(db, enrollment.id, 'completed');
    return;
  }

  const step = steps[stepIndex];
  const stepType = (step as { type?: string }).type ?? 'email';

  await executeStep(db, enrollment, sequence, step, stepIndex);
}

async function executeStep(
  db: DbPool,
  enrollment: Awaited<ReturnType<typeof seqRepo.getDueEnrollments>>[number],
  sequence: seqRepo.SequenceRow,
  step: SequenceStep,
  stepIndex: number,
): Promise<void> {
  const stepType = (step as { type?: string }).type ?? 'email';
  const stepEventPayload = {
    enrollment_id: enrollment.id,
    sequence_id: sequence.id,
    step_index: stepIndex,
    step_type: stepType,
  };
  await emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step.started',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: stepEventPayload,
  });
  let skippedStep = false;

  // Build variable context from contact data
  const contact = await contactRepo.getContact(db, enrollment.tenant_id, enrollment.contact_id);
  const contactPayload = contact ? {
    first_name: (contact as any).first_name,
    last_name: (contact as any).last_name,
    email: (contact as any).email,
    title: (contact as any).title,
    company_name: (contact as any).company_name,
    lifecycle_stage: (contact as any).lifecycle_stage,
  } : {};

  const varContext = {
    ...buildVariableContext({ ...contactPayload, ...((enrollment as any).variables ?? {}) }),
    contact: { ...contactPayload, ...((enrollment as any).variables ?? {}) },
    enrollment: {
      step: stepIndex,
      sequence_name: sequence.name,
      ...(enrollment as any).variables,
    },
  };

  try {
    switch (stepType) {
      case 'email':
        await executeEmailStep(db, enrollment, sequence, step as EmailStep, stepIndex, varContext, contact);
        break;
      case 'notification':
        await executeNotificationStep(db, enrollment, sequence, step as NotificationStep, stepIndex, varContext);
        break;
      case 'task':
        await executeTaskStep(db, enrollment, sequence, step as TaskStep, stepIndex, varContext);
        break;
      case 'webhook':
        await executeWebhookStep(db, enrollment, sequence, step as WebhookStep, stepIndex, varContext);
        break;
      case 'wait':
        await executeWaitStep(db, enrollment, step as WaitStep, stepIndex);
        await emitEvent(db, {
          tenantId: enrollment.tenant_id,
          eventType: 'sequence.step.completed',
          actorType: 'system',
          objectType: 'sequence_enrollment',
          objectId: enrollment.id,
          afterData: { ...stepEventPayload, next_step: null },
        });
        return; // Wait steps reschedule without advancing
      case 'branch':
        await executeBranchStep(db, enrollment, sequence, step as BranchStep, stepIndex);
        await emitEvent(db, {
          tenantId: enrollment.tenant_id,
          eventType: 'sequence.step.completed',
          actorType: 'system',
          objectType: 'sequence_enrollment',
          objectId: enrollment.id,
          afterData: { ...stepEventPayload, next_step: null },
        });
        return; // Branch steps handle their own advancement
      case 'ai_action':
        await executeAiActionStep(db, enrollment, sequence, step as AiActionStep, stepIndex, varContext, contact);
        break;
      default:
        await seqRepo.logStepExecution(db, {
          enrollment_id: enrollment.id,
          tenant_id: enrollment.tenant_id,
          step_index: stepIndex,
          step_type: stepType,
          status: 'skipped',
          error: `Unknown step type: ${stepType}`,
        });
        skippedStep = true;
    }

    // Advance to next step
    const steps = sequence.steps as SequenceStep[];
    const nextIndex = stepIndex + 1;
    if (nextIndex >= steps.length) {
      await seqRepo.completeEnrollment(db, enrollment.id, 'completed');
      emitEvent(db, {
        tenantId: enrollment.tenant_id,
        eventType: 'sequence.enrollment_completed',
        actorType: 'system',
        objectType: 'sequence_enrollment',
        objectId: enrollment.id,
        afterData: { enrollment_id: enrollment.id, sequence_id: enrollment.sequence_id },
      }).catch((err) => console.warn('[sequences] enrollment_completed event failed:', err));
    } else {
      const nextStep = steps[nextIndex];
      const delayDays = (nextStep as { delay_days?: number }).delay_days ?? 0;
      const delayHours = (nextStep as { delay_hours?: number }).delay_hours ?? 0;
      const delayMs = (delayDays * 86_400_000) + (delayHours * 3_600_000);
      const nextSendAt = new Date(Date.now() + delayMs).toISOString();
      await seqRepo.setCurrentStep(db, enrollment.id, nextIndex, nextSendAt);
    }
    await emitEvent(db, {
      tenantId: enrollment.tenant_id,
      eventType: skippedStep ? 'sequence.step.skipped' : 'sequence.step.completed',
      actorType: 'system',
      objectType: 'sequence_enrollment',
      objectId: enrollment.id,
      afterData: {
        ...stepEventPayload,
        next_step: nextIndex >= steps.length ? null : nextIndex,
        ...(skippedStep ? { reason: `Unknown step type: ${stepType}` } : {}),
      },
    });
  } catch (err) {
    const errMsg = err instanceof Error ? err.message : String(err);
    await seqRepo.logStepExecution(db, {
      enrollment_id: enrollment.id,
      tenant_id: enrollment.tenant_id,
      step_index: stepIndex,
      step_type: stepType,
      status: 'failed',
      error: errMsg,
    });
    // Reschedule with exponential backoff (max 3 retries then cancel)
    const retries = ((enrollment as any).metadata?.retries ?? 0) as number;
    if (retries >= 2) {
      await seqRepo.cancelEnrollment(db, enrollment.id, 'step_failed');
    } else {
      const backoffMs = Math.pow(2, retries + 1) * 3_600_000; // 2h, 4h
      const retryAt = new Date(Date.now() + backoffMs).toISOString();
      await db.query(
        `UPDATE sequence_enrollments
         SET next_send_at = $1,
             metadata = COALESCE(metadata,'{}')::jsonb || jsonb_build_object('retries', $2),
             updated_at = now()
         WHERE id = $3`,
        [retryAt, retries + 1, enrollment.id],
      );
    }
    await emitEvent(db, {
      tenantId: enrollment.tenant_id,
      eventType: 'sequence.step.failed',
      actorType: 'system',
      objectType: 'sequence_enrollment',
      objectId: enrollment.id,
      afterData: { ...stepEventPayload, error: errMsg },
    });
    throw err;
  }
}

// ── Step executors ─────────────────────────────────────────────────────────────

async function executeEmailStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: EmailStep,
  stepIndex: number,
  varContext: Record<string, unknown>,
  contact: any,
): Promise<void> {
  // Resolve variables in subject and body
  let subject = interpolate(step.subject ?? '', varContext);
  let bodyText = interpolate(step.body_text ?? '', varContext);

  // AI generation
  if (step.ai_generate && step.ai_prompt) {
    const generated = await generateEmailContent(db, enrollment, sequence, step, varContext, contact);
    if (generated) {
      subject = generated.subject ?? subject;
      bodyText = generated.body_text ?? bodyText;
    }
  }

  const toEmail = (contact as any)?.email;
  if (!toEmail) {
    await seqRepo.logStepExecution(db, {
      enrollment_id: enrollment.id,
      tenant_id: enrollment.tenant_id,
      step_index: stepIndex,
      step_type: 'email',
      status: 'skipped',
      error: 'Contact has no email address',
    });
    return;
  }

  // Compliance: skip if contact has opted out of email (CAN-SPAM / GDPR)
  const seqWithFlag = sequence as seqRepo.SequenceRow & { exit_on_unsubscribe?: boolean };
  const exitOnUnsub = seqWithFlag.exit_on_unsubscribe !== false; // default true
  if (exitOnUnsub && (contact as any)?.email_opted_out) {
    await seqRepo.logStepExecution(db, {
      enrollment_id: enrollment.id,
      tenant_id: enrollment.tenant_id,
      step_index: stepIndex,
      step_type: 'email',
      status: 'skipped',
      error: 'Contact has opted out of email',
    });
    // Also cancel the enrollment — no point continuing if they've opted out
    await seqRepo.cancelEnrollment(db, enrollment.id, 'unsubscribed');
    return;
  }

  const actionContextSummary = await getSequenceEmailActionContext(
    db,
    enrollment.tenant_id,
    enrollment,
    sequence,
    stepIndex,
    subject,
    toEmail,
  );
  const effectiveRequireApproval = step.require_approval === true
    || (actionContextSummary?.review_required === true);

  // HITL approval gate
  if (effectiveRequireApproval) {
    const { createHITLRequest } = await import('../db/repos/hitl.js');
    const hitl = await createHITLRequest(db, enrollment.tenant_id, {
      action_type: 'sequence.step.send',
      action_summary: `Send sequence email step ${stepIndex + 1} of "${sequence.name}" to ${toEmail}: "${subject}"`,
      action_payload: {
        enrollment_id: enrollment.id,
        step_index: stepIndex,
        to_email: toEmail,
        subject,
        body_text: bodyText,
        action_context: actionContextSummary,
      },
      agent_id: 'system',
      priority: 'normal',
      sla_minutes: 1440,
    });

    // Pause enrollment awaiting HITL
    await db.query(
      `UPDATE sequence_enrollments SET status = 'paused', paused_at = now(), updated_at = now() WHERE id = $1`,
      [enrollment.id],
    );
    await seqRepo.logStepExecution(db, {
      enrollment_id: enrollment.id,
      tenant_id: enrollment.tenant_id,
      step_index: stepIndex,
      step_type: 'email',
      status: 'approval_pending',
      metadata: { hitl_request_id: hitl.id, action_context: actionContextSummary },
    });
    return;
  }

  // Create and deliver email
  const email = await emailRepo.createEmail(db, enrollment.tenant_id, {
    contact_id: enrollment.contact_id,
    to_email: toEmail,
    to_name: [(contact as any)?.first_name, (contact as any)?.last_name].filter(Boolean).join(' ') || undefined,
    subject,
    body_text: bodyText,
    body_html: step.body_html ? interpolate(step.body_html, varContext) : undefined,
    status: 'draft',
    generation_metadata: {
      origin: 'sequence',
      sequence_id: sequence.id,
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      ai_generated: step.ai_generate === true,
      action_context: actionContextSummary,
    },
  } as any);

  await deliverEmail(db, enrollment.tenant_id, email.id);

  // Back-link email → enrollment for analytics + reply threading
  await db.query(
    `UPDATE emails SET enrollment_id = $1, sequence_id = $2 WHERE id = $3`,
    [enrollment.id, sequence.id, email.id],
  );

  // Create Activity so this email appears in the contact timeline and feeds context extraction
  const activity = await activityRepo.createActivity(db, enrollment.tenant_id, {
    type: 'outreach_email',
    subject,
    body: bodyText?.slice(0, 500),
    direction: 'outbound',
    status: 'completed',
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    contact_id: enrollment.contact_id,
    performed_by: (enrollment as any).enrolled_by_actor_id ?? undefined,
    source_agent: `sequence:${sequence.id}`,
    occurred_at: new Date().toISOString(),
    detail: {
      sequence_id: sequence.id,
      sequence_name: sequence.name,
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      email_id: email.id,
    },
  });

  // Fire-and-forget context extraction from the email content
  triggerExtraction(db, enrollment.tenant_id, activity.id).catch((err) =>
    console.warn('[sequences] context extraction failed after email step:', { enrollmentId: enrollment.id, stepIndex, err }));

  const execution = await seqRepo.logStepExecution(db, {
    enrollment_id: enrollment.id,
    tenant_id: enrollment.tenant_id,
    step_index: stepIndex,
    step_type: 'email',
    status: 'sent',
    email_id: email.id,
    metadata: { to: toEmail, subject, action_context: actionContextSummary },
  });

  // Back-link step execution → activity
  await db.query(
    `UPDATE sequence_step_executions SET activity_id = $1 WHERE id = $2`,
    [activity.id, execution.id],
  );

  emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step_executed',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: { step_index: stepIndex, step_type: 'email', email_id: email.id, activity_id: activity.id },
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  }).catch((err) => console.warn('[sequences] step_executed event failed:', { enrollmentId: enrollment.id, stepIndex, err }));
}

async function executeNotificationStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: NotificationStep,
  stepIndex: number,
  varContext: Record<string, unknown>,
): Promise<void> {
  const { sendMessage } = await import('../messaging/delivery.js');
  const message = interpolate(step.message, varContext);
  const actionContextSummary = await getSequenceStepActionContext(
    db,
    enrollment.tenant_id,
    enrollment,
    sequence,
    stepIndex,
    'notification',
    { channel_id: step.channel_id, message_preview: message.slice(0, 200) },
  );

  if (!step.channel_id) {
    throw new Error('Notification step requires a channel_id');
  }

  await sendMessage(db, enrollment.tenant_id, {
    channel_id: step.channel_id,
    subject: `Sequence: ${message.slice(0, 80)}`,
    body: message,
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  });

  // Create Activity so this notification appears in the contact timeline
  const activity = await activityRepo.createActivity(db, enrollment.tenant_id, {
    type: 'note',
    subject: `Sequence notification: ${message.slice(0, 80)}`,
    body: message,
    status: 'completed',
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    contact_id: enrollment.contact_id,
    performed_by: (enrollment as any).enrolled_by_actor_id ?? undefined,
    source_agent: `sequence:${(enrollment as any).sequence_id}`,
    occurred_at: new Date().toISOString(),
    detail: {
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      channel_id: step.channel_id,
      action_context: actionContextSummary,
    },
  });

  const execution = await seqRepo.logStepExecution(db, {
    enrollment_id: enrollment.id,
    tenant_id: enrollment.tenant_id,
    step_index: stepIndex,
    step_type: 'notification',
    status: 'sent',
    metadata: { message: message.slice(0, 200), action_context: actionContextSummary },
  });

  await db.query(
    `UPDATE sequence_step_executions SET activity_id = $1 WHERE id = $2`,
    [activity.id, execution.id],
  );

  emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step_executed',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: { step_index: stepIndex, step_type: 'notification', activity_id: activity.id },
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  }).catch((err) => console.warn('[sequences] notification step_executed event failed:', { enrollmentId: enrollment.id, stepIndex, err }));
}

async function executeTaskStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: TaskStep,
  stepIndex: number,
  varContext: Record<string, unknown>,
): Promise<void> {
  const title = interpolate(step.title, varContext);
  const description = step.description ? interpolate(step.description, varContext) : undefined;
  const actionContextSummary = await getSequenceStepActionContext(
    db,
    enrollment.tenant_id,
    enrollment,
    sequence,
    stepIndex,
    'task',
    { title, priority: step.priority ?? 'normal', assign_to: step.assign_to },
  );

  // Resolve assignee — 'contact_owner' means we look up the contact's assigned owner
  let assignedTo: UUID | undefined;
  if (step.assign_to && step.assign_to !== 'contact_owner') {
    assignedTo = step.assign_to as UUID;
  }

  const enrolledByActor = (enrollment as any).enrolled_by_actor_id as UUID | undefined;
  const ownerActor = (enrollment as any).owner_actor_id as UUID | undefined;
  const assignedByActor = enrolledByActor ?? ownerActor ?? ('system' as unknown as UUID);

  const assignment = await assignmentRepo.createAssignment(db, enrollment.tenant_id, {
    title,
    description,
    assignment_type: 'task',
    assigned_by: assignedByActor,
    assigned_to: assignedTo,
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    priority: step.priority ?? 'normal',
    context: `Sequence: ${sequence.name} — step ${stepIndex + 1}`,
    metadata: { sequence_id: enrollment.sequence_id, enrollment_id: enrollment.id, step_index: stepIndex, action_context: actionContextSummary },
  });

  // Create Activity so the task appears in the contact timeline
  const activity = await activityRepo.createActivity(db, enrollment.tenant_id, {
    type: 'task',
    subject: title,
    body: description,
    status: 'pending',
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    contact_id: enrollment.contact_id,
    performed_by: enrolledByActor ?? undefined,
    source_agent: `sequence:${sequence.id}`,
    occurred_at: new Date().toISOString(),
    detail: {
      sequence_id: sequence.id,
      sequence_name: sequence.name,
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      assign_to: step.assign_to,
      assignment_id: assignment.id,
      action_context: actionContextSummary,
    },
  });

  const execution = await seqRepo.logStepExecution(db, {
    enrollment_id: enrollment.id,
    tenant_id: enrollment.tenant_id,
    step_index: stepIndex,
    step_type: 'task',
    status: 'sent',
    metadata: { title, assignment_id: assignment.id, action_context: actionContextSummary },
  });

  await db.query(
    `UPDATE sequence_step_executions SET activity_id = $1 WHERE id = $2`,
    [activity.id, execution.id],
  );

  emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step_executed',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: { step_index: stepIndex, step_type: 'task', assignment_id: assignment.id, activity_id: activity.id },
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  }).catch((err) => console.warn('[sequences] task step_executed event failed:', { enrollmentId: enrollment.id, stepIndex, err }));
}

async function executeWebhookStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: WebhookStep,
  stepIndex: number,
  varContext: Record<string, unknown>,
): Promise<void> {
  const url = interpolate(step.url, varContext);
  const method = step.method ?? 'POST';
  const body = step.body_template
    ? interpolate(step.body_template, varContext)
    : JSON.stringify({ enrollment_id: enrollment.id, contact_id: enrollment.contact_id, step_index: stepIndex });

  const headers: Record<string, string> = { 'Content-Type': 'application/json', ...step.headers };
  const actionContextSummary = await getSequenceStepActionContext(
    db,
    enrollment.tenant_id,
    enrollment,
    sequence,
    stepIndex,
    'webhook',
    { method, url },
  );

  const res = await Promise.race([
    fetch(url, { method, headers, body: method !== 'GET' ? body : undefined }),
    new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Webhook timed out')), 30_000)),
  ]);

  if (!res.ok) throw new Error(`Webhook returned HTTP ${res.status}`);

  // Create Activity for the webhook firing
  const activity = await activityRepo.createActivity(db, enrollment.tenant_id, {
    type: 'status_update',
    subject: `Webhook fired: ${url}`,
    body: body,
    status: 'completed',
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    contact_id: enrollment.contact_id,
    performed_by: (enrollment as any).enrolled_by_actor_id ?? undefined,
    source_agent: `sequence:${(enrollment as any).sequence_id}`,
    occurred_at: new Date().toISOString(),
    detail: {
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      url,
      http_status: res.status,
      action_context: actionContextSummary,
    },
  });

  const execution = await seqRepo.logStepExecution(db, {
    enrollment_id: enrollment.id,
    tenant_id: enrollment.tenant_id,
    step_index: stepIndex,
    step_type: 'webhook',
    status: 'sent',
    metadata: { url, status: res.status, action_context: actionContextSummary },
  });

  await db.query(
    `UPDATE sequence_step_executions SET activity_id = $1 WHERE id = $2`,
    [activity.id, execution.id],
  );

  emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step_executed',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: { step_index: stepIndex, step_type: 'webhook', activity_id: activity.id, http_status: res.status },
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  }).catch((err) => console.warn('[sequences] webhook step_executed event failed:', { enrollmentId: enrollment.id, stepIndex, err }));
}

async function executeWaitStep(
  db: DbPool,
  enrollment: any,
  step: WaitStep,
  stepIndex: number,
): Promise<void> {
  // If no condition, this is a pure delay — advance to next step immediately
  // (the delay was already applied when setting next_send_at)
  if (!step.condition) {
    const sequence = await seqRepo.getSequence(db, enrollment.tenant_id, enrollment.sequence_id);
    if (!sequence) return;
    const steps = sequence.steps as SequenceStep[];
    const nextIndex = stepIndex + 1;
    if (nextIndex >= steps.length) {
      await seqRepo.completeEnrollment(db, enrollment.id, 'completed');
    } else {
      const nextStep = steps[nextIndex];
      const delayDays = (nextStep as { delay_days?: number }).delay_days ?? 0;
      const nextSendAt = new Date(Date.now() + delayDays * 86_400_000).toISOString();
      await seqRepo.setCurrentStep(db, enrollment.id, nextIndex, nextSendAt);
    }
    return;
  }

  // Has a condition: check if condition event has occurred
  const condition = step.condition;
  const executions = await seqRepo.getStepExecutions(db, enrollment.id);
  const conditionMet = executions.some((e: any) =>
    e.metadata?.trigger_event === condition.event ||
    e.metadata?.event === condition.event,
  );

  if (conditionMet) {
    // Condition met — advance
    const sequence = await seqRepo.getSequence(db, enrollment.tenant_id, enrollment.sequence_id);
    if (!sequence) return;
    const steps = sequence.steps as SequenceStep[];
    const nextIndex = stepIndex + 1;
    if (nextIndex >= steps.length) {
      await seqRepo.completeEnrollment(db, enrollment.id, 'completed');
    } else {
      const nextStep = steps[nextIndex];
      const delayDays = (nextStep as { delay_days?: number }).delay_days ?? 0;
      const nextSendAt = new Date(Date.now() + delayDays * 86_400_000).toISOString();
      await seqRepo.setCurrentStep(db, enrollment.id, nextIndex, nextSendAt);
    }
  } else {
    // Check timeout
    const enrolledAt = new Date(enrollment.created_at).getTime();
    const timeoutMs = condition.timeout_days * 86_400_000;
    if (Date.now() - enrolledAt > timeoutMs) {
      if (condition.timeout_branch != null) {
        // Jump to timeout branch step
        const sequence = await seqRepo.getSequence(db, enrollment.tenant_id, enrollment.sequence_id);
        if (!sequence) return;
        const steps = sequence.steps as SequenceStep[];
        const branchIdx = Math.min(condition.timeout_branch, steps.length - 1);
        const branchStep = steps[branchIdx];
        const delayDays = (branchStep as { delay_days?: number }).delay_days ?? 0;
        const nextSendAt = new Date(Date.now() + delayDays * 86_400_000).toISOString();
        await seqRepo.setCurrentStep(db, enrollment.id, branchIdx, nextSendAt);
      } else {
        await seqRepo.completeEnrollment(db, enrollment.id, 'timeout');
      }
    } else {
      // Re-check in 1 hour
      const recheckAt = new Date(Date.now() + 3_600_000).toISOString();
      await db.query(
        'UPDATE sequence_enrollments SET next_send_at = $1, updated_at = now() WHERE id = $2',
        [recheckAt, enrollment.id],
      );
    }
  }
}

async function executeBranchStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: BranchStep,
  stepIndex: number,
): Promise<void> {
  const executions = await seqRepo.getStepExecutions(db, enrollment.id);
  const steps = sequence.steps as SequenceStep[];

  for (const condition of step.conditions) {
    let matched = false;

    switch (condition.trigger) {
      case 'replied':
        matched = (enrollment.exit_reason === 'replied') ||
          executions.some((e: any) => e.metadata?.trigger === 'replied');
        break;
      case 'opened':
        matched = executions.some((e: any) => e.metadata?.opened === true);
        break;
      case 'clicked':
        matched = executions.some((e: any) => e.metadata?.clicked === true);
        break;
      case 'goal_met':
        matched = !!enrollment.goal_met_at;
        break;
      case 'custom_event':
        matched = executions.some((e: any) => e.metadata?.event === condition.event);
        break;
    }

    if (matched) {
      const sequence = await seqRepo.getSequence(db, enrollment.tenant_id, enrollment.sequence_id);
      const branchDesc = condition.exit
        ? `Branch: ${condition.trigger} → exit`
        : `Branch: ${condition.trigger} → step ${(condition.jump_to_step ?? 0) + 1}`;

      // Activity for branch evaluation
      activityRepo.createActivity(db, enrollment.tenant_id, {
        type: 'note',
        subject: branchDesc,
        body: branchDesc,
        status: 'completed',
        subject_type: 'contact',
        subject_id: enrollment.contact_id,
        contact_id: enrollment.contact_id,
        source_agent: `sequence:${enrollment.sequence_id}`,
        occurred_at: new Date().toISOString(),
        detail: { enrollment_id: enrollment.id, step_index: stepIndex, condition },
      }).catch((err) => console.warn('[sequences] branch activity failed:', { enrollmentId: enrollment.id, err }));

      if (condition.exit) {
        await seqRepo.completeEnrollment(db, enrollment.id, 'branch_exit');
      } else if (condition.jump_to_step != null) {
        const targetIdx = Math.min(condition.jump_to_step, steps.length - 1);
        const targetStep = steps[targetIdx];
        const delayDays = (targetStep as { delay_days?: number }).delay_days ?? 0;
        const nextSendAt = new Date(Date.now() + delayDays * 86_400_000).toISOString();
        await seqRepo.setCurrentStep(db, enrollment.id, targetIdx, nextSendAt);
      }
      return;
    }
  }

  // No branch matched — advance normally
  const nextIndex = stepIndex + 1;
  if (nextIndex >= steps.length) {
    await seqRepo.completeEnrollment(db, enrollment.id, 'completed');
  } else {
    const nextStep = steps[nextIndex];
    const delayDays = (nextStep as { delay_days?: number }).delay_days ?? 0;
    const nextSendAt = new Date(Date.now() + delayDays * 86_400_000).toISOString();
    await seqRepo.setCurrentStep(db, enrollment.id, nextIndex, nextSendAt);
  }
}

async function executeAiActionStep(
  db: DbPool,
  enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: AiActionStep,
  stepIndex: number,
  varContext: Record<string, unknown>,
  contact: any,
): Promise<void> {
  const prompt = interpolate(step.prompt, varContext);
  const actionContextSummary = await getSequenceStepActionContext(
    db,
    enrollment.tenant_id,
    enrollment,
    sequence,
    stepIndex,
    'ai_action',
    { prompt: prompt.slice(0, 500), tool_names: step.tool_names ?? [] },
  );

  // Build a concise context summary for the LLM
  const contextSummary = JSON.stringify({
    contact: {
      name: [(contact as any)?.first_name, (contact as any)?.last_name].filter(Boolean).join(' '),
      email: (contact as any)?.email,
      title: (contact as any)?.title,
      company: (contact as any)?.company_name,
    },
    sequence: sequence.name,
    step: stepIndex + 1,
    enrollment_variables: (enrollment as any).variables ?? {},
    action_context: actionContextSummary,
  }, null, 2);

  await requireTenantLLMConfig(db, enrollment.tenant_id);
  const result = await callLLM(db, enrollment.tenant_id, {
    system: (sequence as any).ai_persona ?? 'You are a helpful sales assistant.',
    user: `Context:\n${contextSummary}\n\nTask: ${prompt}`,
  });

  // Create Activity for the AI action — this also triggers context extraction
  const activity = await activityRepo.createActivity(db, enrollment.tenant_id, {
    type: 'note_added',
    subject: `AI action: ${prompt.slice(0, 80)}`,
    body: `[Sequence: ${sequence.name} — step ${stepIndex + 1}]\n\nPrompt: ${prompt}\n\nResult: ${result.slice(0, 500)}`,
    status: 'completed',
    subject_type: 'contact',
    subject_id: enrollment.contact_id,
    contact_id: enrollment.contact_id,
    performed_by: (enrollment as any).enrolled_by_actor_id ?? undefined,
    source_agent: `sequence:${sequence.id}`,
    occurred_at: new Date().toISOString(),
    detail: {
      sequence_id: sequence.id,
      sequence_name: sequence.name,
      enrollment_id: enrollment.id,
      step_index: stepIndex,
      prompt: prompt.slice(0, 500),
      action_context: actionContextSummary,
    },
  });

  // Fire-and-forget context extraction from the AI action result
  triggerExtraction(db, enrollment.tenant_id, activity.id).catch((err) =>
    console.warn('[sequences] context extraction failed after ai_action step:', { enrollmentId: enrollment.id, stepIndex, err }));

  const execution = await seqRepo.logStepExecution(db, {
    enrollment_id: enrollment.id,
    tenant_id: enrollment.tenant_id,
    step_index: stepIndex,
    step_type: 'ai_action',
    status: 'sent',
    metadata: { prompt: prompt.slice(0, 200), result: result.slice(0, 500), action_context: actionContextSummary },
  });

  await db.query(
    `UPDATE sequence_step_executions SET activity_id = $1 WHERE id = $2`,
    [activity.id, execution.id],
  );

  emitEvent(db, {
    tenantId: enrollment.tenant_id,
    eventType: 'sequence.step_executed',
    actorType: 'system',
    objectType: 'sequence_enrollment',
    objectId: enrollment.id,
    afterData: { step_index: stepIndex, step_type: 'ai_action', activity_id: activity.id },
    metadata: { origin: 'sequence', action_context: actionContextSummary },
  }).catch((err) => console.warn('[sequences] ai_action step_executed event failed:', { enrollmentId: enrollment.id, stepIndex, err }));
}

// ── AI content generation helper ───────────────────────────────────────────────

async function generateEmailContent(
  _db: DbPool,
  _enrollment: any,
  sequence: seqRepo.SequenceRow,
  step: EmailStep,
  varContext: Record<string, unknown>,
  contact: any,
): Promise<{ subject?: string; body_text?: string } | null> {
  try {
    await requireTenantLLMConfig(_db, _enrollment.tenant_id);
    const contactInfo = JSON.stringify({
      name: [(contact as any)?.first_name, (contact as any)?.last_name].filter(Boolean).join(' '),
      email: (contact as any)?.email,
      title: (contact as any)?.title,
      company: (contact as any)?.company_name,
      lifecycle_stage: (contact as any)?.lifecycle_stage,
    });

    const aiPrompt = step.ai_prompt
      ? interpolate(step.ai_prompt, varContext)
      : `Write a short, personalized sales email for this contact. Be concise and genuine.`;

    const systemPrompt = (sequence as any).ai_persona ??
      'You are a helpful sales assistant writing personalized outreach emails. Keep emails short, genuine, and focused on value. Return JSON: { "subject": "...", "body_text": "..." }';

    const text = await callLLM(_db, _enrollment.tenant_id, {
      system: systemPrompt,
      user: `Contact info:\n${contactInfo}\n\nTemplate subject: ${step.subject}\nTemplate body: ${step.body_text ?? ''}\n\nInstruction: ${aiPrompt}\n\nReturn only valid JSON: {"subject":"...","body_text":"..."}`,
    });

    if (text) {
      const jsonMatch = text.match(/\{[\s\S]*\}/);
      if (jsonMatch) {
        return JSON.parse(jsonMatch[0]) as { subject?: string; body_text?: string };
      }
    }
  } catch (err) {
    if (err && typeof err === 'object' && 'details' in err && (err as { details?: { reason?: unknown } }).details?.reason === 'agent_config_required') {
      throw err;
    }
    console.warn('[sequences] AI generation failed, using template:', err);
  }
  return null;
}

// ── Reply detection ────────────────────────────────────────────────────────────

/**
 * Called by the inbound email handler when a reply is received.
 * Matches the In-Reply-To header against sequence step execution records.
 */
export async function handleSequenceReply(
  db: DbPool,
  tenantId: UUID,
  inReplyToMsgId: string,
  fromEmail: string,
): Promise<void> {
  // Find step execution with matching provider_msg_id
  const result = await db.query(
    `SELECT sse.enrollment_id, se.sequence_id, se.contact_id, seq.exit_on_reply
     FROM sequence_step_executions sse
     JOIN sequence_enrollments se ON se.id = sse.enrollment_id
     JOIN sequences seq ON seq.id = se.sequence_id
     WHERE sse.tenant_id = $1
       AND sse.metadata->>'provider_msg_id' = $2
       AND se.status IN ('active','paused')
     LIMIT 1`,
    [tenantId, inReplyToMsgId],
  );

  if (!result.rows[0]) return;

  const { enrollment_id, sequence_id, exit_on_reply } = result.rows[0];

  // Log the reply event on the enrollment
  await db.query(
    `UPDATE sequence_enrollments
     SET metadata = COALESCE(metadata,'{}')::jsonb || jsonb_build_object('last_reply_from', $1, 'last_reply_at', $2),
         updated_at = now()
     WHERE id = $3`,
    [fromEmail, new Date().toISOString(), enrollment_id],
  );

  if (exit_on_reply) {
    await seqRepo.cancelEnrollment(db, enrollment_id, 'replied');
    emitEvent(db, {
      tenantId,
      eventType: 'sequence.contact_replied',
      actorType: 'system',
      objectType: 'sequence_enrollment',
      objectId: enrollment_id,
      afterData: { enrollment_id, sequence_id, from_email: fromEmail, exit_reason: 'replied' },
    }).catch((err) => console.warn('[sequences] contact_replied event failed:', { enrollment_id, err }));
  }
}

// ── Goal-event detection ───────────────────────────────────────────────────────

export function resolveSequenceGoalContactId(event: {
  objectType?: string;
  objectId?: UUID;
  afterData?: unknown;
  metadata?: Record<string, unknown>;
}): UUID | undefined {
  const afterData = (event.afterData && typeof event.afterData === 'object')
    ? event.afterData as Record<string, unknown>
    : {};
  const metadata = (event.metadata && typeof event.metadata === 'object') ? event.metadata : {};
  const contact = afterData.contact && typeof afterData.contact === 'object'
    ? afterData.contact as Record<string, unknown>
    : {};

  if (typeof afterData.contact_id === 'string') return afterData.contact_id as UUID;
  if (typeof contact.id === 'string') return contact.id as UUID;
  if (typeof metadata.contact_id === 'string') return metadata.contact_id as UUID;
  if (event.objectType === 'contact' && typeof afterData.id === 'string') return afterData.id as UUID;
  if (event.objectType === 'contact' && typeof event.objectId === 'string') return event.objectId as UUID;
  if (afterData.object_type === 'contact' && typeof afterData.object_id === 'string') return afterData.object_id as UUID;
  if (afterData.subject_type === 'contact' && typeof afterData.subject_id === 'string') return afterData.subject_id as UUID;
  return undefined;
}

/**
 * Called from the event bus listener in index.ts for every CRM event.
 * Checks if any active enrollments in sequences with a matching goal_event should be completed.
 */
export async function handleSequenceGoalEvent(
  db: DbPool,
  tenantId: UUID,
  eventType: string,
  contactId: UUID | undefined,
): Promise<void> {
  if (!contactId) return;

  const result = await db.query(
    `SELECT se.id AS enrollment_id
     FROM sequence_enrollments se
     JOIN sequences seq ON seq.id = se.sequence_id
     WHERE se.tenant_id = $1
       AND se.contact_id = $2
       AND seq.goal_event = $3
       AND se.status = 'active'`,
    [tenantId, contactId, eventType],
  );

  for (const row of result.rows) {
    await db.query(
      `UPDATE sequence_enrollments
       SET status = 'completed', goal_met_at = now(), exit_reason = 'goal_met', updated_at = now()
       WHERE id = $1`,
      [row.enrollment_id],
    );
    emitEvent(db, {
      tenantId,
      eventType: 'sequence.goal_met',
      actorType: 'system',
      objectType: 'sequence_enrollment',
      objectId: row.enrollment_id,
      afterData: { enrollment_id: row.enrollment_id, goal_event: eventType },
    }).catch((err) => console.warn('[sequences] goal_met event failed:', { enrollment_id: row.enrollment_id, err }));
  }
}

// ── HITL auto-resume ───────────────────────────────────────────────────────────

/**
 * Called automatically after a HITL request with action_type='sequence.step.send'
 * is approved or rejected. Resumes the paused enrollment.
 *
 * Approved  → sends the pre-drafted email, marks step sent, advances enrollment.
 * Rejected  → skips the step, advances enrollment so the sequence continues.
 */
export async function resumeEnrollmentAfterHITL(
  db: DbPool,
  hitlRequest: { id: string; tenant_id: string; action_payload: unknown; status: string },
): Promise<void> {
  const payload = (hitlRequest.action_payload ?? {}) as Record<string, unknown>;
  const enrollmentId = payload.enrollment_id as string | undefined;
  const stepIndex    = payload.step_index    as number | undefined;
  const toEmail      = payload.to_email      as string | undefined;
  const subject      = payload.subject       as string | undefined;
  const bodyText     = payload.body_text     as string | undefined;
  const actionContextSummary = payload.action_context && typeof payload.action_context === 'object' && !Array.isArray(payload.action_context)
    ? payload.action_context as Record<string, unknown>
    : undefined;

  if (!enrollmentId || stepIndex == null) {
    console.warn('[sequences] resumeEnrollmentAfterHITL: missing enrollment_id or step_index in payload', payload);
    return;
  }

  // Verify enrollment is still paused
  const enrollResult = await db.query(
    `SELECT * FROM sequence_enrollments WHERE id = $1 AND tenant_id = $2 AND status = 'paused' LIMIT 1`,
    [enrollmentId, hitlRequest.tenant_id],
  );
  const enrollment = enrollResult.rows[0];
  if (!enrollment) {
    console.warn('[sequences] resumeEnrollmentAfterHITL: enrollment not found or not paused', enrollmentId);
    return;
  }

  const sequence = await seqRepo.getSequence(db, hitlRequest.tenant_id as UUID, enrollment.sequence_id);
  if (!sequence) return;

  const approved = hitlRequest.status === 'approved' || hitlRequest.status === 'auto_approved';

  if (approved && toEmail && subject) {
    // Send the pre-drafted email
    try {
      const contact = await contactRepo.getContact(db, hitlRequest.tenant_id as UUID, enrollment.contact_id);
      const email = await emailRepo.createEmail(db, hitlRequest.tenant_id as UUID, {
        contact_id: enrollment.contact_id,
        to_email: toEmail,
        to_name: [(contact as any)?.first_name, (contact as any)?.last_name].filter(Boolean).join(' ') || undefined,
        subject,
        body_text: bodyText ?? '',
        status: 'draft',
        generation_metadata: {
          origin: 'sequence',
          sequence_id: enrollment.sequence_id,
          enrollment_id: enrollmentId,
          step_index: stepIndex,
          action_context: actionContextSummary,
        },
      } as any);

      await deliverEmail(db, hitlRequest.tenant_id as UUID, email.id);

      await db.query(
        `UPDATE emails SET enrollment_id = $1, sequence_id = $2 WHERE id = $3`,
        [enrollmentId, enrollment.sequence_id, email.id],
      );

      const activity = await activityRepo.createActivity(db, hitlRequest.tenant_id as UUID, {
        type: 'outreach_email',
        subject,
        body: (bodyText ?? '').slice(0, 500),
        direction: 'outbound',
        status: 'completed',
        subject_type: 'contact',
        subject_id: enrollment.contact_id,
        contact_id: enrollment.contact_id,
        performed_by: enrollment.enrolled_by_actor_id ?? undefined,
        source_agent: `sequence:${sequence.id}`,
        occurred_at: new Date().toISOString(),
        detail: {
          sequence_id: sequence.id,
          sequence_name: sequence.name,
          enrollment_id: enrollmentId,
          step_index: stepIndex,
          email_id: email.id,
          hitl_request_id: hitlRequest.id,
          action_context: actionContextSummary,
        },
      });

      triggerExtraction(db, hitlRequest.tenant_id as UUID, activity.id).catch((err) =>
        console.warn('[sequences] context extraction failed after approved email step:', { enrollmentId, stepIndex, err }));

      // Update the step execution from approval_pending → sent
      await db.query(
        `UPDATE sequence_step_executions
         SET status = 'sent', executed_at = now(), email_id = $1, activity_id = $2,
             metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object('approved_at', $3, 'action_context', $4::jsonb)
         WHERE enrollment_id = $5 AND step_index = $6`,
        [email.id, activity.id, new Date().toISOString(), JSON.stringify(actionContextSummary ?? null), enrollmentId, stepIndex],
      );

      emitEvent(db, {
        tenantId: hitlRequest.tenant_id as UUID,
        eventType: 'sequence.step_executed',
        actorType: 'system',
        objectType: 'sequence_enrollment',
        objectId: enrollmentId,
        afterData: { step_index: stepIndex, step_type: 'email', email_id: email.id, activity_id: activity.id, resumed_from_hitl: true },
        metadata: { origin: 'sequence', action_context: actionContextSummary },
      }).catch((err) => console.warn('[sequences] step_executed event failed after HITL resume:', { enrollmentId, stepIndex, err }));

      console.info('[sequences] HITL-approved email sent', { enrollmentId, stepIndex, toEmail });
    } catch (err) {
      console.error('[sequences] Failed to send HITL-approved email:', { enrollmentId, stepIndex, err });
      // Mark step failed so the retry mechanism can handle it
      await db.query(
        `UPDATE sequence_step_executions
         SET status = 'failed', error = $1
         WHERE enrollment_id = $2 AND step_index = $3 AND status = 'approval_pending'`,
        [err instanceof Error ? err.message : String(err), enrollmentId, stepIndex],
      );
      return;
    }
  } else if (!approved) {
    // Rejected — mark step skipped
    await db.query(
      `UPDATE sequence_step_executions
       SET status = 'skipped', executed_at = now(),
           metadata = COALESCE(metadata, '{}'::jsonb) || jsonb_build_object('rejected_at', $1)
       WHERE enrollment_id = $2 AND step_index = $3 AND status = 'approval_pending'`,
      [new Date().toISOString(), enrollmentId, stepIndex],
    );
    console.info('[sequences] HITL-rejected step skipped', { enrollmentId, stepIndex });
  }

  // Advance enrollment to next step
  const steps = (sequence.steps as SequenceStep[]);
  const nextIndex = stepIndex + 1;

  if (nextIndex >= steps.length) {
    await seqRepo.completeEnrollment(db, enrollmentId as UUID, 'completed');
    emitEvent(db, {
      tenantId: hitlRequest.tenant_id as UUID,
      eventType: 'sequence.enrollment_completed',
      actorType: 'system',
      objectType: 'sequence_enrollment',
      objectId: enrollmentId,
      afterData: { enrollment_id: enrollmentId, sequence_id: enrollment.sequence_id },
    }).catch((err) => console.warn('[sequences] enrollment_completed event failed after HITL resume:', err));
  } else {
    const nextStep = steps[nextIndex];
    const delayDays  = (nextStep as { delay_days?: number }).delay_days ?? 0;
    const delayHours = (nextStep as { delay_hours?: number }).delay_hours ?? 0;
    const delayMs    = (delayDays * 86_400_000) + (delayHours * 3_600_000);
    const nextSendAt = new Date(Date.now() + delayMs).toISOString();
    await db.query(
      `UPDATE sequence_enrollments
       SET status = 'active', current_step = $1, next_send_at = $2, paused_at = NULL, updated_at = now()
       WHERE id = $3`,
      [nextIndex, nextSendAt, enrollmentId],
    );
    console.info('[sequences] enrollment resumed after HITL', { enrollmentId, nextIndex, nextSendAt });
  }
}
