/**
 * @module wf.opportunity
 *
 * Opportunity Workflow
 * ====================
 *
 * Central workflow engine for the opportunity lifecycle.  All state
 * transitions, follow-up scheduling, cold detection, and CW activity
 * creation are driven through this file.
 *
 * ## Ground rules
 *
 * 1. Every state transition creates a CW activity as the audit trail.
 * 2. Activities carry an `Optima_Type` custom field to tag their type.
 * 3. The opportunity's CW status is the source of truth for current state.
 * 4. The activity history IS the metadata — timestamps, notes, and
 *    state changes are all derived from activities.
 * 5. When `timeSpent` is provided on an activity close, a CW time entry
 *    is automatically submitted.
 * 6. The opportunity's stage MUST be "Optima" before any workflow
 *    action is allowed.
 *
 * ## Statuses (CW IDs)
 *
 * | Enum Key          | CW ID | CW Name               |
 * |-------------------|-------|------------------------|
 * | PendingNew        | 37    | 00. Pending New        |
 * | New               | 24    | 01. New                |
 * | InternalReview    | 56    | 02. Internal Review    |
 * | QuoteSent         | 43    | 03. Quote Sent         |
 * | ConfirmedQuote    | 57    | 04. Confirmed Quote    |
 * | Active            | 58    | 05. Active             |
 * | ReadyToSend       | 63    | Ready to Send          |
 * | PendingSent       | 60    | Pending Sent (Deprecated) |
 * | PendingRevision   | 61    | Pending Revision       |
 * | PendingWon        | 49    | 91. Pending Won        |
 * | Won               | 29    | 95. Won                |
 * | PendingLost       | 50    | 98. Pending Lost       |
 * | Lost              | 53    | 99. Lost               |
 * | Canceled          | 59    | Canceled               |
 */

import { OpportunityController } from "../controllers/OpportunityController";
import { ActivityController } from "../controllers/ActivityController";
import { activityCw } from "../modules/cw-utils/activities/activities";
import {
  checkColdStatus,
  type ColdCheckResult,
} from "../modules/algorithms/algo.coldThreshold";
import {
  submitTimeEntry,
  syncOpportunityStatus,
} from "../services/cw.opportunityService";

// ═══════════════════════════════════════════════════════════════════════════
// CONSTANTS & ENUMS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Canonical status enum mapping workflow names → CW status IDs.
 */
export const OpportunityStatus = {
  PendingNew: 37,
  New: 24,
  InternalReview: 56,
  QuoteSent: 43,
  ConfirmedQuote: 57,
  Active: 58,
  ReadyToSend: 63,
  PendingSent: 60,
  PendingRevision: 61,
  PendingWon: 49,
  Won: 29,
  PendingLost: 50,
  Lost: 53,
  Canceled: 59,
} as const;

export type OpportunityStatusKey = keyof typeof OpportunityStatus;
export type OpportunityStatusId =
  (typeof OpportunityStatus)[OpportunityStatusKey];

/** Reverse lookup: CW ID → workflow key. */
export const StatusIdToKey: Record<number, OpportunityStatusKey> =
  Object.fromEntries(
    Object.entries(OpportunityStatus).map(([k, v]) => [
      v,
      k as OpportunityStatusKey,
    ]),
  ) as Record<number, OpportunityStatusKey>;

/** Terminal (immutable) statuses. */
const TERMINAL_STATUSES = new Set<number>([
  OpportunityStatus.Won,
  OpportunityStatus.Lost,
]);

/**
 * CW Activity custom field for Optima_Type.
 *
 * Field ID 45, with list options:
 *   58 = Quote Setup
 *   58 = Opportunity Setup
 *   59 = Quote Sent
 *   60 = Quote Confirmed
 *   61 = Revision
 *   62 = Finalized
 *   63 = Converted
 *   64 = Opportunity Created
 *   65 = Opportunity Review
 *   66 = Quote Generated
 *   67 = Quote Sent & Confirmed
 */
export const OptimaType = {
  FIELD_ID: 45,
  OpportunityCreated: "Opportunity Created",
  OpportunitySetup: "Opportunity Setup",
  OpportunityReview: "Opportunity Review",
  QuoteSent: "Quote Sent",
  QuoteConfirmed: "Quote Confirmed",
  QuoteSentConfirmed: "Quote Sent & Confirmed",
  QuoteGenerated: "Quote Generated",
  Revision: "Revision",
  Finalized: "Finalized",
  Converted: "Converted",
  ScheduleEntry: "Schedule Entry",
} as const;

/** CW custom field ID for the QuoteID field on activities. */
const QUOTE_ID_FIELD_ID = 48;

/** CW custom field ID for the Close Date field on activities. */
const CLOSE_DATE_FIELD_ID = 49;

/** CW custom field ID for the Parent Activity field on activities. */
export const PARENT_ACTIVITY_FIELD_ID = 50;

/**
 * Optima_Type values whose activities should remain Open until the
 * next workflow transition closes them automatically.
 */
const STAYS_OPEN_TYPES = new Set<OptimaTypeValue>([
  OptimaType.OpportunitySetup,
  OptimaType.OpportunityReview,
  OptimaType.Revision,
  OptimaType.ScheduleEntry,
]);

export type OptimaTypeValue =
  | typeof OptimaType.OpportunityCreated
  | typeof OptimaType.OpportunitySetup
  | typeof OptimaType.OpportunityReview
  | typeof OptimaType.QuoteSent
  | typeof OptimaType.QuoteConfirmed
  | typeof OptimaType.QuoteSentConfirmed
  | typeof OptimaType.QuoteGenerated
  | typeof OptimaType.Revision
  | typeof OptimaType.Finalized
  | typeof OptimaType.Converted
  | typeof OptimaType.ScheduleEntry;

/** Permission nodes required by gated transitions. */
export const WorkflowPermissions = {
  FINALIZE: "sales.opportunity.finalize",
  CANCEL: "sales.opportunity.cancel",
  REVIEW: "sales.opportunity.review",
  SEND: "sales.opportunity.send",
  REOPEN: "sales.opportunity.reopen",
  WIN: "sales.opportunity.win",
  LOSE: "sales.opportunity.lose",
} as const;

/** Required Optima stage name. */
const REQUIRED_STAGE = "Optima";

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION MAP
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Defines which target statuses are reachable from each source status.
 *
 * This map covers DIRECT transitions only. Some transitions are
 * compound (e.g. QuoteSent with `won: true` goes through Won/PendingWon),
 * which are handled inside the individual transition functions.
 */
const ALLOWED_TRANSITIONS: Record<number, Set<number>> = {
  [OpportunityStatus.PendingNew]: new Set([OpportunityStatus.New]),

  [OpportunityStatus.New]: new Set([
    OpportunityStatus.ReadyToSend,
    OpportunityStatus.InternalReview,
    OpportunityStatus.QuoteSent,
    OpportunityStatus.Canceled,
  ]),

  [OpportunityStatus.InternalReview]: new Set([
    OpportunityStatus.ReadyToSend,
    OpportunityStatus.PendingRevision,
    OpportunityStatus.QuoteSent, // reviewer manually sends
    OpportunityStatus.Canceled,
  ]),

  [OpportunityStatus.ReadyToSend]: new Set([OpportunityStatus.QuoteSent]),

  [OpportunityStatus.PendingSent]: new Set([
    OpportunityStatus.ReadyToSend,
    OpportunityStatus.QuoteSent,
  ]),

  [OpportunityStatus.PendingRevision]: new Set([OpportunityStatus.Active]),

  [OpportunityStatus.QuoteSent]: new Set([
    OpportunityStatus.ConfirmedQuote,
    OpportunityStatus.Won,
    OpportunityStatus.PendingWon,
    OpportunityStatus.PendingLost,
    OpportunityStatus.Active,
    OpportunityStatus.PendingRevision, // needs revision
    OpportunityStatus.InternalReview, // cold automation only
  ]),

  [OpportunityStatus.ConfirmedQuote]: new Set([
    OpportunityStatus.Won,
    OpportunityStatus.PendingWon,
    OpportunityStatus.PendingLost,
    OpportunityStatus.Active,
    OpportunityStatus.InternalReview, // cold automation only
    OpportunityStatus.PendingRevision, // send back for revision
  ]),

  [OpportunityStatus.Active]: new Set([
    OpportunityStatus.ReadyToSend,
    OpportunityStatus.QuoteSent,
    OpportunityStatus.InternalReview,
    OpportunityStatus.Canceled,
  ]),

  [OpportunityStatus.PendingWon]: new Set([
    OpportunityStatus.Won,
    OpportunityStatus.Active, // admin revision
  ]),

  [OpportunityStatus.PendingLost]: new Set([
    OpportunityStatus.Lost,
    OpportunityStatus.Active, // resurrection
  ]),

  [OpportunityStatus.Won]: new Set(), // terminal
  [OpportunityStatus.Lost]: new Set(), // terminal

  [OpportunityStatus.Canceled]: new Set([
    OpportunityStatus.Active, // re-open
  ]),
};

// ═══════════════════════════════════════════════════════════════════════════
// TYPES
// ═══════════════════════════════════════════════════════════════════════════

/** User context passed into every workflow action. */
export interface WorkflowUser {
  /** Internal user ID. */
  id: string;
  /** CW member ID for activity assignment. */
  cwMemberId: number;
  /** Resolved permission node strings the user holds. */
  permissions: string[];
}

/** Base payload fields common to all actions. */
interface BaseActionPayload {
  /** Required note for state-change activities. */
  note?: string;
  /** ISO-8601 datetime when work started. Both timeStarted and timeEnded must be provided to submit a time entry. */
  timeStarted?: string;
  /** ISO-8601 datetime when work ended. Both timeStarted and timeEnded must be provided to submit a time entry. */
  timeEnded?: string;
}

/** Transition to New. */
export interface AcceptNewPayload extends BaseActionPayload {}

/** Transition to InternalReview. */
export interface RequestReviewPayload extends BaseActionPayload {
  note: string; // required
}

/** Review decision payload (approve / reject / send / cancel). */
export interface ReviewDecisionPayload extends BaseActionPayload {
  note: string; // required
  decision: "approve" | "reject" | "send" | "cancel";
}

/** Transition from ReadyToSend/PendingSent(deprecated) → QuoteSent. */
export interface SendQuotePayload extends BaseActionPayload {
  /**
   * If true, marks sent AND confirmed simultaneously.
   * Skips receipt confirmation follow-up. Transitions to ConfirmedQuote.
   */
  quoteConfirmed?: boolean;

  /**
   * Quote was presented and won immediately.
   * Without finalize: transitions to PendingWon.
   * With finalize (requires FINALIZE perm): transitions directly to Won.
   * Satisfies QuoteSent and ConfirmedQuote simultaneously.
   */
  won?: boolean;

  /**
   * Quote rejected immediately upon presentation.
   * Without finalize: transitions to PendingLost.
   * With finalize (requires FINALIZE perm): transitions directly to Lost.
   */
  lost?: boolean;

  /**
   * Explicitly finalize the outcome (skip pending state).
   * Requires `sales.opportunity.finalize` permission.
   * Used with `won` or `lost` to go directly to Won/Lost
   * instead of PendingWon/PendingLost.
   */
  finalize?: boolean;

  /**
   * Quote needs revision.
   * Creates a revision activity and transitions to PendingRevision.
   */
  needsRevision?: boolean;
}

/** Mark opportunity as ready to send quote from send-capable statuses. */
export interface MarkReadyToSendPayload extends BaseActionPayload {}

/** Confirm receipt of a quote. */
export interface ConfirmQuotePayload extends BaseActionPayload {}

/** Mark opportunity as won or lost. */
export interface FinalizePayload extends BaseActionPayload {
  note: string; // required
  outcome: "won" | "lost";
}

/** Resurrect from PendingWon / PendingLost → Active. */
export interface ResurrectPayload extends BaseActionPayload {
  note: string; // required
}

/** Begin revision from PendingRevision → Active. */
export interface BeginRevisionPayload extends BaseActionPayload {}

/** Send back for revision from ConfirmedQuote → PendingRevision. */
export interface SendBackForRevisionPayload extends BaseActionPayload {
  note: string; // required
}

/** Create a Schedule Entry activity (no status change). */
export interface CreateScheduleEntryPayload extends BaseActionPayload {
  /** Activity type value: Follow-Up, Appointment, or Admin. */
  activityTypeValue: "Follow-Up" | "Appointment" | "Admin";
  /** ISO-8601 due date. */
  dueDate?: string;
  /** ISO-8601 start time. */
  startTime?: string;
  /** ISO-8601 end time. */
  endTime?: string;
  /** Optional notes for the schedule entry. */
  note?: string;
}

/** Re-send from Active → QuoteSent. */
export interface ResendQuotePayload extends SendQuotePayload {}

/** Cancel payload. */
export interface CancelPayload extends BaseActionPayload {
  note: string; // required
}

/** Re-open from Canceled → Active. */
export interface ReopenPayload extends BaseActionPayload {
  note: string; // required
}

// -----------------------------------------------------------------------
// Action discriminator
// -----------------------------------------------------------------------

export type WorkflowAction =
  | { action: "acceptNew"; payload: AcceptNewPayload }
  | { action: "requestReview"; payload: RequestReviewPayload }
  | { action: "reviewDecision"; payload: ReviewDecisionPayload }
  | { action: "markReadyToSend"; payload: MarkReadyToSendPayload }
  | { action: "sendQuote"; payload: SendQuotePayload }
  | { action: "confirmQuote"; payload: ConfirmQuotePayload }
  | { action: "finalize"; payload: FinalizePayload }
  | { action: "resurrect"; payload: ResurrectPayload }
  | { action: "beginRevision"; payload: BeginRevisionPayload }
  | { action: "resendQuote"; payload: ResendQuotePayload }
  | { action: "cancel"; payload: CancelPayload }
  | { action: "reopen"; payload: ReopenPayload }
  | { action: "sendBackForRevision"; payload: SendBackForRevisionPayload }
  | { action: "createScheduleEntry"; payload: CreateScheduleEntryPayload };

// -----------------------------------------------------------------------
// Result
// -----------------------------------------------------------------------

export interface WorkflowResult {
  success: boolean;
  /** Previous CW status ID, null when the status did not change. */
  previousStatusId: number | null;
  /** New CW status ID after the transition, null on failure. */
  newStatusId: number | null;
  /** Human-readable status keys. */
  previousStatus: OpportunityStatusKey | null;
  newStatus: OpportunityStatusKey | null;
  /** Activities created during the transition. */
  activitiesCreated: ActivityController[];
  /** Cold-check result, when applicable. */
  coldCheck: ColdCheckResult | null;
  /** Error message on failure. */
  error: string | null;
}

// ═══════════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════════

function hasPermission(user: WorkflowUser, node: string): boolean {
  return user.permissions.includes("*") || user.permissions.includes(node);
}

function fail(
  message: string,
  currentStatusId?: number | null,
): WorkflowResult {
  return {
    success: false,
    previousStatusId: currentStatusId ?? null,
    newStatusId: null,
    previousStatus:
      currentStatusId != null ? (StatusIdToKey[currentStatusId] ?? null) : null,
    newStatus: null,
    activitiesCreated: [],
    coldCheck: null,
    error: message,
  };
}

function ok(
  previousStatusId: number,
  newStatusId: number,
  activities: ActivityController[],
  coldCheck: ColdCheckResult | null = null,
): WorkflowResult {
  return {
    success: true,
    previousStatusId,
    newStatusId,
    previousStatus: StatusIdToKey[previousStatusId] ?? null,
    newStatus: StatusIdToKey[newStatusId] ?? null,
    activitiesCreated: activities,
    coldCheck,
    error: null,
  };
}

/**
 * Build the `customFields` array for a CW activity with Optima_Type set,
 * and optionally a QuoteID, CloseDate, or ParentActivity.
 */
function buildCustomFields(
  optimaType: OptimaTypeValue,
  opts?: { quoteId?: string; closeDate?: string; parentActivityCwId?: number },
) {
  const fields: any[] = [
    {
      id: OptimaType.FIELD_ID,
      caption: "Optima_Type",
      type: "Text",
      entryMethod: "List",
      numberOfDecimals: 0,
      value: optimaType,
    },
  ];

  if (opts?.quoteId) {
    fields.push({
      id: QUOTE_ID_FIELD_ID,
      caption: "QuoteID",
      type: "Text",
      entryMethod: "EntryField",
      numberOfDecimals: 0,
      value: opts.quoteId,
    });
  }

  if (opts?.closeDate) {
    fields.push({
      id: CLOSE_DATE_FIELD_ID,
      caption: "Close Date",
      type: "Text",
      entryMethod: "Date",
      numberOfDecimals: 0,
      value: opts.closeDate,
    });
  }

  if (opts?.parentActivityCwId != null) {
    fields.push({
      id: PARENT_ACTIVITY_FIELD_ID,
      caption: "Parent_Activity",
      type: "Text",
      entryMethod: "EntryField",
      numberOfDecimals: 0,
      value: String(opts.parentActivityCwId),
    });
  }

  return fields;
}

/**
 * Create a CW activity for a workflow transition.
 */
export async function createWorkflowActivity(opts: {
  name: string;
  opportunityCwId: number;
  companyCwId: number | null;
  assignToCwMemberId: number;
  notes: string;
  optimaType: OptimaTypeValue;
  quoteId?: string;
  dateStart?: string;
  dateEnd?: string;
  parentActivityCwId?: number | null;
}): Promise<ActivityController> {
  const shouldStayOpen = STAYS_OPEN_TYPES.has(opts.optimaType);

  const activity = await ActivityController.create({
    name: opts.name,
    type: { id: 3 }, // HistoricEntry
    opportunity: { id: opts.opportunityCwId },
    ...(opts.companyCwId ? { company: { id: opts.companyCwId } } : {}),
    assignTo: { id: opts.assignToCwMemberId },
    ...(shouldStayOpen ? {} : { status: { id: 2 } }), // Closed unless stays-open
    notes: opts.notes,
  });

  // Set custom fields (Optima_Type + optional QuoteID + closeDate for closed activities).
  // For non-stays-open types, also re-assert Closed status.
  const now = shouldStayOpen ? undefined : new Date().toISOString();
  const patchOps: any[] = [
    {
      op: "replace",
      path: "customFields",
      value: buildCustomFields(opts.optimaType, {
        quoteId: opts.quoteId,
        closeDate: now,
        parentActivityCwId: opts.parentActivityCwId ?? undefined,
      }),
    },
  ];

  if (!shouldStayOpen) {
    patchOps.push({
      op: "replace",
      path: "status",
      value: { id: 2 }, // Closed
    });
  }

  const patched = await activity.update(patchOps);
  return patched;
}

/**
 * Resolve the parent activity CW ID for a newly generated quote activity.
 *
 * Finds the most recently created workflow activity (by CW ID descending) for
 * the opportunity, excluding QuoteGenerated and ScheduleEntry types. This
 * ensures the quote activity is nested under the current workflow state's
 * activity regardless of whether that activity is open or closed.
 */
export async function resolveQuoteParentActivityCwId(
  opportunityCwId: number,
): Promise<number | null> {
  try {
    const existingActivities = await activityCw.fetchByOpportunityDirect(opportunityCwId);
    // Sort descending by CW id so the most recently created comes first
    const sorted = [...existingActivities].sort((a, b) => (b.id ?? 0) - (a.id ?? 0));
    for (const raw of sorted) {
      const optimaField = raw.customFields?.find(
        (f: any) => f.id === OptimaType.FIELD_ID,
      );
      if (!optimaField?.value) continue;
      // Skip QuoteGenerated and ScheduleEntry — these should not be parents
      if (optimaField.value === OptimaType.QuoteGenerated) continue;
      if (optimaField.value === OptimaType.ScheduleEntry) continue;
      return raw.id;
    }
    return null;
  } catch (err) {
    console.warn(`[Workflow:QuoteParent] Could not resolve parent activity: ${err}`);
    return null;
  }
}

/**
 * Handle optional time entry: submit to CW if timeStart and timeEnd are provided.
 */
async function handleTimeEntry(
  activityCwId: number | undefined,
  cwMemberId: number,
  payload: BaseActionPayload,
  notes: string,
): Promise<void> {
  console.log(
    `[Workflow:TimeEntry] Called — activityCwId=${activityCwId}, cwMemberId=${cwMemberId}, timeStarted=${payload.timeStarted ?? "MISSING"}, timeEnded=${payload.timeEnded ?? "MISSING"}`,
  );

  if (activityCwId == null) {
    console.warn(
      `[Workflow:TimeEntry] SKIPPED — activityCwId is ${activityCwId} (activity was not created or has no CW ID)`,
    );
    return;
  }
  if (!payload.timeStarted || !payload.timeEnded) {
    console.warn(
      `[Workflow:TimeEntry] SKIPPED — timeStarted or timeEnded not provided (timeStarted=${payload.timeStarted ?? "undefined"}, timeEnded=${payload.timeEnded ?? "undefined"})`,
    );
    return;
  }

  console.log(
    `[Workflow:TimeEntry] Submitting — activity=${activityCwId}, member=${cwMemberId}, start=${payload.timeStarted}, end=${payload.timeEnded}, notes="${notes}"`,
  );
  try {
    const result = await submitTimeEntry({
      activityId: activityCwId,
      cwMemberId,
      timeStart: payload.timeStarted,
      timeEnd: payload.timeEnded,
      notes,
    });
    if (result.success) {
      console.log(
        `[Workflow:TimeEntry] SUCCESS — cwTimeEntryId=${result.cwTimeEntryId}, message="${result.message}"`,
      );
    } else {
      console.error(`[Workflow:TimeEntry] FAILED — ${result.message}`);
    }
  } catch (err: any) {
    console.error(
      `[Workflow:TimeEntry] EXCEPTION —`,
      err?.response?.data ?? err?.message ?? err,
    );
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// GUARDS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Top-level guard: opportunity stage must be "Optima".
 */
function assertOptimaStage(opportunity: OpportunityController): string | null {
  if (opportunity.stageName !== REQUIRED_STAGE) {
    return `Workflow actions require the opportunity stage to be "${REQUIRED_STAGE}". Current stage: "${opportunity.stageName ?? "(none)"}"`;
  }
  return null;
}

/**
 * Guard: opportunity must not be in a terminal status.
 */
function assertNotTerminal(statusCwId: number | null): string | null {
  if (statusCwId != null && TERMINAL_STATUSES.has(statusCwId)) {
    const key = StatusIdToKey[statusCwId] ?? "Unknown";
    return `Opportunity is in terminal status "${key}" and cannot be modified.`;
  }
  return null;
}

/**
 * Guard: transition must be allowed by the transition map.
 */
function assertTransitionAllowed(
  fromStatusId: number,
  toStatusId: number,
): string | null {
  const allowed = ALLOWED_TRANSITIONS[fromStatusId];
  if (!allowed || !allowed.has(toStatusId)) {
    const from = StatusIdToKey[fromStatusId] ?? String(fromStatusId);
    const to = StatusIdToKey[toStatusId] ?? String(toStatusId);
    return `Transition from "${from}" to "${to}" is not allowed.`;
  }
  return null;
}

/**
 * Guard: note must be non-empty.
 */
function assertNotePresent(note: string | undefined): string | null {
  if (!note || note.trim().length === 0) {
    return "A non-empty note is required for this action.";
  }
  return null;
}

// ═══════════════════════════════════════════════════════════════════════════
// TRANSITION FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════

/**
 * PendingNew → New
 *
 * Accept/setup an opportunity.
 */
export async function transitionToNew(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: AcceptNewPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId ?? OpportunityStatus.PendingNew;
  const targetStatus = OpportunityStatus.New;

  const err = assertTransitionAllowed(currentStatus, targetStatus);
  if (err) return fail(err, currentStatus);

  const activities: ActivityController[] = [];

  // Create setup activity
  const activity = await createWorkflowActivity({
    name: `[Workflow] Opportunity accepted — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note ?? "Opportunity accepted and moved to New.",
    optimaType: OptimaType.OpportunitySetup,
  });
  activities.push(activity);

  // Sync status to CW
  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note ?? "Opportunity accepted.",
  );

  return ok(currentStatus, targetStatus, activities);
}

/**
 * Any pre-confirmed status → InternalReview
 *
 * Manually request internal review. Requires a note.
 */
export async function transitionToInternalReview(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: RequestReviewPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const targetStatus = OpportunityStatus.InternalReview;

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  if (!hasPermission(user, WorkflowPermissions.REVIEW)) {
    return fail(
      `User lacks the "${WorkflowPermissions.REVIEW}" permission required to submit an opportunity for internal review.`,
      currentStatus,
    );
  }

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] Sent to Internal Review — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.OpportunityReview,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );

  return ok(currentStatus, targetStatus, activities);
}

/**
 * InternalReview → ReadyToSend | PendingRevision | QuoteSent | Canceled
 *
 * Reviewer makes a decision on an opportunity in InternalReview.
 */
export async function handleReviewDecision(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: ReviewDecisionPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  if (currentStatus !== OpportunityStatus.InternalReview) {
    return fail(
      `Review decisions can only be made when the opportunity is in InternalReview. Current status: "${StatusIdToKey[currentStatus] ?? "Unknown"}"`,
      currentStatus,
    );
  }

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  const activities: ActivityController[] = [];

  switch (payload.decision) {
    // ── Approve → ReadyToSend ──────────────────────────────────────────
    case "approve": {
      const targetStatus = OpportunityStatus.ReadyToSend;
      const transErr = assertTransitionAllowed(currentStatus, targetStatus);
      if (transErr) return fail(transErr, currentStatus);

      const activity = await createWorkflowActivity({
        name: `[Workflow] Review approved — ${opportunity.name}`,
        opportunityCwId: opportunity.cwOpportunityId,
        companyCwId: opportunity.companyCwId,
        assignToCwMemberId: user.cwMemberId,
        notes: payload.note,
        optimaType: OptimaType.OpportunityReview,
      });
      activities.push(activity);

      await syncOpportunityStatus({
        opportunityId: opportunity.cwOpportunityId,
        statusCwId: targetStatus,
      });

      await handleTimeEntry(
        activities[0]?.cwActivityId,
        user.cwMemberId,
        payload,
        payload.note,
      );
      return ok(currentStatus, targetStatus, activities);
    }

    // ── Reject → PendingRevision ──────────────────────────────────────
    case "reject": {
      const targetStatus = OpportunityStatus.PendingRevision;
      const transErr = assertTransitionAllowed(currentStatus, targetStatus);
      if (transErr) return fail(transErr, currentStatus);

      const activity = await createWorkflowActivity({
        name: `[Workflow] Review rejected — revision required — ${opportunity.name}`,
        opportunityCwId: opportunity.cwOpportunityId,
        companyCwId: opportunity.companyCwId,
        assignToCwMemberId: user.cwMemberId,
        notes: payload.note,
        optimaType: OptimaType.Revision,
      });
      activities.push(activity);

      await syncOpportunityStatus({
        opportunityId: opportunity.cwOpportunityId,
        statusCwId: targetStatus,
      });

      await handleTimeEntry(
        activities[0]?.cwActivityId,
        user.cwMemberId,
        payload,
        payload.note,
      );
      return ok(currentStatus, targetStatus, activities);
    }

    // ── Send directly (reviewer manually sends) ──────────────────────
    case "send": {
      const targetStatus = OpportunityStatus.QuoteSent;
      const transErr = assertTransitionAllowed(currentStatus, targetStatus);
      if (transErr) return fail(transErr, currentStatus);

      // Create approved activity
      const approvedActivity = await createWorkflowActivity({
        name: `[Workflow] Review approved — ${opportunity.name}`,
        opportunityCwId: opportunity.cwOpportunityId,
        companyCwId: opportunity.companyCwId,
        assignToCwMemberId: user.cwMemberId,
        notes: `Review approved: ${payload.note}`,
        optimaType: OptimaType.OpportunityReview,
      });
      activities.push(approvedActivity);

      // Create quote-sent activity
      const sentActivity = await createWorkflowActivity({
        name: `[Workflow] Quote sent (by reviewer) — ${opportunity.name}`,
        opportunityCwId: opportunity.cwOpportunityId,
        companyCwId: opportunity.companyCwId,
        assignToCwMemberId: user.cwMemberId,
        notes: `Quote sent by reviewer: ${payload.note}`,
        optimaType: OptimaType.QuoteSent,
      });
      activities.push(sentActivity);

      await syncOpportunityStatus({
        opportunityId: opportunity.cwOpportunityId,
        statusCwId: targetStatus,
      });

      await handleTimeEntry(
        activities[0]?.cwActivityId,
        user.cwMemberId,
        payload,
        payload.note,
      );
      return ok(currentStatus, targetStatus, activities);
    }

    // ── Cancel from review ────────────────────────────────────────────
    case "cancel": {
      const targetStatus = OpportunityStatus.Canceled;
      const transErr = assertTransitionAllowed(currentStatus, targetStatus);
      if (transErr) return fail(transErr, currentStatus);

      if (!hasPermission(user, WorkflowPermissions.CANCEL)) {
        return fail(
          `User lacks the "${WorkflowPermissions.CANCEL}" permission required to cancel an opportunity.`,
          currentStatus,
        );
      }

      const activity = await createWorkflowActivity({
        name: `[Workflow] Canceled from review — ${opportunity.name}`,
        opportunityCwId: opportunity.cwOpportunityId,
        companyCwId: opportunity.companyCwId,
        assignToCwMemberId: user.cwMemberId,
        notes: payload.note,
        optimaType: OptimaType.Finalized,
      });
      activities.push(activity);

      await syncOpportunityStatus({
        opportunityId: opportunity.cwOpportunityId,
        statusCwId: targetStatus,
      });

      await handleTimeEntry(
        activities[0]?.cwActivityId,
        user.cwMemberId,
        payload,
        payload.note,
      );
      return ok(currentStatus, targetStatus, activities);
    }

    default:
      return fail(
        `Unknown review decision: "${(payload as any).decision}"`,
        currentStatus,
      );
  }
}

/**
 * ReadyToSend/PendingSent(deprecated) → QuoteSent (and its compound transitions)
 *
 * Also handles New → QuoteSent (direct send, skipping review) and
 * Active → QuoteSent (re-send after revision).
 *
 * Payload flags:
 *   quoteConfirmed — sent + confirmed simultaneously → ConfirmedQuote
 *   won            — presented and won → Won/PendingWon
 *   lost           — rejected immediately → PendingLost
 *   needsRevision  — needs revision → Active
 */
export async function transitionToQuoteSent(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: SendQuotePayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  if (!hasPermission(user, WorkflowPermissions.SEND)) {
    return fail(
      `User lacks the "${WorkflowPermissions.SEND}" permission required to send a quote.`,
      currentStatus,
    );
  }

  // Validate source status allows transition to QuoteSent
  const transErr = assertTransitionAllowed(
    currentStatus,
    OpportunityStatus.QuoteSent,
  );
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];
  const now = new Date().toISOString();

  // ── won flag: presented and won immediately ─────────────────────────
  if (payload.won) {
    if (!hasPermission(user, WorkflowPermissions.WIN)) {
      return fail(
        `User lacks the "${WorkflowPermissions.WIN}" permission required to mark an opportunity as won.`,
        currentStatus,
      );
    }

    const wantsFinalize = !!payload.finalize;
    const canFinalize =
      wantsFinalize && hasPermission(user, WorkflowPermissions.FINALIZE);

    if (wantsFinalize && !canFinalize) {
      return fail(
        `User lacks the "${WorkflowPermissions.FINALIZE}" permission required to finalize an opportunity.`,
        currentStatus,
      );
    }

    const targetStatus = canFinalize
      ? OpportunityStatus.Won
      : OpportunityStatus.PendingWon;

    // Combined Sent & Confirmed activity
    const sentConfirmedActivity = await createWorkflowActivity({
      name: `[Workflow] Quote sent & confirmed — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes:
        `Quote sent and confirmed simultaneously (immediate win). ${payload.note ?? ""}`.trim(),
      optimaType: OptimaType.QuoteSentConfirmed,
    });
    activities.push(sentConfirmedActivity);

    // Won/PendingWon activity
    const outcomeType = canFinalize
      ? OptimaType.Converted
      : OptimaType.Finalized;
    const wonActivity = await createWorkflowActivity({
      name: `[Workflow] ${canFinalize ? "Won" : "Pending Won"} — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes:
        payload.note ?? `Opportunity ${canFinalize ? "won" : "pending won"}.`,
      optimaType: outcomeType,
    });
    activities.push(wonActivity);

    await syncOpportunityStatus({
      opportunityId: opportunity.cwOpportunityId,
      statusCwId: targetStatus,
    });

    await handleTimeEntry(
      activities[0]?.cwActivityId,
      user.cwMemberId,
      payload,
      payload.note ?? "Immediate win.",
    );
    return ok(currentStatus, targetStatus, activities);
  }

  // ── lost flag: rejected immediately ─────────────────────────────────
  if (payload.lost) {
    if (!hasPermission(user, WorkflowPermissions.LOSE)) {
      return fail(
        `User lacks the "${WorkflowPermissions.LOSE}" permission required to mark an opportunity as lost.`,
        currentStatus,
      );
    }

    const wantsFinalize = !!payload.finalize;
    const canFinalize =
      wantsFinalize && hasPermission(user, WorkflowPermissions.FINALIZE);

    if (wantsFinalize && !canFinalize) {
      return fail(
        `User lacks the "${WorkflowPermissions.FINALIZE}" permission required to finalize an opportunity.`,
        currentStatus,
      );
    }

    const targetStatus = canFinalize
      ? OpportunityStatus.Lost
      : OpportunityStatus.PendingLost;

    const wasAlsoConfirmed = !!payload.quoteConfirmed;

    const sentActivity = await createWorkflowActivity({
      name: wasAlsoConfirmed
        ? `[Workflow] Quote sent & confirmed — ${opportunity.name}`
        : `[Workflow] Quote sent — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes: wasAlsoConfirmed
        ? `Quote sent and confirmed, but rejected. ${payload.note ?? ""}`.trim()
        : `Quote sent. ${payload.note ?? ""}`.trim(),
      optimaType: wasAlsoConfirmed
        ? OptimaType.QuoteSentConfirmed
        : OptimaType.QuoteSent,
    });
    activities.push(sentActivity);

    const lostActivity = await createWorkflowActivity({
      name: canFinalize
        ? `[Workflow] Lost (immediate rejection) — ${opportunity.name}`
        : `[Workflow] Pending Lost (immediate rejection) — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes: payload.note ?? "Quote rejected immediately upon presentation.",
      optimaType: canFinalize ? OptimaType.Converted : OptimaType.Finalized,
    });
    activities.push(lostActivity);

    await syncOpportunityStatus({
      opportunityId: opportunity.cwOpportunityId,
      statusCwId: targetStatus,
    });

    await handleTimeEntry(
      activities[0]?.cwActivityId,
      user.cwMemberId,
      payload,
      payload.note ?? "Immediate rejection.",
    );
    return ok(currentStatus, targetStatus, activities);
  }

  // ── needsRevision flag → Active ──────────────────────────────────────
  if (payload.needsRevision) {
    const targetStatus = OpportunityStatus.Active;

    const wasAlsoConfirmed = !!payload.quoteConfirmed;

    const sentActivity = await createWorkflowActivity({
      name: wasAlsoConfirmed
        ? `[Workflow] Quote sent & confirmed — ${opportunity.name}`
        : `[Workflow] Quote sent — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes: wasAlsoConfirmed
        ? `Quote sent and confirmed, but revision needed. ${payload.note ?? ""}`.trim()
        : `Quote sent. ${payload.note ?? ""}`.trim(),
      optimaType: wasAlsoConfirmed
        ? OptimaType.QuoteSentConfirmed
        : OptimaType.QuoteSent,
    });
    activities.push(sentActivity);

    const revisionActivity = await createWorkflowActivity({
      name: `[Workflow] Revision needed — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes: payload.note ?? "Revision requested after quote presentation.",
      optimaType: OptimaType.Revision,
    });
    activities.push(revisionActivity);

    await syncOpportunityStatus({
      opportunityId: opportunity.cwOpportunityId,
      statusCwId: targetStatus,
    });

    await handleTimeEntry(
      activities[0]?.cwActivityId,
      user.cwMemberId,
      payload,
      payload.note ?? "Needs revision.",
    );
    return ok(currentStatus, targetStatus, activities);
  }

  // ── quoteConfirmed flag → ConfirmedQuote ─────────────────────────────
  if (payload.quoteConfirmed) {
    const targetStatus = OpportunityStatus.ConfirmedQuote;

    const sentConfirmedActivity = await createWorkflowActivity({
      name: `[Workflow] Quote sent & confirmed — ${opportunity.name}`,
      opportunityCwId: opportunity.cwOpportunityId,
      companyCwId: opportunity.companyCwId,
      assignToCwMemberId: user.cwMemberId,
      notes:
        `Quote sent and confirmed simultaneously. ${payload.note ?? ""}`.trim(),
      optimaType: OptimaType.QuoteSentConfirmed,
    });
    activities.push(sentConfirmedActivity);

    await syncOpportunityStatus({
      opportunityId: opportunity.cwOpportunityId,
      statusCwId: targetStatus,
    });

    await handleTimeEntry(
      activities[0]?.cwActivityId,
      user.cwMemberId,
      payload,
      payload.note ?? "Quote sent and confirmed.",
    );
    return ok(currentStatus, targetStatus, activities);
  }

  // ── Default: plain QuoteSent (no flags) ──────────────────────────────
  const targetStatus = OpportunityStatus.QuoteSent;

  const sentActivity = await createWorkflowActivity({
    name: `[Workflow] Quote sent — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: `Quote sent. ${payload.note ?? ""}`.trim(),
    optimaType: OptimaType.QuoteSent,
  });
  activities.push(sentActivity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note ?? "Quote sent.",
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * New/Active/PendingSent(deprecated) → ReadyToSend
 *
 * Allows staging an opportunity as ready without immediately sending.
 */
export async function transitionToReadyToSend(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: MarkReadyToSendPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  if (!hasPermission(user, WorkflowPermissions.SEND)) {
    return fail(
      `User lacks the "${WorkflowPermissions.SEND}" permission required to mark an opportunity ready to send.`,
      currentStatus,
    );
  }

  const targetStatus = OpportunityStatus.ReadyToSend;
  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activity = await createWorkflowActivity({
    name: `[Workflow] Marked ready to send — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note ?? "Marked ready to send.",
    optimaType: OptimaType.OpportunityReview,
  });

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activity.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note ?? "Marked ready to send.",
  );

  return ok(currentStatus, targetStatus, [activity]);
}

/**
 * QuoteSent → ConfirmedQuote
 *
 * Customer has acknowledged receipt of the quote.
 */
export async function transitionToConfirmedQuote(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: ConfirmQuotePayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const targetStatus = OpportunityStatus.ConfirmedQuote;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] Quote confirmed — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note ?? "Customer confirmed receipt of quote.",
    optimaType: OptimaType.QuoteConfirmed,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note ?? "Quote confirmed.",
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * PendingWon → Won   OR   PendingLost → Lost
 *
 * Finalize from a pending state. Requires `sales.opportunity.finalize`.
 *
 * Also used for direct Won/Lost from QuoteSent/ConfirmedQuote when
 * the user holds the finalize permission (called internally).
 */
export async function finalizeOpportunity(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: FinalizePayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  const requiredPerm =
    payload.outcome === "won"
      ? WorkflowPermissions.WIN
      : WorkflowPermissions.LOSE;
  if (!hasPermission(user, requiredPerm)) {
    return fail(
      `User lacks the "${requiredPerm}" permission required to mark an opportunity as ${payload.outcome}.`,
      currentStatus,
    );
  }

  if (!hasPermission(user, WorkflowPermissions.FINALIZE)) {
    return fail(
      `User lacks the "${WorkflowPermissions.FINALIZE}" permission required to finalize an opportunity.`,
      currentStatus,
    );
  }

  const targetStatus =
    payload.outcome === "won" ? OpportunityStatus.Won : OpportunityStatus.Lost;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const optimaType =
    payload.outcome === "won" ? OptimaType.Converted : OptimaType.Finalized;

  const activity = await createWorkflowActivity({
    name: `[Workflow] ${payload.outcome === "won" ? "Won" : "Lost"} — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * ConfirmedQuote/QuoteSent → PendingWon | PendingLost
 *
 * When the user DOES NOT have finalize permission, win/lose actions
 * route to the pending variant.
 */
export async function transitionToPending(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: FinalizePayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  const requiredPerm =
    payload.outcome === "won"
      ? WorkflowPermissions.WIN
      : WorkflowPermissions.LOSE;
  if (!hasPermission(user, requiredPerm)) {
    return fail(
      `User lacks the "${requiredPerm}" permission required to mark an opportunity as ${payload.outcome}.`,
      currentStatus,
    );
  }

  const targetStatus =
    payload.outcome === "won"
      ? OpportunityStatus.PendingWon
      : OpportunityStatus.PendingLost;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] ${payload.outcome === "won" ? "Pending Won" : "Pending Lost"} — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.Finalized,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * PendingWon / PendingLost → Active (resurrection / revision)
 *
 * Allows an admin (with finalize permission) to send a pending
 * opportunity back to Active for revision.
 */
export async function resurrectOpportunity(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: ResurrectPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  // Only finalize-permissioned users can resurrect from PendingWon
  if (
    currentStatus === OpportunityStatus.PendingWon &&
    !hasPermission(user, WorkflowPermissions.FINALIZE)
  ) {
    return fail(
      "You do not have permission to send a Pending Won opportunity back for revision.",
      currentStatus,
    );
  }

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  const targetStatus = OpportunityStatus.Active;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const fromLabel =
    currentStatus === OpportunityStatus.PendingWon
      ? "Pending Won"
      : "Pending Lost";
  const activity = await createWorkflowActivity({
    name: `[Workflow] Resurrected from ${fromLabel} — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.Revision,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * PendingRevision → Active
 *
 * Rep begins revising the opportunity after review rejection.
 */
export async function beginRevision(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: BeginRevisionPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const targetStatus = OpportunityStatus.Active;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] Revision started — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note ?? "Revision started after review rejection.",
    optimaType: OptimaType.Revision,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note ?? "Revision started.",
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * ConfirmedQuote → PendingRevision
 *
 * Sends the opportunity back for revision from ConfirmedQuote.
 * Requires a mandatory note explaining why.
 */
export async function sendBackForRevision(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: SendBackForRevisionPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  const targetStatus = OpportunityStatus.PendingRevision;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activity = await createWorkflowActivity({
    name: `[Workflow] Sent back for revision — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.Revision,
  });

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activity.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );

  return ok(currentStatus, targetStatus, [activity]);
}

/**
 * Create a Schedule Entry activity without changing the opportunity status.
 *
 * Schedule Entry activities stay open until time is logged against them.
 */
export async function createScheduleEntry(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: CreateScheduleEntryPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  // CW activities require ISO-8601 without milliseconds, e.g. "2026-04-19T20:15:00Z"
  const toCwDateTime = (iso: string): string => iso.replace(/\.\d+Z$/, "Z");

  const dateStart = payload.startTime
    ? toCwDateTime(payload.startTime)
    : payload.dueDate
      ? toCwDateTime(payload.dueDate)
      : undefined;
  const dateEnd = payload.endTime ? toCwDateTime(payload.endTime) : undefined;

  // Find the currently open workflow activity (OpportunitySetup, OpportunityReview,
  // or Revision) to use as the parent for this schedule entry.
  let parentActivityCwId: number | null = null;
  try {
    const existingActivities = await activityCw.fetchByOpportunityDirect(
      opportunity.cwOpportunityId,
    );
    for (const raw of existingActivities) {
      if (raw.status?.id === 2) continue; // already closed
      const optimaField = raw.customFields?.find(
        (f: any) => f.id === OptimaType.FIELD_ID,
      );
      if (!optimaField?.value) continue;
      if (optimaField.value === OptimaType.ScheduleEntry) continue; // skip other schedule entries
      if (STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) {
        parentActivityCwId = raw.id;
        break;
      }
    }
  } catch (err) {
    // Non-fatal — schedule entry will be created without a parent
    console.warn(
      `[Workflow:ScheduleEntry] Could not resolve parent activity: ${err}`,
    );
  }

  const activity = await ActivityController.create({
    name: `[Schedule Entry] ${payload.activityTypeValue} — ${opportunity.name}`,
    type: { id: 3 }, // HistoricEntry
    opportunity: { id: opportunity.cwOpportunityId },
    ...(opportunity.companyCwId ? { company: { id: opportunity.companyCwId } } : {}),
    assignTo: { id: user.cwMemberId },
    notes: payload.note ?? "",
    ...(dateStart ? { dateStart } : {}),
    ...(dateEnd ? { dateEnd } : {}),
  });

  // Build custom fields: always Optima_Type, plus Parent_Activity when resolved
  const customFields: any[] = [
    {
      id: OptimaType.FIELD_ID,
      caption: "Optima_Type",
      type: "Text",
      entryMethod: "List",
      numberOfDecimals: 0,
      value: OptimaType.ScheduleEntry,
    },
  ];

  if (parentActivityCwId != null) {
    customFields.push({
      id: PARENT_ACTIVITY_FIELD_ID,
      caption: "Parent_Activity",
      type: "Text",
      entryMethod: "EntryField",
      numberOfDecimals: 0,
      value: String(parentActivityCwId),
    });
  }

  // Set Optima_Type (+ Parent_Activity) on the new schedule entry
  await activity.update([
    {
      op: "replace",
      path: "customFields",
      value: customFields,
    },
  ]);

  // Return a no-transition result (status unchanged)
  return {
    success: true,
    previousStatusId: currentStatus,
    newStatusId: currentStatus,
    previousStatus: StatusIdToKey[currentStatus] ?? null,
    newStatus: StatusIdToKey[currentStatus] ?? null,
    activitiesCreated: [activity],
    coldCheck: null,
    error: null,
  };
}

/**
 * Any cancelable status → Canceled
 *
 * Requires `sales.opportunity.cancel` permission.
 */
export async function cancelOpportunity(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: CancelPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  if (!hasPermission(user, WorkflowPermissions.CANCEL)) {
    return fail(
      `User lacks the "${WorkflowPermissions.CANCEL}" permission required to cancel an opportunity.`,
      currentStatus,
    );
  }

  const targetStatus = OpportunityStatus.Canceled;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] Canceled — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.Finalized,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * Canceled → Active (re-open)
 *
 * Dedicated re-open action. Creates an audit trail activity with a
 * required note explaining why the opportunity is being re-opened.
 */
export async function reopenCancelledOpportunity(
  opportunity: OpportunityController,
  user: WorkflowUser,
  payload: ReopenPayload,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const noteErr = assertNotePresent(payload.note);
  if (noteErr) return fail(noteErr, currentStatus);

  if (!hasPermission(user, WorkflowPermissions.REOPEN)) {
    return fail(
      `User lacks the "${WorkflowPermissions.REOPEN}" permission required to re-open a cancelled opportunity.`,
      currentStatus,
    );
  }

  const targetStatus = OpportunityStatus.Active;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) return fail(transErr, currentStatus);

  const activities: ActivityController[] = [];

  const activity = await createWorkflowActivity({
    name: `[Workflow] Re-opened from Canceled — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: user.cwMemberId,
    notes: payload.note,
    optimaType: OptimaType.Revision,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  await handleTimeEntry(
    activities[0]?.cwActivityId,
    user.cwMemberId,
    payload,
    payload.note,
  );
  return ok(currentStatus, targetStatus, activities);
}

/**
 * Cold detection automation trigger.
 *
 * → InternalReview (system-generated)
 *
 * Called by automation/scheduler, not by a user action.
 * Evaluates the cold threshold and, if cold, transitions the
 * opportunity to InternalReview with a system-generated activity.
 */
export async function triggerColdDetection(
  opportunity: OpportunityController,
  lastActivityDate: Date | null,
): Promise<WorkflowResult> {
  const currentStatus = opportunity.statusCwId;
  if (currentStatus == null) return fail("Opportunity has no current status.");

  const coldResult = checkColdStatus({
    statusCwId: currentStatus,
    lastActivityDate,
  });

  if (!coldResult.cold) {
    return {
      success: true,
      previousStatusId: currentStatus,
      newStatusId: currentStatus,
      previousStatus: StatusIdToKey[currentStatus] ?? null,
      newStatus: StatusIdToKey[currentStatus] ?? null,
      activitiesCreated: [],
      coldCheck: coldResult,
      error: null,
    };
  }

  const targetStatus = OpportunityStatus.InternalReview;

  const transErr = assertTransitionAllowed(currentStatus, targetStatus);
  if (transErr) {
    return {
      ...fail(transErr, currentStatus),
      coldCheck: coldResult,
    };
  }

  const activities: ActivityController[] = [];

  // System-generated activity — no user assignment, use a system marker.
  // We use assignTo with a placeholder; the caller should provide a
  // system member ID when integrating.
  const trigger = coldResult.triggeredBy!;
  const activity = await createWorkflowActivity({
    name: `[Workflow][Auto] Cold detected — ${opportunity.name}`,
    opportunityCwId: opportunity.cwOpportunityId,
    companyCwId: opportunity.companyCwId,
    assignToCwMemberId: 0, // system — will be resolved by caller
    notes:
      `Opportunity went cold. Status "${trigger.statusName}" exceeded ` +
      `${trigger.thresholdDays}-day threshold (stale for ${trigger.staleDays} days). ` +
      `Automatically moved to Internal Review.`,
    optimaType: OptimaType.OpportunityReview,
  });
  activities.push(activity);

  await syncOpportunityStatus({
    opportunityId: opportunity.cwOpportunityId,
    statusCwId: targetStatus,
  });

  return {
    ...ok(currentStatus, targetStatus, activities),
    coldCheck: coldResult,
  };
}

// ═══════════════════════════════════════════════════════════════════════════
// CLOSE OPEN WORKFLOW ACTIVITIES
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Close any open workflow activities for this opportunity.
 *
 * Called at the start of every workflow transition so that activities
 * marked as "stays open" (Opportunity Setup, Opportunity Review, Revision)
 * are closed when the opportunity moves to the next stage.
 */
async function closeOpenWorkflowActivities(
  opportunityCwId: number,
): Promise<void> {
  try {
    const activities =
      await activityCw.fetchByOpportunityDirect(opportunityCwId);

    for (const raw of activities) {
      // Only close Open activities (status id !== 2)
      if (raw.status?.id === 2) continue;

      // Only close activities that have a workflow Optima_Type custom field
      const optimaField = raw.customFields?.find(
        (f: any) => f.id === OptimaType.FIELD_ID,
      );
      if (!optimaField?.value) continue;

      // Only close activities whose type is in the stays-open set
      if (!STAYS_OPEN_TYPES.has(optimaField.value as OptimaTypeValue)) continue;

      // Never auto-close Schedule Entry activities — they close only when time is logged
      if (optimaField.value === OptimaType.ScheduleEntry) continue;

      const closeDate = new Date().toISOString();
      const existingFields = (raw.customFields ?? []).map((f: any) =>
        f.id === CLOSE_DATE_FIELD_ID ? { ...f, value: closeDate } : f,
      );
      // Add Close Date if it wasn't already present
      if (!existingFields.some((f: any) => f.id === CLOSE_DATE_FIELD_ID)) {
        existingFields.push({
          id: CLOSE_DATE_FIELD_ID,
          caption: "Close Date",
          type: "Text",
          entryMethod: "Date",
          numberOfDecimals: 0,
          value: closeDate,
        });
      }

      const controller = new ActivityController(raw);
      await controller.update([
        { op: "replace", path: "status", value: { id: 2 } },
        { op: "replace", path: "customFields", value: existingFields },
      ]);
    }
  } catch (err) {
    console.error(
      `[Workflow] Failed to close open activities for opportunity ${opportunityCwId}:`,
      err,
    );
    // Non-fatal — don't block the transition
  }
}

// ═══════════════════════════════════════════════════════════════════════════
// MASTER DISPATCHER
// ═══════════════════════════════════════════════════════════════════════════

/**
 * Process an opportunity workflow action.
 *
 * This is the single entry point for all workflow transitions. It:
 *   1. Validates the opportunity stage is "Optima".
 *   2. Validates the opportunity is not in a terminal status.
 *   3. Routes the action to the correct transition function.
 *   4. On success, refreshes the opportunity from CW to keep
 *      the local database in sync.
 *
 * @param opportunity - The OpportunityController instance to act on.
 * @param action      - The discriminated action union.
 * @param user        - The authenticated user performing the action.
 */
export async function processOpportunityAction(
  opportunity: OpportunityController,
  { action, payload }: WorkflowAction,
  user: WorkflowUser,
): Promise<WorkflowResult> {
  // ── Stage gate ──────────────────────────────────────────────────────
  const stageErr = assertOptimaStage(opportunity);
  if (stageErr) return fail(stageErr, opportunity.statusCwId);

  // ── Terminal gate (except reopen which starts from Canceled) ────────
  if (action !== "reopen") {
    const termErr = assertNotTerminal(opportunity.statusCwId);
    if (termErr) return fail(termErr, opportunity.statusCwId);
  }

  // ── Close any open workflow activities from previous stage ──────────
  // Skip for createScheduleEntry — we intentionally preserve open activities.
  if (action !== "createScheduleEntry") {
    await closeOpenWorkflowActivities(opportunity.cwOpportunityId);
  }

  // ── Route to transition function ────────────────────────────────────
  let result: WorkflowResult;

  switch (action) {
    case "acceptNew":
      result = await transitionToNew(opportunity, user, payload);
      break;

    case "requestReview":
      result = await transitionToInternalReview(opportunity, user, payload);
      break;

    case "reviewDecision":
      result = await handleReviewDecision(opportunity, user, payload);
      break;

    case "markReadyToSend":
      result = await transitionToReadyToSend(opportunity, user, payload);
      break;

    case "sendQuote":
      result = await transitionToQuoteSent(opportunity, user, payload);
      break;

    case "confirmQuote":
      result = await transitionToConfirmedQuote(opportunity, user, payload);
      break;

    case "finalize": {
      // If user has finalize permission, go directly to Won/Lost.
      // Otherwise, go to PendingWon/PendingLost.
      result = hasPermission(user, WorkflowPermissions.FINALIZE)
        ? await finalizeOpportunity(opportunity, user, payload)
        : await transitionToPending(opportunity, user, payload);
      break;
    }

    case "resurrect":
      result = await resurrectOpportunity(opportunity, user, payload);
      break;

    case "beginRevision":
      result = await beginRevision(opportunity, user, payload);
      break;

    case "resendQuote":
      result = await transitionToQuoteSent(opportunity, user, payload);
      break;

    case "cancel":
      result = await cancelOpportunity(opportunity, user, payload);
      break;

    case "reopen":
      result = await reopenCancelledOpportunity(opportunity, user, payload);
      break;

    case "sendBackForRevision":
      result = await sendBackForRevision(opportunity, user, payload);
      break;

    case "createScheduleEntry":
      // Schedule Entry does not close open activities — skip that step.
      // We call it directly rather than falling through closeOpenWorkflowActivities.
      result = await createScheduleEntry(opportunity, user, payload);
      break;

    default: {
      const _exhaustive: never = action;
      return fail(`Unknown workflow action: "${_exhaustive}"`);
    }
  }

  // ── Post-transition: refresh opportunity from CW → local DB ────────
  if (result.success) {
    try {
      await opportunity.refreshFromCW();
    } catch (refreshErr) {
      console.error(
        "[Workflow] Failed to refresh opportunity from CW after transition:",
        refreshErr,
      );
      // Don't fail the workflow — the transition itself succeeded.
    }
  }

  return result;
}
