/**
 * Version Registry — Maps action types and features to protocol versions.
 *
 * @module version/registry
 */

import type { StateAction } from '../actions.js';
import { ActionType } from '../actions.js';
import type { ServerNotificationMap } from '../messages.js';

// ─── Protocol Version Constants ──────────────────────────────────────────────

/**
 * The current protocol version that new code speaks.
 *
 * Formatted as a [SemVer](https://semver.org) `MAJOR.MINOR.PATCH` string.
 */
export const PROTOCOL_VERSION = '0.4.0';

/**
 * Every protocol version a client built from this source tree is willing
 * to negotiate via the `initialize` handshake. Ordered **most preferred
 * first** so a server picking the first acceptable entry honors the
 * client's preference (see [versioning](../../docs/specification/versioning.md)).
 *
 * The first entry MUST equal {@link PROTOCOL_VERSION} — the version
 * "new code speaks" is by definition the most preferred one. Older
 * versions may be appended if a client retains the ability to fall back
 * to them; today only one version is advertised.
 *
 * Every generated client (Rust, Kotlin, Swift) re-exports this constant
 * verbatim. The TypeScript client consumes it directly. The per-client
 * `release-metadata.json` files are validated against this list by
 * `scripts/verify-release-metadata.ts`.
 */
export const SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = Object.freeze([
  '0.4.0',
  '0.3.0',
]);

// ─── SemVer Comparison ───────────────────────────────────────────────────────

/**
 * Parses a `MAJOR.MINOR.PATCH` SemVer string into its three numeric
 * components. Pre-release and build metadata are not supported and MUST NOT
 * be present in protocol version strings.
 *
 * Throws if `version` is not a well-formed `MAJOR.MINOR.PATCH` string.
 */
function parseSemver(version: string): readonly [number, number, number] {
  const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
  if (!match) {
    throw new Error(`Invalid protocol version: ${version}`);
  }
  return [Number(match[1]), Number(match[2]), Number(match[3])] as const;
}

/**
 * Compares two `MAJOR.MINOR.PATCH` SemVer strings.
 *
 * Returns a negative number if `a < b`, zero if `a === b`, and a positive
 * number if `a > b`.
 */
export function compareProtocolVersions(a: string, b: string): number {
  const [aMajor, aMinor, aPatch] = parseSemver(a);
  const [bMajor, bMinor, bPatch] = parseSemver(b);
  return (aMajor - bMajor) || (aMinor - bMinor) || (aPatch - bPatch);
}

// ─── Exhaustive Action → Version Map ─────────────────────────────────────────

/**
 * Maps every action type to the protocol version that introduced it.
 * Adding a new action to `StateAction` without adding it here is a compile error.
 *
 * Versions are SemVer `MAJOR.MINOR.PATCH` strings (see `PROTOCOL_VERSION`).
 */
export const ACTION_INTRODUCED_IN: { readonly [K in StateAction['type']]: string } = {
  [ActionType.RootAgentsChanged]: '0.1.0',
  [ActionType.RootActiveSessionsChanged]: '0.1.0',
  [ActionType.SessionReady]: '0.1.0',
  [ActionType.SessionCreationFailed]: '0.1.0',
  [ActionType.SessionTurnStarted]: '0.1.0',
  [ActionType.SessionDelta]: '0.1.0',
  [ActionType.SessionResponsePart]: '0.1.0',
  [ActionType.SessionToolCallStart]: '0.1.0',
  [ActionType.SessionToolCallDelta]: '0.1.0',
  [ActionType.SessionToolCallReady]: '0.1.0',
  [ActionType.SessionToolCallConfirmed]: '0.1.0',
  [ActionType.SessionToolCallComplete]: '0.1.0',
  [ActionType.SessionToolCallResultConfirmed]: '0.1.0',
  [ActionType.SessionToolCallContentChanged]: '0.1.0',
  [ActionType.SessionTurnComplete]: '0.1.0',
  [ActionType.SessionTurnCancelled]: '0.1.0',
  [ActionType.SessionError]: '0.1.0',
  [ActionType.SessionTitleChanged]: '0.1.0',
  [ActionType.SessionUsage]: '0.1.0',
  [ActionType.SessionReasoning]: '0.1.0',
  [ActionType.SessionModelChanged]: '0.1.0',
  [ActionType.SessionAgentChanged]: '0.2.0',
  [ActionType.SessionServerToolsChanged]: '0.1.0',
  [ActionType.SessionActiveClientChanged]: '0.1.0',
  [ActionType.SessionActiveClientToolsChanged]: '0.1.0',
  [ActionType.SessionPendingMessageSet]: '0.1.0',
  [ActionType.SessionPendingMessageRemoved]: '0.1.0',
  [ActionType.SessionQueuedMessagesReordered]: '0.1.0',
  [ActionType.SessionInputRequested]: '0.1.0',
  [ActionType.SessionInputAnswerChanged]: '0.1.0',
  [ActionType.SessionInputCompleted]: '0.1.0',
  [ActionType.SessionCustomizationsChanged]: '0.1.0',
  [ActionType.SessionCustomizationToggled]: '0.1.0',
  [ActionType.SessionCustomizationUpdated]: '0.1.0',
  [ActionType.SessionCustomizationRemoved]: '0.2.0',
  [ActionType.SessionMcpServerStateChanged]: '0.3.0',
  [ActionType.SessionTruncated]: '0.1.0',
  [ActionType.SessionIsReadChanged]: '0.1.0',
  [ActionType.SessionIsArchivedChanged]: '0.1.0',
  [ActionType.SessionActivityChanged]: '0.1.0',
  [ActionType.SessionChangesetsChanged]: '0.2.0',
  [ActionType.SessionConfigChanged]: '0.1.0',
  [ActionType.SessionMetaChanged]: '0.1.0',
  [ActionType.ChangesetStatusChanged]: '0.2.0',
  [ActionType.ChangesetFileSet]: '0.2.0',
  [ActionType.ChangesetFileRemoved]: '0.2.0',
  [ActionType.ChangesetOperationsChanged]: '0.2.0',
  [ActionType.ChangesetOperationStatusChanged]: '0.3.0',
  [ActionType.ChangesetCleared]: '0.2.0',
  [ActionType.AnnotationsSet]: '0.3.0',
  [ActionType.AnnotationsUpdated]: '0.4.0',
  [ActionType.AnnotationsRemoved]: '0.3.0',
  [ActionType.AnnotationsEntrySet]: '0.3.0',
  [ActionType.AnnotationsEntryRemoved]: '0.3.0',
  [ActionType.RootTerminalsChanged]: '0.1.0',
  [ActionType.RootConfigChanged]: '0.1.0',
  [ActionType.TerminalData]: '0.1.0',
  [ActionType.TerminalInput]: '0.1.0',
  [ActionType.TerminalResized]: '0.1.0',
  [ActionType.TerminalClaimed]: '0.1.0',
  [ActionType.TerminalTitleChanged]: '0.1.0',
  [ActionType.TerminalCwdChanged]: '0.1.0',
  [ActionType.TerminalExited]: '0.1.0',
  [ActionType.TerminalCleared]: '0.1.0',
  [ActionType.TerminalCommandDetectionAvailable]: '0.1.0',
  [ActionType.TerminalCommandExecuted]: '0.1.0',
  [ActionType.TerminalCommandFinished]: '0.1.0',
  [ActionType.ResourceWatchChanged]: '0.2.0',
};

/**
 * Returns whether the given action type is known to the specified protocol version.
 */
export function isActionKnownToVersion(action: StateAction, clientVersion: string): boolean {
  return compareProtocolVersions(ACTION_INTRODUCED_IN[action.type], clientVersion) <= 0;
}

// ─── Exhaustive Notification Method → Version Map ──────────────────────────

/**
 * Server → client notification method names that are part of the AHP
 * protocol surface. The set is a subset of {@link ServerNotificationMap}
 * keys that excludes `action` (the action envelope) since action versions
 * are tracked via {@link ACTION_INTRODUCED_IN}.
 */
export type ProtocolNotificationMethod = Exclude<keyof ServerNotificationMap, 'action'>;

/**
 * Maps every server → client protocol notification method to the protocol
 * version that introduced it. Adding a new notification method to
 * {@link ServerNotificationMap} without adding it here is a compile error.
 *
 * Versions are SemVer `MAJOR.MINOR.PATCH` strings (see `PROTOCOL_VERSION`).
 */
export const NOTIFICATION_INTRODUCED_IN: { readonly [K in ProtocolNotificationMethod]: string } = {
  'root/sessionAdded': '0.1.0',
  'root/sessionRemoved': '0.1.0',
  'root/sessionSummaryChanged': '0.1.0',
  'auth/required': '0.1.0',
  'otlp/exportLogs': '0.2.0',
  'otlp/exportTraces': '0.2.0',
  'otlp/exportMetrics': '0.2.0',
};

/**
 * Returns whether the given notification method is known to the specified
 * protocol version.
 */
export function isNotificationKnownToVersion(method: ProtocolNotificationMethod, clientVersion: string): boolean {
  return compareProtocolVersions(NOTIFICATION_INTRODUCED_IN[method], clientVersion) <= 0;
}
