import type { AppliedGuard, SolType } from '@lifi/compose-spec';

import type { OutputKind, TypedLiteral, TypedRef } from '../types.js';

/**
 * Creates a typed `$ref` pointer for use in bind slots.
 *
 * This is an **escape hatch** for referencing values that the typed builder
 * API doesn't cover — for example, outputs of an `untypedOp` node or custom
 * context paths. The caller is responsible for choosing the correct type
 * parameter; no runtime validation occurs.
 *
 * Prefer typed handles (`OutputHandle`, `InputHandle`) and `builder.context`
 * whenever possible. Use `ref` only when you need to construct a `$ref` path
 * manually and want it accepted by a `Bindable<T>` slot.
 *
 * @typeParam T - The output kind this ref represents (e.g. `'address'`, `'uint256'`, `'resource'`).
 * @param path - The `$ref` path (e.g. `"context.sender"`, `"myNode.result"`).
 * @returns A {@link TypedRef} accepted by `Bindable<T>` slots matching `T`.
 *
 * @example
 * ```ts
 * import { raw } from '@lifi/composer-sdk';
 *
 * // Reference an untypedOp output in a typed operation:
 * builder.untypedOp('custom', 'some.op', {
 *   bind: { x: { $ref: 'input.token' } },
 *   config: {},
 * });
 * builder.core.add('sum', {
 *   bind: {
 *     a: raw.ref<'uint256'>('custom.result'),
 *     b: someTypedHandle,
 *   },
 * });
 * ```
 */
export const ref = <T extends OutputKind>(path: string): TypedRef<T> =>
  // The brand is phantom, so the runtime value is the bare wire ref. This is
  // the only place a `TypedRef` is minted; the cast is what the brand buys.
  ({ $ref: path }) as TypedRef<T>;

/**
 * Creates a typed literal for use in bind slots: a constant baked into the
 * flow document rather than supplied at run time.
 *
 * Prefer a declared flow input and `builder.inputs.<name>` — a value the
 * caller supplies per run stays out of the document. Use a literal when the
 * flow has no run of its own to take the value from: the `continuationFlow`
 * of a `continuation.settle` node is stored by the settlement backend and
 * re-run later against the deferred proceeds, so its delivery address has to
 * be part of the committed document.
 *
 * @typeParam T - The Solidity type of the literal (e.g. `'address'`).
 * @param kind - The literal's Solidity type.
 * @param value - The literal value, as the wire format carries it: a string.
 * @returns A {@link TypedLiteral} accepted by `Bindable<T>` slots matching `T`.
 *
 * @example
 * ```ts
 * import { raw } from '@lifi/composer-sdk';
 *
 * builder.core.transfer('deliver', {
 *   bind: {
 *     amount: zap.amountOut,
 *     recipient: raw.literal('address', '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045'),
 *   },
 *   config: {},
 * });
 * ```
 */
export const literal = <T extends SolType>(
  kind: T,
  value: string,
): TypedLiteral<T> => ({ kind, value });

/**
 * Creates a raw guard object for use in the `guards` array of an operation call.
 *
 * @param kind - The guard type (e.g. `"slippage"`).
 * @param config - Guard-specific configuration (e.g. `{ toleranceBps: 300 }`).
 * @returns An {@link AppliedGuard} object.
 */
export const rawGuard = (
  kind: string,
  config: Record<string, unknown> = {},
): AppliedGuard => {
  if ('kind' in config) {
    throw new Error(
      `rawGuard: 'kind' must not appear in config (got "${String(
        config.kind,
      )}"). Pass it as the first argument instead.`,
    );
  }
  return { ...config, kind };
};
