import type { CommandCapability } from '../capabilities.ts';
import type { DaemonRequest } from '../../daemon/types.ts';
import { resolveWaitBudgetMs } from '../wait-positionals.ts';
import {
  DEFAULT_TIMEOUT_POLICY,
  INSTALL_REQUEST_TIMEOUT_MS,
  PREPARE_REQUEST_TIMEOUT_MS,
} from './timeout-policy.ts';
import { resolvePostActionObservationSupport } from './post-action-observation.ts';
import type { PostActionObservationSupport } from './post-action-observation.ts';
import type {
  CommandCatalogGroup,
  CommandDescriptor,
  CommandResponseDataTransform,
  CommandTimeoutPolicy,
} from './types.ts';

type RawCommandDescriptor = Omit<CommandDescriptor, 'mcpExposed'> & {
  mcpExposed?: boolean;
};

type RawCommandCatalogGroup<T> = T extends { catalog: { group: infer Group } } ? Group : never;

type RawCommandCatalogKey<T> = T extends { catalog: { key: infer Key extends string } }
  ? Key
  : T extends { name: infer Name extends string }
    ? Name
    : never;

export type DescriptorCommandNameForCatalogGroup<Group extends CommandCatalogGroup> =
  Extract<(typeof commandDescriptors)[number], { name: string }> extends infer Descriptor
    ? Descriptor extends { name: infer Name extends string }
      ? RawCommandCatalogGroup<Descriptor> extends Group
        ? Name
        : never
      : never
    : never;

export type DescriptorCliCommandName =
  | DescriptorCommandNameForCatalogGroup<'public'>
  | DescriptorCommandNameForCatalogGroup<'local-cli'>;

export type DescriptorCatalogRecord<Group extends CommandCatalogGroup> = {
  readonly [Descriptor in Extract<
    (typeof commandDescriptors)[number],
    { name: string }
  > as RawCommandCatalogGroup<Descriptor> extends Group
    ? RawCommandCatalogKey<Descriptor>
    : never]: Descriptor['name'];
};

export type DescriptorDispatchCommandName =
  Extract<(typeof commandDescriptors)[number], { dispatch: object }> extends infer Descriptor
    ? Descriptor extends { name: infer Name extends string }
      ? Name
      : never
    : never;

// ---------------------------------------------------------------------------
// Daemon request-policy trait bundles — copied VERBATIM from
// src/daemon/daemon-command-registry.ts (DAEMON_COMMAND_DESCRIPTORS).
// ---------------------------------------------------------------------------

const ADMISSION_AND_LOCK_EXEMPT = {
  leaseAdmissionExempt: true,
  sessionExecutionLockExempt: true,
} as const;

const REQUEST_EXECUTION_EXEMPT = {
  leaseAdmissionExempt: true,
  sessionExecutionLockExempt: true,
  selectorValidationExempt: true,
} as const;

const allowAnyDeviceSessionless = (): boolean => true;

const isRecordingStartRequest = (req: DaemonRequest): boolean =>
  (req.positionals?.[0] ?? '').toLowerCase() === 'start';

const isShardedTestRequest = (req: DaemonRequest): boolean =>
  req.command === 'test' &&
  (typeof req.flags?.shardAll === 'number' || typeof req.flags?.shardSplit === 'number');

// ---------------------------------------------------------------------------
// Capability matrices — platform/kind buckets, copied VERBATIM from
// src/core/capabilities.ts (BASE_COMMAND_CAPABILITY_MATRIX).
//
// The per-command `supports()` / `unsupportedHint()` device closures that used to
// live here were RELOCATED VERBATIM onto the owning PlatformPlugin's
// `capability.supportsByDefault` / `unsupportedHintByDefault` in Phase 3 step b.2
// (the Apple family's closures live on the Apple plugin, src/platforms/apple/plugin.ts;
// android/linux/web plugins are wired in src/core/interactors/register-builtins.ts). The
// capability facet now carries platform/kind buckets only; admission reads the closure
// off the plugin.
// ---------------------------------------------------------------------------

const APPLE_SIM_AND_DEVICE = { simulator: true, device: true };
const ANDROID_ALL = { emulator: true, device: true, unknown: true };
const LINUX_DEVICE = { device: true };
const LINUX_NONE = {};

const ALL_DEVICE_COMMAND_CAPABILITY = {
  apple: APPLE_SIM_AND_DEVICE,
  android: ANDROID_ALL,
  linux: LINUX_DEVICE,
} satisfies CommandCapability;
const APP_RUNTIME_CAPABILITY = ALL_DEVICE_COMMAND_CAPABILITY;
const APP_INVENTORY_CAPABILITY = {
  apple: APPLE_SIM_AND_DEVICE,
  android: ANDROID_ALL,
  linux: LINUX_NONE,
} satisfies CommandCapability;
const APP_INSTALL_CAPABILITY = {
  apple: APPLE_SIM_AND_DEVICE,
  android: ANDROID_ALL,
  linux: LINUX_NONE,
} satisfies CommandCapability;

// ---------------------------------------------------------------------------
// Timeout policies — descriptor-owned request-envelope budget source and
// on-timeout daemon policy (ADR 0008). This replaces the two deleted client
// hand lists (`isExplicitTimeoutCommand` in daemon-client.ts and
// `DAEMON_PRESERVING_TIMEOUT_COMMANDS` in daemon-client-timeout.ts) plus the
// per-command envelope branches of `resolveDaemonRequestTimeoutMs`.
// ---------------------------------------------------------------------------

// Read-only capture commands that can block in platform accessibility bridges
// while the app is crashed or never idle share snapshot's failure mode. Keep the
// daemon/session alive on their timeouts so callers can still collect
// screenshot/perf/log evidence and close the session after the runner abort
// path has been triggered — resetting the daemon here turned one timed-out wait
// into a lost session for every session the daemon owned.
const PRESERVE_DAEMON_TIMEOUT_POLICY: CommandTimeoutPolicy = {
  ...DEFAULT_TIMEOUT_POLICY,
  onTimeout: 'preserve-daemon',
};

// Installs run long device subprocesses; their envelope stays above the longest
// platform install subprocess timeout (see INSTALL_REQUEST_TIMEOUT_MS).
const INSTALL_TIMEOUT_POLICY: CommandTimeoutPolicy = {
  ...DEFAULT_TIMEOUT_POLICY,
  envelopeMs: INSTALL_REQUEST_TIMEOUT_MS,
};

const DEFAULT_SETTLE_TIMEOUT_MS = 10_000;

// Settle-capable interaction commands also resolve their target through the
// same platform accessibility capture as snapshot/find (#1105): a hung capture
// is their dominant timeout mode, so on top of the --settle flag-sourced
// widening envelope above, keep the daemon (and sessions) alive on timeout too.
const SETTLE_FLAG_PRESERVE_DAEMON_TIMEOUT_POLICY: CommandTimeoutPolicy = {
  ...DEFAULT_TIMEOUT_POLICY,
  // --settle (#1101) makes --timeout bound the SETTLE wait, not the whole
  // request. Widen the envelope by the settle budget so selector/action
  // overhead still has room before the post-action wait.
  budget: {
    source: 'flag',
    envelope: 'widen',
    defaultBudgetMs: DEFAULT_SETTLE_TIMEOUT_MS,
  },
  onTimeout: 'preserve-daemon',
};

const TOUCH_INTERACTION_RESPONSE_DATA_TRANSFORM = {
  fields: {
    count: { defaultValue: 1, omitDefault: true },
    intervalMs: { defaultValue: 0, omitDefault: true },
    holdMs: { defaultValue: 0, omitDefault: true },
    jitterPx: { defaultValue: 0, omitDefault: true },
    doubleTap: { defaultValue: false, omitDefault: true },
  },
} as const satisfies CommandResponseDataTransform;

const FILL_INTERACTION_RESPONSE_DATA_TRANSFORM = {
  fields: {
    delayMs: { defaultValue: 0 },
  },
} as const satisfies CommandResponseDataTransform;

function interactionTimeoutPolicy(command: string): CommandTimeoutPolicy {
  return resolvePostActionObservationSupport(command) !== undefined
    ? SETTLE_FLAG_PRESERVE_DAEMON_TIMEOUT_POLICY
    : PRESERVE_DAEMON_TIMEOUT_POLICY;
}

function postActionObservation(command: string): PostActionObservationSupport {
  const support = resolvePostActionObservationSupport(command);
  if (support === undefined) {
    throw new Error(`Missing post-action observation descriptor support for ${command}`);
  }
  return support;
}

// ---------------------------------------------------------------------------
// The additive single source. Each entry carries the command identity facets
// plus whichever daemon, capability, batch, MCP, timeout, observation, and
// platform-dispatch traits that command owns. Public catalog identity and the
// non-public dispatch aliases now live here too; leaf views derive from this
// array rather than recreating command-name sets.
// ---------------------------------------------------------------------------

const RAW_COMMAND_DESCRIPTORS = [
  // -- lease (route: lease) --
  {
    name: 'lease_allocate',
    ownerFiles: ['src/daemon/handlers/lease.ts'],
    catalog: { group: 'internal', key: 'leaseAllocate' },
    daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'lease_heartbeat',
    ownerFiles: ['src/daemon/handlers/lease.ts'],
    catalog: { group: 'internal', key: 'leaseHeartbeat' },
    daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'lease_release',
    ownerFiles: ['src/daemon/handlers/lease.ts'],
    catalog: { group: 'internal', key: 'leaseRelease' },
    daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'artifacts',
    ownerFiles: ['src/commands/management/artifacts.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'lease', ...ADMISSION_AND_LOCK_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },

  // -- session (route: session) --
  {
    name: 'session_list',
    ownerFiles: ['src/daemon/handlers/session-inventory.ts'],
    catalog: { group: 'internal', key: 'sessionList' },
    daemon: { route: 'session', sessionKind: 'inventory', ...REQUEST_EXECUTION_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'devices',
    ownerFiles: ['src/commands/management/device.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'inventory',
      lockPolicySelectorOverride: true,
      ...REQUEST_EXECUTION_EXEMPT,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'capabilities',
    ownerFiles: ['src/commands/management/device.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'inventory',
      lockPolicySelectorOverride: true,
      preferExplicitDeviceOverExistingSession: true,
      ...REQUEST_EXECUTION_EXEMPT,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'doctor',
    ownerFiles: ['src/commands/management/doctor.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'inventory',
      lockPolicySelectorOverride: true,
      allowSessionlessDefaultDevice: allowAnyDeviceSessionless,
      ...REQUEST_EXECUTION_EXEMPT,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'apps',
    ownerFiles: ['src/commands/management/app.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'inventory',
      lockPolicySelectorOverride: true,
      preferExplicitDeviceOverExistingSession: true,
    },
    capability: APP_INVENTORY_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'boot',
    ownerFiles: ['src/commands/management/device.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'state' },
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'shutdown',
    ownerFiles: ['src/commands/management/device.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'state' },
    capability: {
      apple: { simulator: true },
      android: { emulator: true },
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'appstate',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public', key: 'appState' },
    daemon: { route: 'session', sessionKind: 'state' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'perf',
    ownerFiles: ['src/commands/perf/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'observability' },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'logs',
    ownerFiles: ['src/commands/observability/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'observability' },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'events',
    ownerFiles: ['src/commands/observability/index.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'observability',
      allowInvalidRecording: true,
      ...REQUEST_EXECUTION_EXEMPT,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'network',
    ownerFiles: ['src/commands/observability/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'observability' },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'audio',
    ownerFiles: ['src/commands/observability/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', sessionKind: 'observability' },
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: { emulator: true },
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'replay',
    ownerFiles: ['src/commands/replay/index.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'replay',
      skipSessionlessProviderDevice: isShardedTestRequest,
    },
    // Replay durations are script-dependent; --timeout bounds the envelope.
    timeoutPolicy: { ...DEFAULT_TIMEOUT_POLICY, budget: { source: 'flag' } },
    batchable: false,
  },
  {
    name: 'test',
    ownerFiles: ['src/commands/replay/index.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'session',
      sessionKind: 'replay',
      skipSessionlessProviderDevice: isShardedTestRequest,
    },
    // Test runs stream per-scenario progress and are budgeted downstream; no
    // client envelope at all.
    timeoutPolicy: { ...DEFAULT_TIMEOUT_POLICY, envelopeMs: 'unbounded' },
    batchable: true,
  },
  {
    name: 'runtime',
    ownerFiles: ['src/daemon/handlers/session-runtime-command.ts'],
    catalog: { group: 'internal' },
    daemon: { route: 'session' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'clipboard',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', replayScopedAction: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_DEVICE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'keyboard',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'install',
    ownerFiles: ['src/commands/management/install.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session' },
    capability: APP_INSTALL_CAPABILITY,
    timeoutPolicy: INSTALL_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'reinstall',
    ownerFiles: ['src/commands/management/install.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session' },
    capability: APP_INSTALL_CAPABILITY,
    timeoutPolicy: INSTALL_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'install_source',
    ownerFiles: ['src/daemon/handlers/install-source.ts'],
    catalog: { group: 'internal', key: 'installSource' },
    daemon: { route: 'session' },
    timeoutPolicy: INSTALL_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'release_materialized_paths',
    ownerFiles: ['src/daemon/handlers/install-source.ts'],
    catalog: { group: 'internal', key: 'releaseMaterializedPaths' },
    daemon: { route: 'session', ...REQUEST_EXECUTION_EXEMPT },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'push',
    ownerFiles: ['src/commands/management/push.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session' },
    dispatch: {},
    capability: {
      apple: { simulator: true },
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'trigger-app-event',
    ownerFiles: ['src/commands/management/push.ts'],
    catalog: { group: 'public', key: 'triggerAppEvent' },
    daemon: { route: 'session' },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'open',
    ownerFiles: ['src/commands/management/app.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', allowSessionlessDefaultDevice: allowAnyDeviceSessionless },
    dispatch: {},
    capability: APP_RUNTIME_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'prepare',
    ownerFiles: ['src/commands/management/prepare.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session' },
    // Runner warm-up builds are the longest fixed envelope; --timeout overrides.
    timeoutPolicy: {
      budget: { source: 'flag' },
      envelopeMs: PREPARE_REQUEST_TIMEOUT_MS,
      onTimeout: 'reset-daemon',
    },
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'batch',
    ownerFiles: ['src/commands/batch/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'close',
    ownerFiles: ['src/commands/management/app.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'session', allowInvalidRecording: true },
    dispatch: {},
    capability: APP_RUNTIME_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },

  // -- snapshot (route: snapshot) --
  {
    name: 'snapshot',
    ownerFiles: ['src/commands/capture/snapshot.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'snapshot', replayScopedAction: true },
    dispatch: {},
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    // First Apple snapshot on a device can sit behind runner startup; --timeout
    // widens the envelope, and a timeout must not tear down the daemon.
    timeoutPolicy: { ...PRESERVE_DAEMON_TIMEOUT_POLICY, budget: { source: 'flag' } },
    batchable: true,
  },
  {
    name: 'diff',
    ownerFiles: ['src/commands/capture/diff.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'snapshot', replayScopedAction: true },
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'wait',
    ownerFiles: ['src/commands/capture/wait.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'snapshot', replayScopedAction: true },
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    // The wait budget travels as a positional, not a flag; parse it the same
    // way the daemon will so the request envelope extends past it (#1075).
    timeoutPolicy: {
      ...PRESERVE_DAEMON_TIMEOUT_POLICY,
      budget: { source: 'positional-parser', parser: resolveWaitBudgetMs },
    },
    batchable: true,
  },
  {
    name: 'alert',
    ownerFiles: ['src/commands/capture/alert.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'snapshot', replayScopedAction: true },
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'settings',
    ownerFiles: ['src/commands/capture/settings.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'snapshot', replayScopedAction: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },

  // -- specialized routes --
  {
    name: 'react-native',
    ownerFiles: ['src/commands/react-native/index.ts'],
    catalog: { group: 'public', key: 'reactNative' },
    daemon: { route: 'reactNative', replayScopedAction: true },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'record',
    ownerFiles: ['src/commands/recording/index.ts'],
    catalog: { group: 'public' },
    daemon: {
      route: 'recordTrace',
      replayScopedAction: true,
      allowInvalidRecording: true,
      allowSessionlessDefaultDevice: isRecordingStartRequest,
    },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'trace',
    ownerFiles: ['src/commands/recording/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'recordTrace' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'find',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'find', replayScopedAction: true },
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: PRESERVE_DAEMON_TIMEOUT_POLICY,
    batchable: true,
  },

  // -- interaction (route: interaction) --
  // Interaction commands resolve their target through the same platform accessibility
  // capture as snapshot, so a hung capture is their dominant timeout mode. Resetting the
  // daemon here destroyed every app session the daemon owned while the app itself was
  // still healthy (#1105): keep the daemon (and sessions) alive like snapshot/wait/find,
  // and rely on request cancellation + the per-request runner recycle budget to abort the
  // stuck Apple runner work.
  {
    name: 'click',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: interactionTimeoutPolicy('click'),
    postActionObservation: postActionObservation('click'),
    responseDataTransform: TOUCH_INTERACTION_RESPONSE_DATA_TRANSFORM,
    batchable: true,
  },
  {
    name: 'fill',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: interactionTimeoutPolicy('fill'),
    postActionObservation: postActionObservation('fill'),
    responseDataTransform: FILL_INTERACTION_RESPONSE_DATA_TRANSFORM,
    batchable: true,
  },
  {
    name: 'longpress',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public', key: 'longPress' },
    daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: interactionTimeoutPolicy('longpress'),
    postActionObservation: postActionObservation('longpress'),
    batchable: true,
  },
  {
    name: 'press',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: interactionTimeoutPolicy('press'),
    postActionObservation: postActionObservation('press'),
    responseDataTransform: TOUCH_INTERACTION_RESPONSE_DATA_TRANSFORM,
    batchable: true,
  },
  {
    name: 'type',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: interactionTimeoutPolicy('type'),
    batchable: true,
  },
  {
    name: 'get',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true },
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: interactionTimeoutPolicy('get'),
    batchable: true,
  },
  {
    name: 'read',
    ownerFiles: ['src/daemon/handlers/interaction.ts'],
    catalog: { group: 'dispatch-alias' },
    dispatch: {},
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'is',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'interaction', replayScopedAction: true },
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: interactionTimeoutPolicy('is'),
    batchable: true,
  },

  // -- generic (route: generic) --
  {
    name: 'back',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'gesture',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'home',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_DEVICE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'tv-remote',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public', key: 'tvRemote' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'rotate',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'scroll',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'swipe',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'swipe-preset',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    dispatch: {},
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'pinch',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    daemon: { route: 'generic', replayScopedAction: true, androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'focus',
    ownerFiles: ['src/commands/interaction/index.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'screenshot',
    ownerFiles: ['src/commands/capture/screenshot.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true },
    dispatch: {},
    capability: ALL_DEVICE_COMMAND_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'viewport',
    ownerFiles: ['src/commands/management/viewport.ts'],
    catalog: { group: 'public' },
    daemon: { route: 'generic', replayScopedAction: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'pan',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    daemon: { route: 'generic', androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_DEVICE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'fling',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    daemon: { route: 'generic', androidBlockingDialogGuard: true },
    dispatch: {},
    capability: { apple: APPLE_SIM_AND_DEVICE, android: ANDROID_ALL, linux: LINUX_NONE },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'rotate-gesture',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    daemon: { route: 'generic', androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'transform-gesture',
    ownerFiles: ['src/core/dispatch.ts'],
    catalog: { group: 'dispatch-alias' },
    daemon: { route: 'generic', androidBlockingDialogGuard: true },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },

  // -- capability/batch-only commands (no daemon route) --
  {
    name: 'app-switcher',
    ownerFiles: ['src/commands/system/index.ts'],
    catalog: { group: 'public', key: 'appSwitcher' },
    dispatch: {},
    capability: {
      apple: APPLE_SIM_AND_DEVICE,
      android: ANDROID_ALL,
      linux: LINUX_NONE,
    },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },
  {
    name: 'install-from-source',
    ownerFiles: ['src/commands/management/install.ts'],
    catalog: { group: 'public', key: 'installFromSource' },
    capability: APP_INSTALL_CAPABILITY,
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: true,
  },

  // -- local client-backed CLI/MCP commands (no daemon route/capability) --
  {
    name: 'debug',
    ownerFiles: ['src/commands/debugging/index.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'metro',
    ownerFiles: ['src/commands/metro/index.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'session',
    ownerFiles: ['src/commands/management/session.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
  },
  {
    name: 'cdp',
    ownerFiles: ['src/cli/commands/agent-cdp.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'auth',
    ownerFiles: ['src/cli/commands/auth.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'connect',
    ownerFiles: ['src/cli/commands/connection.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'connection',
    ownerFiles: ['src/cli/commands/connection.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'disconnect',
    ownerFiles: ['src/cli/commands/connection.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'mcp',
    ownerFiles: ['src/bin.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'proxy',
    ownerFiles: ['src/cli/commands/proxy.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'react-devtools',
    ownerFiles: ['src/cli/commands/react-devtools.ts'],
    catalog: { group: 'local-cli', key: 'reactDevtools' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
  {
    name: 'web',
    ownerFiles: ['src/cli/commands/web.ts'],
    catalog: { group: 'local-cli' },
    timeoutPolicy: DEFAULT_TIMEOUT_POLICY,
    batchable: false,
    mcpExposed: false,
  },
] as const satisfies readonly RawCommandDescriptor[];

const CLI_CATALOG_GROUPS = new Set<CommandCatalogGroup>(['public', 'local-cli']);

const CLI_COMMAND_NAMES = new Set<string>(
  RAW_COMMAND_DESCRIPTORS.filter((descriptor) =>
    CLI_CATALOG_GROUPS.has(readCatalogGroup(descriptor)),
  ).map((descriptor) => descriptor.name),
);

/**
 * The additive single source of truth (ADR-0008, Phase 1 step 1). Proven
 * byte-equal to the live hand tables by `__tests__/parity.test.ts`.
 *
 * The `as const` on {@link RAW_COMMAND_DESCRIPTORS} flows through this `.map`,
 * so each entry keeps its literal `name`. That is what makes the {@link Command}
 * union below a precise set of command-name literals rather than `string`.
 */
export const commandDescriptors = RAW_COMMAND_DESCRIPTORS.map((descriptor) => ({
  ...descriptor,
  mcpExposed: resolveMcpExposure(descriptor),
})) satisfies readonly CommandDescriptor[];

/** The literal union of every registered command name. */
export type Command = (typeof commandDescriptors)[number]['name'];

export function listDescriptorCatalogCommandNames<Group extends CommandCatalogGroup>(
  group: Group,
): Array<DescriptorCommandNameForCatalogGroup<Group>> {
  return listDescriptorCatalogEntries(group)
    .map(([, name]) => name)
    .sort();
}

export function listDescriptorCatalogEntries<Group extends CommandCatalogGroup>(
  group: Group,
): Array<readonly [key: string, name: DescriptorCommandNameForCatalogGroup<Group>]> {
  return commandDescriptors
    .filter((descriptor) => readCatalogGroup(descriptor) === group)
    .map(
      (descriptor) =>
        [
          readCatalogKey(descriptor),
          descriptor.name as DescriptorCommandNameForCatalogGroup<Group>,
        ] as const,
    );
}

export function listDescriptorDispatchCommandNames(): DescriptorDispatchCommandName[] {
  return commandDescriptors
    .filter((descriptor) => 'dispatch' in descriptor && descriptor.dispatch !== undefined)
    .map((descriptor) => descriptor.name as DescriptorDispatchCommandName)
    .sort();
}

export function listMcpExposedCommandNames(): DescriptorCliCommandName[] {
  return commandDescriptors
    .filter((descriptor) => isMcpExposedCliCommand(descriptor))
    .map((descriptor) => descriptor.name as DescriptorCliCommandName)
    .sort();
}

export function listCapabilityCheckedCommandNames(): DescriptorCliCommandName[] {
  return commandDescriptors
    .filter((descriptor) => isCapabilityCheckedCliCommand(descriptor))
    .map((descriptor) => descriptor.name as DescriptorCliCommandName)
    .sort();
}

const COMMAND_DESCRIPTOR_BY_NAME: ReadonlyMap<string, CommandDescriptor> = new Map(
  commandDescriptors.map((descriptor) => [descriptor.name, descriptor]),
);

function isCliCommandName(command: string): command is DescriptorCliCommandName {
  return CLI_COMMAND_NAMES.has(command);
}

function resolveMcpExposure(descriptor: RawCommandDescriptor): boolean {
  return descriptor.mcpExposed ?? CLI_COMMAND_NAMES.has(descriptor.name);
}

function isMcpExposedCliCommand(descriptor: CommandDescriptor): boolean {
  return descriptor.mcpExposed && isCliCommandName(descriptor.name);
}

function isCapabilityCheckedCliCommand(descriptor: CommandDescriptor): boolean {
  return Boolean(descriptor.capability) && isCliCommandName(descriptor.name);
}

function readCatalogGroup(descriptor: {
  name: string;
  catalog: { group: CommandCatalogGroup; key?: string };
}): CommandCatalogGroup {
  return descriptor.catalog.group;
}

function readCatalogKey(descriptor: {
  name: string;
  catalog: { group: CommandCatalogGroup; key?: string };
}): string {
  return descriptor.catalog.key ?? descriptor.name;
}

const TIMEOUT_POLICY_BY_COMMAND: ReadonlyMap<string, CommandTimeoutPolicy> = new Map(
  commandDescriptors.map((descriptor) => [descriptor.name, descriptor.timeoutPolicy]),
);

const RESPONSE_DATA_TRANSFORM_BY_COMMAND: ReadonlyMap<string, CommandResponseDataTransform> =
  new Map(
    Array.from(COMMAND_DESCRIPTOR_BY_NAME.values()).flatMap((descriptor) =>
      descriptor.responseDataTransform
        ? [[descriptor.name, descriptor.responseDataTransform] as const]
        : [],
    ),
  );

export function resolveCommandPostActionObservationSupport(
  command: string | undefined,
): PostActionObservationSupport | undefined {
  if (command === undefined) return undefined;
  return COMMAND_DESCRIPTOR_BY_NAME.get(command)?.postActionObservation;
}

export function commandSupportsSettleObservation(command: string | undefined): boolean {
  return resolveCommandPostActionObservationSupport(command) !== undefined;
}

export function commandSupportsVerifyEvidence(command: string | undefined): boolean {
  return resolveCommandPostActionObservationSupport(command) === 'settle-and-verify';
}

/**
 * The declared timeout policy for a command (ADR 0008). Command names outside
 * the registry (internal probes, unknown commands) fall back to
 * {@link DEFAULT_TIMEOUT_POLICY} — standard envelope, reset-daemon — exactly as
 * the deleted hand lists treated unlisted commands.
 */
export function resolveCommandTimeoutPolicy(command: string | undefined): CommandTimeoutPolicy {
  if (command === undefined) return DEFAULT_TIMEOUT_POLICY;
  return TIMEOUT_POLICY_BY_COMMAND.get(command) ?? DEFAULT_TIMEOUT_POLICY;
}

export function resolveCommandResponseDataTransform(
  command: string | undefined,
): CommandResponseDataTransform | undefined {
  if (command === undefined) return undefined;
  return RESPONSE_DATA_TRANSFORM_BY_COMMAND.get(command);
}

export function listCommandResponseDataTransforms(): Array<{
  command: string;
  transform: CommandResponseDataTransform;
}> {
  return Array.from(RESPONSE_DATA_TRANSFORM_BY_COMMAND, ([command, transform]) => ({
    command,
    transform,
  }));
}

export function listCommandResponseDataTransformFieldNames(): string[] {
  return [
    ...new Set(
      Array.from(RESPONSE_DATA_TRANSFORM_BY_COMMAND.values()).flatMap((transform) =>
        Object.keys(transform.fields),
      ),
    ),
  ].sort();
}
