import { getPairedTurnOutputs } from './db.js';
import { logger } from './logger.js';
import {
  buildArbiterPromptForTask,
  buildPairedTurnPrompt,
} from './message-runtime-prompts.js';
import { resolveOwnerTaskForHumanMessage } from './paired-execution-context.js';
import {
  buildPendingPairedTurn,
  executePendingPairedTurn,
  isBotOnlyPairedRoomTurn,
} from './message-runtime-flow.js';
import { buildPairedTurnIdentity } from './paired-turn-identity.js';
import { resolveStoredVisibleVerdict } from './paired-verdict.js';
import {
  advanceLastAgentCursor,
  finalizeQueuedRunCursor,
  isExternalHumanMessage,
  resolveActiveRole,
  resolveCursorKeyForRole,
  resolveQueuedPairedTurnRole,
  resolveQueuedTurnRole,
} from './message-runtime-rules.js';
import { getTaskContextMessages } from './message-runtime-task-context.js';
import { claimPairedTurnExecution } from './paired-follow-up-scheduler.js';
import type {
  ExecuteTurnFn,
  RoleToChannelMap,
} from './message-runtime-types.js';
import type {
  Channel,
  NewMessage,
  PairedTask,
  PairedTaskStatus,
  PairedTurnReservationIntentKind,
  RegisteredGroup,
} from './types.js';

function resolveQueuedTurnReservationIntent(args: {
  task: PairedTask;
  turnRole: 'owner' | 'reviewer' | 'arbiter';
  hasHumanMessage: boolean;
}): PairedTurnReservationIntentKind {
  if (args.turnRole === 'reviewer') {
    return 'reviewer-turn';
  }
  if (args.turnRole === 'arbiter') {
    return 'arbiter-turn';
  }
  if (args.hasHumanMessage) {
    return 'owner-turn';
  }
  if (args.task.status === 'merge_ready') {
    return 'finalize-owner-turn';
  }
  return 'owner-follow-up';
}

function timestampMs(value: string | null | undefined): number | null {
  if (!value) return null;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : null;
}

function isHumanMessageFreshForTask(
  task: PairedTask | null | undefined,
  message: NewMessage,
): boolean {
  if (!isExternalHumanMessage(message)) {
    return false;
  }
  if (!task) {
    return true;
  }
  if (task.status !== 'active' || (task.owner_failure_count ?? 0) <= 0) {
    return true;
  }
  const messageTime = timestampMs(message.timestamp);
  const taskUpdatedAt = timestampMs(task.updated_at);
  if (messageTime == null || taskUpdatedAt == null) {
    return true;
  }
  return messageTime > taskUpdatedAt;
}

function buildQueuedGroupTurnPrompt(args: {
  turnRole: 'owner' | 'reviewer' | 'arbiter';
  currentTask: PairedTask | null | undefined;
  chatJid: string;
  timezone: string;
  missedMessages: NewMessage[];
  fallbackMessages: NewMessage[];
  turnOutputs: ReturnType<typeof getPairedTurnOutputs>;
  labelPairedSenders: (chatJid: string, messages: NewMessage[]) => NewMessage[];
  formatMessages: (messages: NewMessage[], timezone: string) => string;
}): string {
  if (args.turnRole === 'arbiter' && args.currentTask) {
    const recentMessages = getTaskContextMessages(
      args.chatJid,
      args.currentTask,
    );
    return buildArbiterPromptForTask({
      task: args.currentTask,
      chatJid: args.chatJid,
      timezone: args.timezone,
      turnOutputs: args.turnOutputs,
      recentMessages,
      labeledRecentMessages: args.labelPairedSenders(
        args.chatJid,
        recentMessages,
      ),
    });
  }

  if (args.currentTask) {
    return buildPairedTurnPrompt({
      taskId: args.currentTask.id,
      chatJid: args.chatJid,
      timezone: args.timezone,
      missedMessages: args.missedMessages,
      labeledFallbackMessages: args.labelPairedSenders(
        args.chatJid,
        args.fallbackMessages,
      ),
      turnOutputs: args.turnOutputs,
    });
  }

  return args.formatMessages(
    args.labelPairedSenders(args.chatJid, args.missedMessages),
    args.timezone,
  );
}

function claimQueuedPairedTurn(args: {
  chatJid: string;
  runId: string;
  task: PairedTask | null | undefined;
  taskStatus: PairedTaskStatus | null | undefined;
  intentKind: PairedTurnReservationIntentKind | null;
  turnRole: 'owner' | 'reviewer' | 'arbiter';
  log: typeof logger;
}): boolean {
  if (!args.task) {
    return true;
  }
  const claimed = claimPairedTurnExecution({
    chatJid: args.chatJid,
    runId: args.runId,
    task: args.task,
    intentKind: args.intentKind!,
  });
  if (claimed) {
    return true;
  }
  args.log.info(
    {
      taskId: args.task.id,
      taskStatus: args.taskStatus,
      taskUpdatedAt: args.task.updated_at,
      intentKind: args.intentKind,
      turnRole: args.turnRole,
    },
    'Skipped queued paired turn because the task revision was already claimed elsewhere',
  );
  return false;
}

export async function runPendingPairedTurnIfNeeded(args: {
  chatJid: string;
  group: RegisteredGroup;
  runId: string;
  log: typeof logger;
  timezone: string;
  task: PairedTask | null | undefined;
  rawMissedMessages: NewMessage[];
  saveState: () => void;
  lastAgentTimestamps: Record<string, string>;
  executeTurn: ExecuteTurnFn;
  getFixedRoleChannelName: (role: 'reviewer' | 'arbiter') => string;
  roleToChannel: RoleToChannelMap;
  labelPairedSenders: (chatJid: string, messages: NewMessage[]) => NewMessage[];
  mode: 'idle' | 'bot-only';
  missedMessages?: NewMessage[];
}): Promise<boolean | null> {
  const { chatJid, task, roleToChannel } = args;
  if (!task) {
    return null;
  }

  if (
    args.mode === 'bot-only' &&
    (!args.missedMessages ||
      !isBotOnlyPairedRoomTurn(chatJid, args.missedMessages))
  ) {
    return null;
  }

  const taskContextMessages = getTaskContextMessages(chatJid, task);
  const recentHumanMessages = taskContextMessages.filter(
    (message) => !message.is_bot_message,
  );
  const labeledRecentMessages = args.labelPairedSenders(
    chatJid,
    taskContextMessages,
  );
  const pendingTurn = buildPendingPairedTurn({
    chatJid,
    timezone: args.timezone,
    task,
    rawMissedMessages: args.rawMissedMessages,
    recentHumanMessages,
    labeledRecentMessages,
    resolveChannel: (taskStatus) =>
      roleToChannel[resolveActiveRole(taskStatus)] ?? null,
  });

  if (!pendingTurn) {
    return null;
  }

  if (pendingTurn.channel) {
    const claimed = claimPairedTurnExecution({
      chatJid,
      runId: args.runId,
      task,
      intentKind: pendingTurn.intentKind,
    });
    if (!claimed) {
      args.log.info(
        {
          chatJid,
          taskId: task.id,
          taskStatus: task.status,
          taskUpdatedAt: task.updated_at,
          intentKind: pendingTurn.intentKind,
        },
        'Skipped paired pending turn because the task revision was already claimed elsewhere',
      );
      return true;
    }
  }

  return executePendingPairedTurn({
    pendingTurn,
    chatJid,
    group: args.group,
    runId: args.runId,
    log: args.log,
    saveState: args.saveState,
    lastAgentTimestamps: args.lastAgentTimestamps,
    executeTurn: args.executeTurn,
    getFixedRoleChannelName: args.getFixedRoleChannelName,
  });
}

export async function runQueuedGroupTurn(args: {
  chatJid: string;
  group: RegisteredGroup;
  runId: string;
  log: typeof logger;
  timezone: string;
  missedMessages: NewMessage[];
  task: PairedTask | null | undefined;
  roleToChannel: RoleToChannelMap;
  ownerChannel: Channel;
  lastAgentTimestamps: Record<string, string>;
  saveState: () => void;
  executeTurn: ExecuteTurnFn;
  getFixedRoleChannelName: (role: 'reviewer' | 'arbiter') => string;
  labelPairedSenders: (chatJid: string, messages: NewMessage[]) => NewMessage[];
  formatMessages: (messages: NewMessage[], timezone: string) => string;
}): Promise<boolean> {
  const { chatJid, group, runId, log, missedMessages, task, roleToChannel } =
    args;
  let currentTask = task;
  const hasHumanMsg = task
    ? missedMessages.some((message) =>
        isHumanMessageFreshForTask(task, message),
      )
    : !isBotOnlyPairedRoomTurn(chatJid, missedMessages);
  let fallbackMessages = missedMessages;
  if (currentTask !== undefined && hasHumanMsg) {
    const resolvedTask = resolveOwnerTaskForHumanMessage({
      group,
      chatJid,
      existingTask: currentTask ?? null,
    });
    currentTask = resolvedTask.task;
    if (resolvedTask.supersededTask) {
      fallbackMessages = currentTask
        ? getTaskContextMessages(chatJid, currentTask).filter(
            (message) => !message.is_bot_message,
          )
        : missedMessages;
    }
  }
  const taskStatus = currentTask?.status;
  const turnOutputs = currentTask ? getPairedTurnOutputs(currentTask.id) : [];
  const lastTurnOutputRole = currentTask
    ? (turnOutputs.at(-1)?.role ?? null)
    : null;
  const lastTurnOutputVerdict = currentTask
    ? resolveStoredVisibleVerdict({
        verdict: turnOutputs.at(-1)?.verdict ?? null,
        outputText: turnOutputs.at(-1)?.output_text ?? null,
      })
    : null;
  const turnRole = currentTask
    ? hasHumanMsg
      ? resolveQueuedTurnRole({
          taskStatus,
          hasHumanMessage: true,
        })
      : resolveQueuedPairedTurnRole({
          taskStatus,
          hasHumanMessage: false,
          lastTurnOutputRole,
          lastTurnOutputVerdict,
          ownerFailureCount: currentTask.owner_failure_count,
        })
    : 'owner';
  if (!turnRole) {
    const endSeq = missedMessages[missedMessages.length - 1]?.seq ?? null;
    if (endSeq !== null) {
      advanceLastAgentCursor(
        args.lastAgentTimestamps,
        args.saveState,
        chatJid,
        endSeq,
        resolveCursorKeyForRole(chatJid, resolveActiveRole(taskStatus)),
      );
    }
    log.info(
      {
        chatJid,
        taskId: task?.id ?? null,
        taskStatus,
        lastTurnOutputRole,
      },
      'Skipped queued paired turn because the latest persisted turn already closed the pending handoff',
    );
    return true;
  }
  const turnChannel =
    turnRole === 'owner' ? args.ownerChannel : roleToChannel[turnRole];
  const cursorKey = resolveCursorKeyForRole(chatJid, turnRole);
  const forcedRole = currentTask ? turnRole : undefined;
  const queuedIntentKind = currentTask
    ? resolveQueuedTurnReservationIntent({
        task: currentTask,
        turnRole,
        hasHumanMessage: hasHumanMsg,
      })
    : null;
  const pairedTurnIdentity =
    currentTask && queuedIntentKind
      ? buildPairedTurnIdentity({
          taskId: currentTask.id,
          taskUpdatedAt: currentTask.updated_at,
          intentKind: queuedIntentKind,
          role: turnRole,
        })
      : undefined;

  const prompt = buildQueuedGroupTurnPrompt({
    turnRole,
    currentTask,
    chatJid,
    timezone: args.timezone,
    missedMessages,
    fallbackMessages,
    turnOutputs,
    labelPairedSenders: args.labelPairedSenders,
    formatMessages: args.formatMessages,
  });

  const startSeq = missedMessages[0].seq ?? null;
  const endSeq = missedMessages[missedMessages.length - 1].seq ?? null;
  log.info(
    {
      messageCount: missedMessages.length,
      messageSeqStart: startSeq,
      messageSeqEnd: endSeq,
    },
    'Dispatching queued messages to agent',
  );

  if (!turnChannel) {
    const missingRole = turnRole === 'arbiter' ? 'arbiter' : 'reviewer';
    log.error(
      {
        taskStatus,
        role: turnRole,
        requiredChannel: args.getFixedRoleChannelName(missingRole),
      },
      'Skipping paired-room run because the dedicated Discord role channel is not configured',
    );
    return false;
  }

  if (
    !claimQueuedPairedTurn({
      chatJid,
      runId,
      task: currentTask,
      taskStatus,
      intentKind: queuedIntentKind!,
      turnRole,
      log,
    })
  ) {
    return true;
  }

  const cursorAdvanced = endSeq !== null;

  const { outputStatus, deliverySucceeded, visiblePhase } =
    await args.executeTurn({
      group,
      prompt,
      chatJid,
      runId,
      channel: turnChannel,
      deliveryRole: currentTask ? turnRole : undefined,
      startSeq,
      endSeq,
      hasHumanMessage: hasHumanMsg,
      forcedRole,
      pairedTurnIdentity,
    });

  if (
    !finalizeQueuedRunCursor({
      outputStatus,
      visiblePhase,
      startSeq,
      endSeq,
      rollbackOnSilentError:
        hasHumanMsg &&
        turnRole === 'owner' &&
        currentTask?.owner_agent_type === 'codex',
      log,
    })
  ) {
    return false;
  }

  if (cursorAdvanced) {
    advanceLastAgentCursor(
      args.lastAgentTimestamps,
      args.saveState,
      chatJid,
      endSeq,
      cursorKey,
    );
  }

  if (!deliverySucceeded) {
    log.warn(
      {
        messageSeqStart: startSeq,
        messageSeqEnd: endSeq,
      },
      'Persisted produced output for delivery retry without rerunning agent',
    );
    return false;
  }

  log.info(
    {
      visiblePhase,
      messageSeqStart: startSeq,
      messageSeqEnd: endSeq,
    },
    'Queued run completed successfully',
  );

  return true;
}
