// Pure, framework-free state machine for a single transcription job.
//
// A job moves through a fixed lifecycle:
//   pending -> decoding -> transcribing -> done
// with `error` and `cancelled` as alternative terminal states reachable from
// any non-terminal state. The reducer is PURE: it never mutates its input and
// always returns a fresh object. Events delivered to a terminal job are
// ignored (the same reference is returned), so callers can fire-and-forget.

/** Lifecycle states a job can occupy. */
export type JobStatus =
  | 'pending'
  | 'decoding'
  | 'transcribing'
  | 'done'
  | 'error'
  | 'cancelled';

/** Immutable snapshot of a job's progress through the pipeline. */
export interface Job {
  /** Stable, caller-assigned identity. */
  id: string;
  /** Current lifecycle state. */
  status: JobStatus;
  /** Number of chunks fully transcribed so far. */
  chunkIndex: number;
  /** Total chunks to transcribe; 0 until `startTranscribe`. */
  chunkCount: number;
  /** Human-readable failure reason; present only when status === 'error'. */
  error?: string;
}

/** Events that drive a job forward. */
export type JobEvent =
  | { type: 'startDecode' }
  | { type: 'startTranscribe'; chunkCount: number }
  /** `index` = total chunks now completed (cumulative, not a delta). */
  | { type: 'chunkDone'; index: number }
  | { type: 'complete' }
  | { type: 'fail'; error: string }
  | { type: 'cancel' };

/** Create a fresh job in the initial `pending` state. */
export function createJob(id: string): Job {
  return { id, status: 'pending', chunkIndex: 0, chunkCount: 0 };
}

/** True once the job can no longer change state. */
export function isTerminal(job: Job): boolean {
  return (
    job.status === 'done' ||
    job.status === 'error' ||
    job.status === 'cancelled'
  );
}

/**
 * Pure reducer. Returns a NEW Job for any state-changing event; returns the
 * SAME reference (unchanged) when the job is already terminal. Never mutates
 * `job`.
 */
export function reduceJob(job: Job, ev: JobEvent): Job {
  // Terminal jobs absorb all further events with no change.
  if (isTerminal(job)) return job;

  switch (ev.type) {
    case 'startDecode':
      return { ...job, status: 'decoding' };

    case 'startTranscribe':
      return {
        ...job,
        status: 'transcribing',
        chunkCount: ev.chunkCount,
      };

    case 'chunkDone': {
      // Progress is monotonic (never goes backward) and never exceeds the
      // known chunk count.
      const advanced = Math.max(job.chunkIndex, ev.index);
      const clamped = Math.min(advanced, job.chunkCount);
      return { ...job, chunkIndex: clamped };
    }

    case 'complete':
      return { ...job, status: 'done', chunkIndex: job.chunkCount };

    case 'fail':
      return { ...job, status: 'error', error: ev.error };

    case 'cancel':
      return { ...job, status: 'cancelled' };

    default: {
      // Exhaustiveness guard: if a new event type is added without handling,
      // this fails to compile.
      const _exhaustive: never = ev;
      return _exhaustive;
    }
  }
}

/**
 * Fractional progress in [0, 1]. A `done` job is always 1. Otherwise it is the
 * ratio of completed chunks to total, or 0 when the total is not yet known.
 */
export function jobProgress(job: Job): number {
  if (job.status === 'done') return 1;
  if (job.chunkCount > 0) return job.chunkIndex / job.chunkCount;
  return 0;
}
