import {
  FAILURE_CODES,
  withFailureSignal,
  type DeviceInfo,
  type FailureSignal,
  type Platform,
  type ToolCapability,
} from "@argent/registry";

/**
 * Thrown when a tool is invoked against a device whose platform/kind it does
 * not declare in its `capability` field. The HTTP dispatcher maps this to
 * `400 Bad Request`; the message names the tool and the device so the agent
 * sees a clear "wrong target" error instead of a deeper failure.
 */
export class UnsupportedOperationError extends Error {
  readonly toolId: string;
  readonly device: DeviceInfo;
  constructor(toolId: string, device: DeviceInfo, reason?: string) {
    const detail = reason ? ` (${reason})` : "";
    super(`Tool '${toolId}' is not supported on ${device.platform} ${device.kind}${detail}.`);
    this.name = "UnsupportedOperationError";
    this.toolId = toolId;
    this.device = device;
    withFailureSignal(this, {
      error_code: FAILURE_CODES.TOOL_CAPABILITY_UNSUPPORTED_OPERATION,
      failure_stage: "tool_capability_assert_supported",
      failure_area: "tool_server",
      error_kind: "unsupported",
    });
  }
}

/**
 * Thrown by per-platform stub handlers (typically `platforms/android.ts`)
 * when the cross-platform architecture is wired up but the impl is missing.
 *
 * Distinct from `UnsupportedOperationError`:
 *   - UnsupportedOperationError: capability says no, this device class is not
 *     a target for the tool (e.g. pinch on Android — adb has no multi-touch).
 *   - NotImplementedOnPlatformError: capability *could* say yes once filled
 *     in; the work just hasn't happened. The agent should report this back
 *     to the user verbatim instead of retrying.
 *
 * The HTTP dispatcher maps this to `501 Not Implemented` and surfaces the
 * `hint` field so the agent (and contributor) can see exactly what to wire.
 *
 * Usage in a stub at `tools/<your-tool>/platforms/<platform>.ts`:
 *
 *   throw new NotImplementedOnPlatformError({
 *     toolId: "<your-tool>",
 *     platform: "android",
 *     hint: "Optional one-line nudge — e.g. which adb / xcrun command to wrap.",
 *   });
 */
export class NotImplementedOnPlatformError extends Error {
  readonly toolId: string;
  readonly platform: Platform;
  readonly hint: string | null;

  constructor(opts: { toolId: string; platform: Platform; hint?: string }) {
    const hint = opts.hint ? ` ${opts.hint}` : "";
    super(
      `Tool '${opts.toolId}' is not yet implemented on ${opts.platform}. ` +
        `The cross-platform architecture is in place — fill in ` +
        `tools/${opts.toolId}/platforms/${opts.platform}.ts and add the matching ` +
        `'${opts.platform}' block to the tool's capability declaration.${hint}`
    );
    this.name = "NotImplementedOnPlatformError";
    this.toolId = opts.toolId;
    this.platform = opts.platform;
    this.hint = opts.hint ?? null;
    withFailureSignal(this, {
      error_code: FAILURE_CODES.TOOL_PLATFORM_NOT_IMPLEMENTED,
      failure_stage: "tool_platform_dispatch",
      failure_area: "tool_server",
      error_kind: "not_implemented",
    });
  }
}

/**
 * Thrown when a tool rejects the *arguments* the caller passed — input that is
 * well-typed for the zod schema but that this tool/platform cannot carry out
 * (e.g. an unknown named key on any keyboard backend, a newline in Android/Vega
 * `keyboard` text that the on-device `input`/`send_text` can't represent, or a
 * character with no keycode on iOS/chromium). The HTTP dispatcher maps this to
 * `400 Bad Request`, matching the zod-validation path: it is a client input
 * error, not an internal fault, so it must not surface as a `500`. The
 * `.message` is the human-friendly reason, safe to bubble straight to the agent.
 */
export class InvalidToolInputError extends Error {
  /**
   * @param signal Optional telemetry-signal overrides. The HTTP 400 mapping keys
   *   off the error *class* (see http.ts), not the `error_code`, so a caller can
   *   pass a more granular `error_code` / `failure_stage` / `error_kind` — e.g.
   *   the keyboard backends' `KEYBOARD_KEY_UNSUPPORTED` /
   *   `KEYBOARD_CHARACTER_UNSUPPORTED` / `VEGA_TEXT_INVALID` classifications
   *   (from #420) — and keep both the granular telemetry bucket AND the 400
   *   status. Defaults to the generic `TOOL_INPUT_INVALID` / validation signal.
   */
  constructor(message: string, signal?: Partial<FailureSignal>) {
    super(message);
    this.name = "InvalidToolInputError";
    withFailureSignal(this, {
      error_code: FAILURE_CODES.TOOL_INPUT_INVALID,
      failure_stage: "tool_input_validation",
      failure_area: "tool_server",
      error_kind: "validation",
      ...signal,
    });
  }
}

function platformMatrix(
  platform: Platform,
  capability: ToolCapability
): Record<string, boolean | undefined> | undefined {
  switch (platform) {
    case "ios":
      return capability.apple;
    case "ios-remote":
      return capability.appleRemote;
    case "android":
      return capability.android;
    case "chromium":
      return capability.chromium;
    case "vega":
      return capability.vega;
  }
}

/**
 * Throws if the tool's `capability` declaration doesn't include the given
 * device. A tool with no `capability` is treated as universally supported —
 * useful for system / workspace tools that don't touch a device.
 */
export function assertSupported(
  toolId: string,
  capability: ToolCapability | undefined,
  device: DeviceInfo
): void {
  if (!capability) return;
  const matrix = platformMatrix(device.platform, capability);
  if (!matrix) {
    throw new UnsupportedOperationError(toolId, device, `no ${device.platform} support declared`);
  }
  const supported = matrix[device.kind] === true;
  if (!supported) {
    throw new UnsupportedOperationError(toolId, device, `kind '${device.kind}' not supported`);
  }
  if (capability.supports && !capability.supports(device)) {
    throw new UnsupportedOperationError(toolId, device, "supports() refiner rejected device");
  }
}
