import {
  PROTOCOL_VERSION,
  type CommandId,
  type ExecutionAttempt,
  type ExecutorCommand,
  type Message,
  type WorkerId,
} from '@platform-modules/chatgpt-orchestrator-protocol';
import type { OrchestratorService } from '@platform-modules/chatgpt-orchestrator-core';
import { workerBootstrap, workerFollowupPrompt } from './bootstrap.js';
import { workerResumePrompt } from './execution-prompts.js';
import type { ExecutorBridge } from './executor-bridge.js';
import { createSteeringCommand, deliveryCommandId } from './steering.js';

export interface ExecutionContinuitySchedulerOptions {
  reconcileIntervalMs?: number;
  commandDeadlineMs?: number;
}

function commandIdForExecution(executionId: string): CommandId {
  const separator = executionId.indexOf('_');
  if (separator < 0) throw new Error(`Cannot derive command ID from execution ${executionId}.`);
  return `cmd_${executionId.slice(separator + 1)}` as CommandId;
}

function deadlineFrom(timestamp: string, deltaMs: number): string {
  return new Date(Date.parse(timestamp) + deltaMs).toISOString();
}

function messageDeliveryKey(message: Message): string {
  return `message.delivery:${message.messageId}:${message.deliveryAttempts + 1}`;
}

function ordinaryMessageCommand(message: Message, deadlineMs: number): Extract<ExecutorCommand, { type: 'conversation.send' }> {
  const idempotencyKey = messageDeliveryKey(message);
  return {
    protocolVersion: PROTOCOL_VERSION,
    commandId: deliveryCommandId(idempotencyKey),
    type: 'conversation.send',
    workerId: message.toWorkerId,
    idempotencyKey,
    deadline: deadlineFrom(new Date().toISOString(), deadlineMs),
    prompt: message.body,
  };
}

export interface ExecutionCommandPromptOptions {
  bootstrap?: string;
  followupPrompt?: string;
  followupRequiresCreate?: boolean;
}

function fallbackFollowupPrompt(workerId: WorkerId): string {
  return `New orchestrator work is available for ${workerId}. Attach to this worker using the ChatGPT Orchestrator MCP, retrieve the current authoritative assignment, and continue until its acceptance criteria are satisfied.`;
}

export function commandForExecutionAttempt(
  attempt: ExecutionAttempt,
  commandDeadlineMs = 10 * 60_000,
  prompts: ExecutionCommandPromptOptions = {},
): ExecutorCommand {
  const base = {
    protocolVersion: PROTOCOL_VERSION,
    commandId: commandIdForExecution(attempt.executionId),
    workerId: attempt.workerId,
    idempotencyKey: `conversation.execution:${attempt.executionId}`,
    deadline: deadlineFrom(attempt.scheduledAt, commandDeadlineMs),
  } as const;

  if (attempt.reason === 'initial') {
    return { ...base, type: 'conversation.create', bootstrap: prompts.bootstrap ?? workerBootstrap(attempt.workerId) };
  }

  if (attempt.reason === 'followup') {
    if (prompts.followupRequiresCreate) {
      return { ...base, type: 'conversation.create', bootstrap: prompts.bootstrap ?? workerBootstrap(attempt.workerId) };
    }
    return { ...base, type: 'conversation.send', prompt: prompts.followupPrompt ?? fallbackFollowupPrompt(attempt.workerId) };
  }

  if (attempt.reason === 'recovery') {
    return { ...base, type: 'conversation.send', prompt: 'continue' };
  }

  return {
    ...base,
    type: 'conversation.send',
    prompt: workerResumePrompt({
      workerId: attempt.workerId,
      reason: attempt.reason,
      sequence: attempt.sequence,
    }),
  };
}

/**
 * Recoverable application scheduler. All eligibility, policy, stall, blocking,
 * attempt, and idempotency semantics remain in OrchestratorService. This class
 * only turns already-authoritative scheduled attempts into durable executor
 * commands and periodically asks core for due candidates.
 */
export class ExecutionContinuityScheduler {
  private readonly reconcileIntervalMs: number;
  private readonly commandDeadlineMs: number;
  private timer: NodeJS.Timeout | null = null;
  private reconcilePromise: Promise<void> | null = null;
  private readonly dispatching = new Set<string>();

  constructor(
    private readonly service: OrchestratorService,
    private readonly bridge?: ExecutorBridge,
    options: ExecutionContinuitySchedulerOptions = {},
  ) {
    this.reconcileIntervalMs = Math.max(100, options.reconcileIntervalMs ?? 500);
    this.commandDeadlineMs = Math.max(1_000, options.commandDeadlineMs ?? 10 * 60_000);
  }

  start(): void {
    if (this.timer) return;
    void this.reconcile();
    this.timer = setInterval(() => { void this.reconcile(); }, this.reconcileIntervalMs);
    this.timer.unref();
  }

  stop(): void {
    if (this.timer) clearInterval(this.timer);
    this.timer = null;
  }

  async dispatchAttemptForReason(workerId: WorkerId, reason: 'initial' | 'followup'): Promise<ExecutionAttempt> {
    const attempts = await this.service.listExecutionAttempts(workerId);
    const attempt = attempts.filter((candidate) => candidate.reason === reason).at(-1);
    if (!attempt) throw new Error(`Worker ${workerId} has no ${reason} execution attempt to dispatch.`);
    if (attempt.state === 'scheduled') await this.dispatchAttempt(attempt);
    return attempt;
  }

  async dispatchAttempt(attempt: ExecutionAttempt): Promise<void> {
    if (this.dispatching.has(attempt.executionId)) return;
    this.dispatching.add(attempt.executionId);
    try {
      // Re-read before dispatch: completion/cancel/pause may have invalidated a
      // scheduled resume after evaluation but before command creation.
      const current = (await this.service.listExecutionAttempts(attempt.workerId))
        .find((candidate) => candidate.executionId === attempt.executionId);
      if (!current || current.state !== 'scheduled') return;

      const context = await this.service.getWorkerAssignmentContext(current.workerId);
      const binding = await this.service.getConversationBinding(current.workerId);
      const command = commandForExecutionAttempt(current, this.commandDeadlineMs, {
        bootstrap: workerBootstrap(context),
        followupPrompt: workerFollowupPrompt(context),
        followupRequiresCreate: current.reason === 'followup' && (!binding || binding.state === 'closed'),
      });
      await this.service.enqueueExecutorCommand(command);
      await this.bridge?.dispatchNow();
    } finally {
      this.dispatching.delete(attempt.executionId);
    }
  }

  async dispatchMessage(message: Message): Promise<Message> {
    const key = `message:${message.messageId}`;
    if (this.dispatching.has(key)) return message;
    this.dispatching.add(key);
    try {
      const context = await this.service.getMessageDeliveryContext(message.messageId);
      const current = context.message;
      if (current.delivery === 'record_only' || ['recorded', 'delivered', 'ambiguous', 'dispatching'].includes(current.deliveryState)) return current;
      if (current.notBefore && Date.parse(current.notBefore) > Date.now()) return current;

      const binding = context.binding;
      if (!binding || ['loading', 'error', 'closed'].includes(binding.state)) {
        return this.service.deferMessageDelivery(current.messageId, new Date(Date.now() + 5_000).toISOString(), `conversation_${binding?.state ?? 'unavailable'}`);
      }

      let command: ExecutorCommand;
      if (current.delivery === 'next_turn') {
        if (!['idle', 'ready'].includes(binding.state)) {
          return this.service.deferMessageDelivery(current.messageId, new Date(Date.now() + 5_000).toISOString(), 'waiting_for_idle');
        }
        command = ordinaryMessageCommand(current, this.commandDeadlineMs);
      } else if (binding.state === 'generating') {
        if (current.priority === 'normal' && context.continuity.continuity.lastRoutineSteerAt) {
          const floor = Date.parse(context.continuity.continuity.lastRoutineSteerAt) + context.continuity.effectivePolicy.minContinuationSpacingMs;
          if (floor > Date.now()) {
            return this.service.deferMessageDelivery(current.messageId, new Date(floor).toISOString(), 'routine_steer_spacing');
          }
        }
        const idempotencyKey = messageDeliveryKey(current);
        command = createSteeringCommand({
          workerId: current.toWorkerId,
          prompt: current.body,
          reason: current.priority === 'corrective' ? 'corrective' : 'checkpoint',
          idempotencyKey,
          deadline: deadlineFrom(new Date().toISOString(), this.commandDeadlineMs),
        });
      } else if (['idle', 'ready'].includes(binding.state)) {
        // Idle delivery intentionally bypasses the non-idle steering floor and
        // uses the ordinary send transport rather than mid-turn steering.
        command = ordinaryMessageCommand(current, this.commandDeadlineMs);
      } else {
        return this.service.deferMessageDelivery(current.messageId, new Date(Date.now() + 5_000).toISOString(), `conversation_${binding.state}`);
      }

      await this.service.enqueueExecutorCommand(command);
      const linked = await this.service.markMessageDeliveryDispatched(current.messageId, command.commandId);
      await this.bridge?.dispatchNow();
      return linked;
    } finally {
      this.dispatching.delete(key);
    }
  }

  async sendMessage(input: Parameters<OrchestratorService['sendMessage']>[0]): Promise<Message> {
    const message = await this.service.sendMessage(input);
    if (message.delivery !== 'record_only') await this.dispatchMessage(message);
    return (await this.service.getMessageDeliveryContext(message.messageId)).message;
  }

  async continueNow(workerId: WorkerId, options: { overrideBlock?: boolean; idempotencyKey?: string } = {}): Promise<Awaited<ReturnType<OrchestratorService['continueNow']>>> {
    const result = await this.service.continueNow(workerId, options);
    if (result.attempt) await this.dispatchAttempt(result.attempt);
    return result;
  }

  async recoverDeliveryTimeout(workerId: WorkerId): Promise<ExecutionAttempt | null> {
    const attempt = await this.service.recordDeliveryTimeout(workerId);
    if (attempt) await this.dispatchAttempt(attempt);
    return attempt;
  }

  async reconcile(): Promise<void> {
    if (this.reconcilePromise) return this.reconcilePromise;
    this.reconcilePromise = this.reconcileOnce().finally(() => { this.reconcilePromise = null; });
    return this.reconcilePromise;
  }

  private async reconcileOnce(): Promise<void> {
    // Expired pre-submission commands are terminalized before eligibility is
    // evaluated, so they cannot leave a phantom active execution behind.
    await this.service.expirePendingExecutorCommands();

    // Materialize backend-owned keepalive deadlines into durable wake intents
    // before evaluating dispatch candidates. This survives process restart.
    await this.service.reconcileWakeIntents();

    // Durable inter-worker delivery is independent of execution-attempt
    // continuation. This includes checkpoint steering while a lane is still
    // generating and retry of previously unavailable executor/UI delivery.
    const pendingMessages = await this.service.listPendingMessageDeliveries();
    await Promise.all(pendingMessages.map((message) => this.dispatchMessage(message)));

    // The authoritative core exposes due candidates after durable policy,
    // wake-not-before, idle-grace, active-attempt, and stall checks.
    const candidates = await this.service.listContinuationCandidates();
    await Promise.all(candidates.map(async (workerId) => {
      const evaluation = await this.service.evaluateContinuation(workerId);
      if (evaluation.action === 'schedule' && evaluation.attempt) {
        await this.dispatchAttempt(evaluation.attempt);
      }
    }));

    // Recover any durable scheduled attempt whose application process died
    // before creating or dispatching its executor command. Core remains the
    // source of truth for which attempts are still runnable.
    const pending = await this.service.listPendingExecutionAttempts();
    await Promise.all(pending.map((attempt) => this.dispatchAttempt(attempt)));

    await this.bridge?.dispatchNow();
  }

}
