// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
//
// WU-2733 (INIT-060 Phase 3, ADR-013 §1 + §6):
// Real kernel-side implementations for the six remote-control
// operations. Each operation:
//   1. Loads/mutates RemoteControlSessionState via the sidecar store
//      (so state survives process restarts).
//   2. Emits the ADR-013-governed event via the supplied sink
//      (turn_aborted / autonomy_changed / tool_called).
//   3. Returns a result that adheres to RemoteControlPort.
//
// No auto-wu:recover. recovery_action is always a hint; ADR-013 §1
// safety lock is preserved.

import { randomUUID } from 'node:crypto';
import {
  buildToolCalledEvent,
  buildTurnAbortedEvent,
  buildAutonomyChangedEvent,
  emitAgentRuntimeEvent,
  type AgentRuntimeCleanupStatus,
  type AgentRuntimeEventSink,
} from '../turn-lifecycle-events.js';
import {
  AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES,
  type AbortTurnInvocationResult,
  type AgentRuntimeRemoteControlToolName,
  type RemoteControlInvocationResult,
} from './types.js';
import type { AbortTurnInvocationInput, RemoteControlInvocationInput } from './port.js';
import {
  AGENT_RUNTIME_AUTONOMY_LEVELS,
  AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES,
  type AgentRuntimeAutonomyLevel,
  type RemoteControlStateStore,
} from './state-store.js';

/**
 * Observed turn index emitted in remote-control tool_called events.
 * The real kernel runtime increments a per-turn counter; remote-control
 * invocations happen OUTSIDE an active turn (that is precisely their
 * purpose), so we emit 0 to signal "not tied to a turn index". This
 * mirrors the mock adapter's constant for wire parity.
 */
const REMOTE_CONTROL_TURN_INDEX = 0;

export interface RemoteControlOperationContext {
  store: RemoteControlStateStore;
  eventSink?: AgentRuntimeEventSink;
  now: () => string;
}

function resolveSessionId(input: string | undefined): string | null {
  if (input !== undefined && input.trim().length > 0) {
    return input;
  }
  return null;
}

function emitToolCalled(
  ctx: RemoteControlOperationContext,
  toolName: AgentRuntimeRemoteControlToolName,
  sessionId: string,
): void {
  emitAgentRuntimeEvent(
    ctx.eventSink,
    buildToolCalledEvent({
      session_id: sessionId,
      turn_index: REMOTE_CONTROL_TURN_INDEX,
      tool_name: toolName,
      tool_call_id: randomUUID(),
    }),
  );
}

function requireSessionId(
  toolName: AgentRuntimeRemoteControlToolName,
  input: string | undefined,
): string {
  const resolved = resolveSessionId(input);
  if (!resolved) {
    throw new Error(`Remote-control tool "${toolName}" requires a non-empty session_id input.`);
  }
  return resolved;
}

// ---------------------------------------------------------------------------
// pause_turn
// ---------------------------------------------------------------------------

/**
 * Soft-pause marker honoured at the next checkpoint. The running turn
 * (if any) observes the marker via a state load and suspends gracefully.
 * Re-pausing an already-paused session is idempotent — we refresh the
 * reason+timestamp without toggling.
 */
export async function pauseTurn(
  input: RemoteControlInvocationInput & { reason?: string | null },
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  const sessionId = requireSessionId(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN,
    input.session_id,
  );
  const now = ctx.now();
  const state = await ctx.store.loadOrDefault(sessionId, now);
  const reason = typeof input.reason === 'string' ? input.reason : null;
  await ctx.store.save({
    ...state,
    pause: {
      paused: true,
      paused_at: now,
      resumed_at: null,
      reason,
    },
    updated_at: now,
  });
  emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN, sessionId);
  return {
    status: 'ok',
    tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.PAUSE_TURN,
    session_id: sessionId,
  };
}

// ---------------------------------------------------------------------------
// resume_workflow
// ---------------------------------------------------------------------------

/**
 * Clears the pause marker so the next checkpoint observer advances the
 * workflow. Does NOT re-enter the agent loop — the pauser/scheduler
 * resumes the session on its next poll. Emits tool_called for audit.
 */
export async function resumeWorkflow(
  input: RemoteControlInvocationInput,
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  const sessionId = requireSessionId(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW,
    input.session_id,
  );
  const now = ctx.now();
  const state = await ctx.store.loadOrDefault(sessionId, now);
  await ctx.store.save({
    ...state,
    pause: {
      paused: false,
      paused_at: state.pause.paused_at,
      resumed_at: now,
      reason: state.pause.reason,
    },
    updated_at: now,
  });
  emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW, sessionId);
  return {
    status: 'ok',
    tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.RESUME_WORKFLOW,
    session_id: sessionId,
  };
}

// ---------------------------------------------------------------------------
// abort_turn (ADR-013 §1)
// ---------------------------------------------------------------------------

/**
 * Hard-aborts the current turn and records the cleanup_status +
 * recovery_action per ADR-013 §1:
 *   - 'clean'           — no prior mutations; recovery_action = null
 *   - 'partial'         — inflight approvals existed; recovery_action
 *                         is a prose hint
 *   - 'needs_recovery'  — abort interrupted a mid-flight approval with
 *                         the abort ordered while paused; operator must
 *                         run `wu:recover`
 *
 * NEVER auto-triggers wu:recover. The returned recovery_action is a
 * hint cloud renders in the conductor UI (§1 safety lock).
 */
export async function abortTurn(
  input: AbortTurnInvocationInput,
  ctx: RemoteControlOperationContext,
): Promise<AbortTurnInvocationResult> {
  const sessionId = requireSessionId(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN,
    input.session_id,
  );
  const now = ctx.now();
  const state = await ctx.store.loadOrDefault(sessionId, now);
  const { cleanup_status, recovery_action } = classifyAbort(state);
  const reason =
    typeof input.reason === 'string' && input.reason.trim().length > 0
      ? input.reason
      : 'operator-requested';
  await ctx.store.save({
    ...state,
    abort: {
      aborted: true,
      aborted_at: now,
      cleanup_status,
      recovery_action,
      reason,
    },
    pause: state.pause,
    updated_at: now,
  });

  // ADR-013 §1: emit turn_aborted carrying the cleanup contract.
  emitAgentRuntimeEvent(
    ctx.eventSink,
    buildTurnAbortedEvent({
      session_id: sessionId,
      turn_index: REMOTE_CONTROL_TURN_INDEX,
      cleanup_status,
      recovery_action,
      reason,
    }),
  );
  // ADR-013 §6: emit tool_called for audit of the governance surface.
  emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN, sessionId);

  return {
    status: 'ok',
    tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ABORT_TURN,
    session_id: sessionId,
    cleanup_status,
    recovery_action,
  };
}

interface AbortClassification {
  cleanup_status: AgentRuntimeCleanupStatus;
  recovery_action: string | null;
}

/**
 * Decide cleanup_status per ADR-013 §1 abort table.
 *
 * - Fresh/unused session state → 'clean'; no mutations occurred.
 * - Paused session with ≥1 inflight approval → 'needs_recovery'; state
 *   may be inconsistent because an approval was mid-flight when pause
 *   landed and the operator ordered an abort on top.
 * - Either "paused" OR "inflight approvals" alone → 'partial'; the
 *   session is self-consistent but has uncommitted work.
 */
function classifyAbort(state: {
  pause: { paused: boolean };
  inflight_approvals: readonly unknown[];
}): AbortClassification {
  const hasInflight = state.inflight_approvals.length > 0;
  const isPaused = state.pause.paused;

  if (!hasInflight && !isPaused) {
    return { cleanup_status: 'clean', recovery_action: null };
  }
  if (hasInflight && isPaused) {
    return {
      cleanup_status: 'needs_recovery',
      recovery_action:
        'Run `pnpm wu:recover --session ${session}` to reconcile the paused session with pending approval state.',
    };
  }
  if (hasInflight) {
    return {
      cleanup_status: 'partial',
      recovery_action:
        'Review inflight approvals in the conductor UI; re-run `wu:prep` after the approval is cleared.',
    };
  }
  return {
    cleanup_status: 'partial',
    recovery_action:
      'Session was paused when the abort landed; review the paused continuation before resuming.',
  };
}

// ---------------------------------------------------------------------------
// elevate_autonomy / lower_autonomy
// ---------------------------------------------------------------------------

function autonomyIndex(level: AgentRuntimeAutonomyLevel): number {
  return AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES.indexOf(level);
}

function nextAutonomyLevel(
  current: AgentRuntimeAutonomyLevel,
  direction: 'elevate' | 'lower',
): AgentRuntimeAutonomyLevel {
  const index = autonomyIndex(current);
  const delta = direction === 'elevate' ? 1 : -1;
  const candidate = index + delta;
  if (candidate < 0 || candidate >= AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES.length) {
    return current;
  }
  return AGENT_RUNTIME_AUTONOMY_LEVEL_VALUES[candidate] ?? current;
}

async function transitionAutonomy(
  toolName: AgentRuntimeRemoteControlToolName,
  direction: 'elevate' | 'lower',
  input: RemoteControlInvocationInput & { reason?: string | null },
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  const sessionId = requireSessionId(toolName, input.session_id);
  const now = ctx.now();
  const state = await ctx.store.loadOrDefault(sessionId, now);
  const previousLevel = state.autonomy_level;
  const newLevel = nextAutonomyLevel(previousLevel, direction);
  await ctx.store.save({
    ...state,
    autonomy_level: newLevel,
    updated_at: now,
  });

  // §6 governance — record BOTH autonomy_changed AND tool_called.
  emitAgentRuntimeEvent(
    ctx.eventSink,
    buildAutonomyChangedEvent({
      session_id: sessionId,
      previous_level: previousLevel,
      new_level: newLevel,
      direction,
      reason: typeof input.reason === 'string' ? input.reason : null,
    }),
  );
  emitToolCalled(ctx, toolName, sessionId);

  return {
    status: 'ok',
    tool_name: toolName,
    session_id: sessionId,
  };
}

export async function elevateAutonomy(
  input: RemoteControlInvocationInput & { reason?: string | null },
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  return transitionAutonomy(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.ELEVATE_AUTONOMY,
    'elevate',
    input,
    ctx,
  );
}

export async function lowerAutonomy(
  input: RemoteControlInvocationInput & { reason?: string | null },
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  return transitionAutonomy(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.LOWER_AUTONOMY,
    'lower',
    input,
    ctx,
  );
}

// ---------------------------------------------------------------------------
// approve_inflight
// ---------------------------------------------------------------------------

/**
 * Resolves the oldest pending in-flight approval for a session. The
 * approval queue lives inside RemoteControlSessionState; workflow-level
 * approval gating remains the orchestration module's responsibility —
 * this tool only clears the queue entry so the workflow observer sees
 * the approval as satisfied.
 *
 * If there are no pending approvals, we still emit tool_called and
 * return `status: 'ok'` — the operator's click is auditable even when
 * it was a no-op race against an auto-resolved approval.
 */
export async function approveInflight(
  input: RemoteControlInvocationInput,
  ctx: RemoteControlOperationContext,
): Promise<RemoteControlInvocationResult> {
  const sessionId = requireSessionId(
    AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT,
    input.session_id,
  );
  const now = ctx.now();
  const state = await ctx.store.loadOrDefault(sessionId, now);
  const [, ...remaining] = state.inflight_approvals;
  await ctx.store.save({
    ...state,
    inflight_approvals: remaining,
    updated_at: now,
  });
  emitToolCalled(ctx, AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT, sessionId);
  return {
    status: 'ok',
    tool_name: AGENT_RUNTIME_REMOTE_CONTROL_TOOL_NAMES.APPROVE_INFLIGHT,
    session_id: sessionId,
  };
}

export { AGENT_RUNTIME_AUTONOMY_LEVELS };
