import type {
  ComposeRun,
  InputSpec,
  MaterialiserInput,
  Precondition,
  SimulationPolicy,
  SweepTo,
} from '@lifi/compose-spec';

import type { Address, InputSchema, InputSpecOf } from '../types.js';

/**
 * Runtime inputs for compiling a flow. Passed to `builder.compile()` or `sdk.request()`.
 *
 * @typeParam T - The flow's input schema, used to type-check `inputs` and `assumptions`.
 */
export interface ComposeRunInput<T extends InputSchema = InputSchema> {
  /**
   * Concrete values for each declared flow input. Resource inputs accept a
   * materialiser or a literal integer amount; scalar inputs accept the matching type.
   */
  readonly inputs: { readonly [K in keyof T]: InputSpecOf<T[K]> };
  /** Optional preconditions that must hold before the flow executes (e.g. minimum balances). */
  readonly preconditions?: readonly Precondition[];
  /** The `0x`-prefixed address of the transaction signer. */
  readonly signer: Address;
  /**
   * Optional assumed token amounts for resource inputs, keyed by input name.
   * When a materialiser resolves amounts at execution time rather than upfront,
   * assumptions let the compiler plan routes using estimated values. The keys
   * must match the input names declared in the flow schema.
   */
  readonly assumptions?: { readonly [K in keyof T]?: bigint };
  /** Optional integrator referrer identifier. */
  readonly referrer?: string;
  /**
   * Optional integrator fee in basis points (1bp = 0.01%, max `9000` = 90%).
   * A non-zero value requires the request to be authenticated with an
   * integration-scoped API key — the integration is derived from the key, not
   * this field. Defaults to `0` (no fee) when omitted.
   */
  readonly integratorFeeBps?: number;
  /** Maximum acceptable price impact in basis points (e.g. `300` = 3%). */
  readonly maxPriceImpactBps?: number;
  /**
   * Destination for sweeping all proxy-held terminal resources after execution.
   * Pass a literal `0x`-prefixed address or `{ $ref: "context.sender" }` to
   * send remaining tokens to the signer.
   */
  readonly sweepTo?: SweepTo;
  /**
   * Controls behavior when simulation detects a revert.
   * `'strict'` (default) returns an error; `'allow-revert'` returns a partial
   * result with the transaction and revert diagnostics.
   */
  readonly simulationPolicy?: SimulationPolicy;
  /**
   * When `true`, the server also simulates the final user-facing transaction
   * after the structured simulation succeeds; reverts honour
   * `simulationPolicy`. On success the result is echoed back on the response as
   * `userProgramSimulation`. Defaults to `false`.
   */
  readonly simulateUserProgram?: boolean;
  /**
   * When `true`, the server checks on-chain ERC-20 allowances and omits
   * approvals that are already sufficient. Defaults to `false`.
   */
  readonly checkOnChainAllowances?: boolean;
}

/**
 * Creates an untyped materialiser input spec. This is a low-level escape hatch
 * for materialiser kinds not yet covered by the generated helpers.
 *
 * Prefer the typed helpers in the `materialisers` namespace (e.g.
 * `materialisers.balanceOf()`, `materialisers.directDeposit()`) which provide
 * compile-time config validation.
 *
 * @param kind - The materialiser type (e.g. `"balanceOf"`, `"directDeposit"`).
 * @param config - Optional materialiser-specific configuration.
 * @returns A {@link MaterialiserInput} for use in {@link ComposeRunInput.inputs}.
 *
 * @example
 * ```ts
 * const run = {
 *   inputs: { token: materialiser('customKind', { myParam: '0x...' }) },
 *   signer: '0x...',
 * };
 * ```
 */
export const materialiser = (
  kind: string,
  config: Record<string, unknown> = {},
): MaterialiserInput => ({
  kind,
  ...config,
});

/**
 * Drops the keys whose value is `undefined`, so optional fields are omitted
 * from the wire payload rather than serialized as `null`.
 *
 * A local copy of `pickDefined` from `@lifi/utils`: this package is published
 * to npm and `@lifi/utils` is a private workspace package, so it cannot be a
 * runtime dependency here.
 */
const pickDefined = <S extends Record<string, unknown>>(
  obj: S,
): Partial<{ [K in keyof S]: Exclude<S[K], undefined> }> =>
  Object.fromEntries(
    Object.entries(obj).filter(([, value]) => value !== undefined),
  ) as Partial<{ [K in keyof S]: Exclude<S[K], undefined> }>;

/**
 * Assembles a {@link ComposeRun} object from individual parameters.
 * Used internally by the SDK; prefer {@link ComposeRunInput} for external use.
 *
 * @internal
 */
export const buildRun = (run: {
  inputs: Record<string, InputSpec>;
  preconditions?: readonly Precondition[];
  signer: string;
  assumptions?: Record<string, bigint>;
  referrer?: string;
  integratorFeeBps?: number;
  maxPriceImpactBps?: number;
  sweepTo?: SweepTo;
  simulationPolicy?: SimulationPolicy;
  simulateUserProgram?: boolean;
  checkOnChainAllowances?: boolean;
}): ComposeRun => ({
  inputs: run.inputs,
  signer: run.signer,
  ...pickDefined({
    // An empty precondition list is the same as none, so drop it too.
    preconditions: run.preconditions?.length ? run.preconditions : undefined,
    assumptions: run.assumptions,
    referrer: run.referrer,
    integratorFeeBps: run.integratorFeeBps,
    maxPriceImpactBps: run.maxPriceImpactBps,
    sweepTo: run.sweepTo,
    simulationPolicy: run.simulationPolicy,
    simulateUserProgram: run.simulateUserProgram,
    checkOnChainAllowances: run.checkOnChainAllowances,
  }),
});
