/**
 * Session Channel Actions — Mutations of an `ahp-session:` channel's state.
 *
 * @module channels-session/actions
 */

import { ActionType } from '../common/actions.js';
import type { StringOrMarkdown, ErrorInfo, FileEdit, UsageInfo } from '../common/state.js';
import type {
  Message,
  ResponsePart,
  ToolCallResult,
  ToolResultContent,
  ToolDefinition,
  SessionActiveClient,
  Customization,
  McpServerState,
  SessionInputAnswer,
  SessionInputRequest,
  SessionInputResponseKind,
  ConfirmationOption,
  AgentSelection,
  ToolCallContributor,
} from './state.js';
import type { ModelSelection } from '../channels-root/state.js';
import type { URI } from '../common/state.js';
import {
  ToolCallConfirmationReason,
  ToolCallCancellationReason,
  PendingMessageKind,
} from './state.js';
import type { Changeset } from '../channels-changeset/state.js';

// ─── Tool Call Action Base ───────────────────────────────────────────────────

/**
 * Base interface for all tool-call-scoped actions, carrying the common turn
 * and tool call identifiers. The owning session URI is identified by the
 * enclosing {@link ActionEnvelope}'s `channel` field.
 *
 * @category Session Actions
 */
interface ToolCallActionBase {
  /** Turn identifier */
  turnId: string;
  /** Tool call identifier */
  toolCallId: string;
  /**
   * Additional provider-specific metadata for this tool call.
   *
   * Clients MAY look for well-known keys here to provide enhanced UI.
   * For example, a `ptyTerminal` key with `{ input: string; output: string }`
   * indicates the tool operated on a terminal (both `input` and `output` may
   * contain escape sequences).
   */
  _meta?: Record<string, unknown>;
}

// ─── Session Actions ─────────────────────────────────────────────────────────

/**
 * Session backend initialized successfully.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionReadyAction {
  type: ActionType.SessionReady;
}

/**
 * Session backend failed to initialize.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionCreationFailedAction {
  type: ActionType.SessionCreationFailed;
  /** Error details */
  error: ErrorInfo;
}

/**
 * A new message has been sent to the agent, and a new turn starts.
 *
 * A client is only allowed to send {@link MessageKind.User} messages.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionTurnStartedAction {
  type: ActionType.SessionTurnStarted;
  /** Turn identifier */
  turnId: string;
  /** The new message */
  message: Message;
  /** If this turn was auto-started from a queued message, the ID of that message */
  queuedMessageId?: string;
}

/**
 * Streaming text chunk from the assistant, appended to a specific response part.
 *
 * The server MUST first emit a `session/responsePart` to create the target
 * part (markdown or reasoning), then use this action to append text to it.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionDeltaAction {
  type: ActionType.SessionDelta;
  /** Turn identifier */
  turnId: string;
  /** Identifier of the response part to append to */
  partId: string;
  /** Text chunk */
  content: string;
}

/**
 * Structured content appended to the response.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionResponsePartAction {
  type: ActionType.SessionResponsePart;
  /** Turn identifier */
  turnId: string;
  /** Response part (markdown or content ref) */
  part: ResponsePart;
}

/**
 * A tool call begins — parameters are streaming from the LM.
 *
 * The server sets {@link ToolCallContributor | `contributor`} to identify
 * the origin of the tool. For client-provided tools, the named client is
 * responsible for executing the tool once it reaches the `running` state
 * and dispatching `session/toolCallComplete`. For MCP-served tools, the
 * server executes the call against the named `McpServerCustomization`.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionToolCallStartAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallStart;
  /** Internal tool name (for debugging/logging) */
  toolName: string;
  /** Human-readable tool name */
  displayName: string;
  /**
   * Reference to the contributor of the tool being called. Absent for
   * server-side tools that are not contributed by a client or MCP server.
   */
  contributor?: ToolCallContributor;
}

/**
 * Streaming partial parameters for a tool call.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionToolCallDeltaAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallDelta;
  /** Partial parameter content to append */
  content: string;
  /** Updated progress message */
  invocationMessage?: StringOrMarkdown;
}

/**
 * Tool call parameters are complete, or a running tool requires re-confirmation.
 *
 * When dispatched for a `streaming` tool call, transitions to `pending-confirmation`
 * or directly to `running` if `confirmed` is set.
 *
 * When dispatched for a `running` tool call (e.g. mid-execution permission needed),
 * transitions back to `pending-confirmation`. The `invocationMessage` and `_meta`
 * SHOULD be updated to describe the specific confirmation needed. Clients use the
 * standard `session/toolCallConfirmed` flow to approve or deny.
 *
 * For client-provided tools, the server typically sets `confirmed` to
 * `'not-needed'` so the tool transitions directly to `running`, where the
 * owning client can begin execution immediately.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionToolCallReadyAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallReady;
  /** Message describing what the tool will do or what confirmation is needed */
  invocationMessage: StringOrMarkdown;
  /** Raw tool input */
  toolInput?: string;
  /** Short title for the confirmation prompt (e.g. `"Run in terminal"`, `"Write file"`) */
  confirmationTitle?: StringOrMarkdown;
  /** File edits that this tool call will perform, for preview before confirmation */
  edits?: { items: FileEdit[] };
  /** Whether the agent host allows the client to edit the tool's input parameters before confirming */
  editable?: boolean;
  /** If set, the tool was auto-confirmed and transitions directly to `running` */
  confirmed?: ToolCallConfirmationReason;
  /**
   * Options the server offers for this confirmation. When present, the client
   * SHOULD render these instead of a plain approve/deny UI. Each option
   * belongs to a {@link ConfirmationOptionGroup} so the client can still
   * categorise the choices.
   */
  options?: ConfirmationOption[];
}

/**
 * Client approves a pending tool call. The tool transitions to `running`.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionToolCallApprovedAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallConfirmed;
  /** The tool call was approved */
  approved: true;
  /** How the tool was confirmed */
  confirmed: ToolCallConfirmationReason;
  /** Edited tool input parameters, if the client modified them before confirming */
  editedToolInput?: string;
  /** ID of the selected confirmation option, if the server provided options */
  selectedOptionId?: string;
}

/**
 * Client denies a pending tool call. The tool transitions to `cancelled`.
 *
 * For client-provided tools, the owning client MUST dispatch this if it does
 * not recognize the tool or cannot execute it.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionToolCallDeniedAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallConfirmed;
  /** The tool call was denied */
  approved: false;
  /** Why the tool was cancelled */
  reason: ToolCallCancellationReason.Denied | ToolCallCancellationReason.Skipped;
  /** What the user suggested doing instead */
  userSuggestion?: Message;
  /** Optional explanation for the denial */
  reasonMessage?: StringOrMarkdown;
  /** ID of the selected confirmation option, if the server provided options */
  selectedOptionId?: string;
}

/**
 * Client confirms or denies a pending tool call.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export type SessionToolCallConfirmedAction =
  | SessionToolCallApprovedAction
  | SessionToolCallDeniedAction;

/**
 * Tool execution finished. Transitions to `completed` or `pending-result-confirmation`
 * if `requiresResultConfirmation` is `true`.
 *
 * For client-provided tools (where `toolClientId` is set on the tool call state),
 * the owning client dispatches this action with the execution result. The server
 * SHOULD reject this action if the dispatching client does not match `toolClientId`.
 *
 * Servers waiting on a client tool call MAY time out after a reasonable duration
 * if the implementing client disconnects or becomes unresponsive, and dispatch
 * this action with `result.success = false` and an appropriate error.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionToolCallCompleteAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallComplete;
  /** Execution result */
  result: ToolCallResult;
  /** If true, the result requires client approval before finalizing */
  requiresResultConfirmation?: boolean;
}

/**
 * Client approves or denies a tool's result.
 *
 * If `approved` is `false`, the tool transitions to `cancelled` with reason `result-denied`.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionToolCallResultConfirmedAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallResultConfirmed;
  /** Whether the result was approved */
  approved: boolean;
}

/**
 * Partial content produced while a tool is still executing.
 *
 * Replaces the `content` array on the running tool call state. Clients can
 * use this to display live feedback (e.g. a terminal reference) before the
 * tool completes.
 *
 * For client-provided tools (where `toolClientId` is set on the tool call state),
 * the owning client dispatches this action to stream intermediate content while
 * executing. The server SHOULD reject this action if the dispatching client does
 * not match `toolClientId`.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionToolCallContentChangedAction extends ToolCallActionBase {
  type: ActionType.SessionToolCallContentChanged;
  /** The current partial content for the running tool call */
  content: ToolResultContent[];
}

/**
 * Turn finished — the assistant is idle.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionTurnCompleteAction {
  type: ActionType.SessionTurnComplete;
  /** Turn identifier */
  turnId: string;
}

/**
 * Turn was aborted; server stops processing.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionTurnCancelledAction {
  type: ActionType.SessionTurnCancelled;
  /** Turn identifier */
  turnId: string;
}

/**
 * Error during turn processing.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionErrorAction {
  type: ActionType.SessionError;
  /** Turn identifier */
  turnId: string;
  /** Error details */
  error: ErrorInfo;
}

/**
 * Session title updated. Fired by the server when the title is auto-generated
 * from conversation, or dispatched by a client to rename a session.
 *
 * @category Session Actions
 * @clientDispatchable
 * @version 1
 */
export interface SessionTitleChangedAction {
  type: ActionType.SessionTitleChanged;
  /** New title */
  title: string;
}

/**
 * Token usage report for a turn.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionUsageAction {
  type: ActionType.SessionUsage;
  /** Turn identifier */
  turnId: string;
  /** Token usage data */
  usage: UsageInfo;
}

/**
 * Reasoning/thinking text from the model, appended to a specific reasoning response part.
 *
 * The server MUST first emit a `session/responsePart` to create the target
 * reasoning part, then use this action to append text to it.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionReasoningAction {
  type: ActionType.SessionReasoning;
  /** Turn identifier */
  turnId: string;
  /** Identifier of the reasoning response part to append to */
  partId: string;
  /** Reasoning text chunk */
  content: string;
}

/**
 * Model changed for this session.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionModelChangedAction {
  type: ActionType.SessionModelChanged;
  /** New model selection */
  model: ModelSelection;
}

/**
 * Custom agent selection changed for this session.
 *
 * Omitting `agent` (or setting it to `undefined`) clears the selection and
 * resets the session to no selected custom agent (provider default behavior).
 *
 * When a turn is currently active, the server MUST defer the change until
 * the active turn completes, then apply it for the next turn (same rule as
 * {@link SessionModelChangedAction | `session/modelChanged`}).
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionAgentChangedAction {
  type: ActionType.SessionAgentChanged;
  /**
   * New agent selection, or `undefined` to clear the selection and reset the
   * session to no selected custom agent.
   */
  agent?: AgentSelection;
}

/**
 * The read state of the session changed.
 *
 * Dispatched by a client to mark a session as read (e.g. after viewing it)
 * or unread (e.g. after new activity since the client last looked at it).
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionIsReadChangedAction {
  type: ActionType.SessionIsReadChanged;
  /** Whether the session has been read */
  isRead: boolean;
}

/**
 * The archived state of the session changed.
 *
 * Dispatched by a client to archive a session (e.g. the task is
 * complete) or to unarchive it.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionIsArchivedChangedAction {
  type: ActionType.SessionIsArchivedChanged;
  /** Whether the session is archived */
  isArchived: boolean;
}

/**
 * The activity description of the session changed.
 *
 * Dispatched by the server to indicate what the session is currently doing
 * (e.g. running a tool, thinking). Clear activity by setting it to `undefined`.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionActivityChangedAction {
  type: ActionType.SessionActivityChanged;
  /** Human-readable description of current activity, or `undefined` to clear */
  activity: string | undefined;
}

/**
 * The {@link Changeset | catalogue of changesets} the agent host
 * advertises for this session changed. Replaces
 * {@link SessionState.changesets | `state.changesets`} entirely
 * (full-replacement semantics) — set to `undefined` to clear the
 * catalogue.
 *
 * Producers dispatch this whenever entries are added or removed. The
 * fan-out happens through this action so observers see catalogue
 * mutations in the same {@link ChangesetAction | per-changeset} action
 * stream they already follow for file-level updates.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionChangesetsChangedAction {
  type: ActionType.SessionChangesetsChanged;
  /** New catalogue, or `undefined` to clear it */
  changesets: Changeset[] | undefined;
}

/**
 * Server tools for this session have changed.
 *
 * Full-replacement semantics: the `tools` array replaces the previous `serverTools` entirely.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionServerToolsChangedAction {
  type: ActionType.SessionServerToolsChanged;
  /** Updated server tools list (full replacement) */
  tools: ToolDefinition[];
}

/**
 * The active client for this session has changed.
 *
 * A client dispatches this action with its own `SessionActiveClient` to claim
 * the active role, or with `null` to release it. The server SHOULD reject if
 * another client is already active. The server SHOULD automatically dispatch
 * this action with `activeClient: null` when the active client disconnects.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionActiveClientChangedAction {
  type: ActionType.SessionActiveClientChanged;
  /** The new active client, or `null` to unset */
  activeClient: SessionActiveClient | null;
}

/**
 * The active client's tool list has changed.
 *
 * Full-replacement semantics: the `tools` array replaces the active client's
 * previous tools entirely. The server SHOULD reject if the dispatching client
 * is not the current active client.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionActiveClientToolsChangedAction {
  type: ActionType.SessionActiveClientToolsChanged;
  /** Updated client tools list (full replacement) */
  tools: ToolDefinition[];
}

// ─── Customization Actions ───────────────────────────────────────────────────

/**
 * The session's customizations have changed.
 *
 * Full-replacement semantics: the `customizations` array replaces the
 * previous `customizations` entirely.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionCustomizationsChangedAction {
  type: ActionType.SessionCustomizationsChanged;
  /** Updated customization list (full replacement). */
  customizations: Customization[];
}

/**
 * A client toggled a container customization on or off.
 *
 * Targets a top-level container (plugin or directory) by `id`. Only
 * containers have an `enabled` flag; children are always active when
 * their container is enabled. Is a no-op when no matching container is
 * found.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionCustomizationToggledAction {
  type: ActionType.SessionCustomizationToggled;
  /** The id of the container to toggle. */
  id: string;
  /** Whether to enable or disable the container. */
  enabled: boolean;
}

/**
 * Upserts a top-level customization (plugin or directory).
 *
 * The reducer locates the existing entry by `customization.id`:
 *
 * - If found, the entry is replaced entirely with `customization`,
 *   including its `children` array. To preserve existing children, the
 *   host must include them on the payload.
 * - If not found, the entry is appended.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionCustomizationUpdatedAction {
  type: ActionType.SessionCustomizationUpdated;
  /** The customization to upsert (matched by `customization.id`). */
  customization: Customization;
}

/**
 * Removes a customization by id.
 *
 * Searches every container and its children for the entry. If the entry
 * is a container, its children are removed with it. Is a no-op when no
 * matching id is found.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionCustomizationRemovedAction {
  type: ActionType.SessionCustomizationRemoved;
  /** The id of the customization to remove. */
  id: string;
}

/**
 * Updates the runtime fields of an existing
 * {@link McpServerCustomization} — narrow alternative to
 * {@link SessionCustomizationUpdatedAction} for the high-frequency
 * `starting` ↔ `ready` ↔ `authRequired` transitions.
 *
 * Locates the target entry by `id`, searching both the top-level
 * customization list and the `children` array of every container.
 * Replaces the entry's {@link McpServerCustomization.state | `state`}
 * and {@link McpServerCustomization.channel | `channel`}
 * (full-replacement semantics: omit `channel` to clear an existing
 * channel URI). Other fields of the customization are preserved.
 *
 * Is a no-op when no matching `McpServerCustomization` is found. To
 * update any other field (name, icons, `mcpApp` capabilities, etc.) use
 * {@link SessionCustomizationUpdatedAction} instead.
 *
 * When the transition is to {@link McpServerStatus.AuthRequired}
 * because of a request issued mid-turn, the host SHOULD also raise
 * {@link SessionStatus.InputNeeded} on the session — see
 * {@link McpServerAuthRequiredState} for the rationale.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionMcpServerStateChangedAction {
  type: ActionType.SessionMcpServerStateChanged;
  /** The id of the {@link McpServerCustomization} to update. */
  id: string;
  /** The new lifecycle state. */
  state: McpServerState;
  /**
   * Updated `mcp://` side-channel URI. Full-replacement: omit to clear
   * an existing channel (typical when leaving
   * {@link McpServerStatus.Ready | `Ready`}).
   */
  channel?: URI;
}

// ─── Config Actions ──────────────────────────────────────────────────────────

/**
 * Client changed a mutable config value mid-session.
 *
 * Only properties with `sessionMutable: true` in the config schema may be
 * changed. The server validates and broadcasts the action; the reducer merges
 * the new values into `state.config.values`.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionConfigChangedAction {
  type: ActionType.SessionConfigChanged;
  /** Updated config values */
  config: Record<string, unknown>;
  /** When `true`, replaces all config values instead of merging */
  replace?: boolean;
}

/**
 * The session's `_meta` side-channel changed. Replaces `state._meta`
 * entirely (full-replacement semantics). Producers SHOULD merge any
 * keys they wish to preserve into the new value before dispatching.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionMetaChangedAction {
  type: ActionType.SessionMetaChanged;
  /** New `_meta` payload, or `undefined` to clear it */
  _meta: Record<string, unknown> | undefined;
}

// ─── Truncation ──────────────────────────────────────────────────────────────

/**
 * Truncates a session's history. If `turnId` is provided, all turns after that
 * turn are removed and the specified turn is kept. If `turnId` is omitted, all
 * turns are removed.
 *
 * If there is an active turn it is silently dropped and the session status
 * returns to `idle`.
 *
 * Common use-case: truncate old data then dispatch a new
 * `session/turnStarted` with an edited message.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionTruncatedAction {
  type: ActionType.SessionTruncated;
  /** Keep turns up to and including this turn. Omit to clear all turns. */
  turnId?: string;
}

// ─── Pending Message Actions ─────────────────────────────────────────────────

/**
 * A pending message was set (upsert semantics: creates or replaces).
 *
 * For steering messages, this always replaces the single steering message.
 * For queued messages, if a message with the given `id` already exists it is
 * updated in place; otherwise it is appended to the queue. If the session is
 * idle when a queued message is set, the server SHOULD immediately consume it
 * and start a new turn.
 *
 * A client is only allowed to send {@link MessageKind.User} messages.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionPendingMessageSetAction {
  type: ActionType.SessionPendingMessageSet;
  /** Whether this is a steering or queued message */
  kind: PendingMessageKind;
  /** Unique identifier for this pending message */
  id: string;
  /** The message content */
  message: Message;
}

/**
 * A pending message was removed (steering or queued).
 *
 * Dispatched by clients to cancel a pending message, or by the server when
 * it consumes a message (e.g. starting a turn from a queued message or
 * injecting a steering message into the current turn).
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionPendingMessageRemovedAction {
  type: ActionType.SessionPendingMessageRemoved;
  /** Whether this is a steering or queued message */
  kind: PendingMessageKind;
  /** Identifier of the pending message to remove */
  id: string;
}

/**
 * Reorder the queued messages.
 *
 * The `order` array contains the IDs of queued messages in their new
 * desired order. IDs not present in the current queue are ignored.
 * Queued messages whose IDs are absent from `order` are appended at
 * the end in their original relative order (so a client with a stale
 * view of the queue never silently drops messages).
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionQueuedMessagesReorderedAction {
  type: ActionType.SessionQueuedMessagesReordered;
  /** Queued message IDs in the desired order */
  order: string[];
}

// ─── Session Input Actions ──────────────────────────────────────────────────

/**
 * A session requested input from the user.
 *
 * Full-request upsert semantics: the `request` replaces any existing request
 * with the same `id`, or is appended if it is new. Answer drafts are preserved
 * unless `request.answers` is provided.
 *
 * @category Session Actions
 * @version 1
 */
export interface SessionInputRequestedAction {
  type: ActionType.SessionInputRequested;
  /** Input request to create or replace */
  request: SessionInputRequest;
}

/**
 * A client updated, submitted, skipped, or removed a single in-progress answer.
 *
 * Dispatching with `answer: undefined` removes that question's answer draft.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionInputAnswerChangedAction {
  type: ActionType.SessionInputAnswerChanged;
  /** Input request identifier */
  requestId: string;
  /** Question identifier within the input request */
  questionId: string;
  /** Updated answer, or `undefined` to clear an answer draft */
  answer?: SessionInputAnswer;
}

/**
 * A client accepted, declined, or cancelled a session input request.
 *
 * If accepted, the server uses `answers` (when provided) plus the request's
 * synced answer state to resume the blocked operation.
 *
 * @category Session Actions
 * @version 1
 * @clientDispatchable
 */
export interface SessionInputCompletedAction {
  type: ActionType.SessionInputCompleted;
  /** Input request identifier */
  requestId: string;
  /** Completion outcome */
  response: SessionInputResponseKind;
  /** Optional final answer replacement, keyed by question ID */
  answers?: Record<string, SessionInputAnswer>;
}
