import type { Ref } from '@lifi/compose-spec';
import { type ParseAbiItem, parseAbiItem } from 'abitype';

import type { AbiTypeToOutputKind, TypedGuard } from '../types.js';

import type { AnyBindable, CallArgs } from './FlowBuilderCore.js';
import type { Bindable, OutputHandle } from './handles.js';
import { handleToRef } from './handles.js';

/**
 * Type-level extraction of named bind keys from a function signature string.
 * When `TSig` is a string literal matching a Solidity function signature,
 * resolves to a record whose keys are the parameter names and values are
 * `Bindable<kind>` where the kind is derived from each parameter's Solidity
 * type via `AbiTypeToOutputKind`. For recognised `SolType` parameters
 * (e.g. `uint256`, `address`, `uint128`) the bind slot is precisely typed;
 * for other Solidity types (e.g. tuples) the slot accepts any scalar handle.
 * Falls back to `Record<string, AnyBindable>` for non-literal strings.
 */
export type SignatureBind<TSig extends string> = string extends TSig
  ? Record<string, AnyBindable>
  : ParseAbiItem<TSig> extends {
        type: 'function';
        inputs: infer I extends readonly { name: string; type: string }[];
      }
    ? {
        readonly [P in I[number] as P['name']]: Bindable<
          AbiTypeToOutputKind<P['type']>
        >;
      }
    : Record<string, AnyBindable>;

// ---------------------------------------------------------------------------
// Return-type inference from function signatures
// ---------------------------------------------------------------------------

/**
 * Describes the output type(s) of a parsed function signature as a
 * human-readable string for inclusion in type-level error messages.
 */
type DescribeOutputs<O extends readonly { type: string }[]> =
  O extends readonly [{ type: infer T extends string }] ? T : 'multiple values';

/**
 * A string-literal type that surfaces a clear error when the user provides a
 * function signature whose return type is not supported by core.call.
 * The message is visible in IDE tooltips and compiler diagnostics.
 */
type UnsupportedReturnTypeError<TLabel extends string, TSig extends string> =
  ParseAbiItem<TSig> extends {
    type: 'function';
    outputs: infer O extends readonly { type: string }[];
  }
    ? `[TypeError] ${TLabel}: return type '${DescribeOutputs<O>}' is not supported — only 'uint256' or void signatures are allowed`
    : `[TypeError] ${TLabel}: unable to parse return type from signature`;

/**
 * Conditional output type for `core.call` based on the function signature's
 * return type:
 *
 * - `returns (uint256)` → `{ result: OutputHandle }`
 * - No returns clause   → `{ result: undefined }`
 * - Anything else        → `{ result: "[TypeError] ..." }` (compile-time error)
 *
 * When `TSig` is a non-literal `string` (e.g. a variable, not a string
 * constant), the type falls back to `{ result: OutputHandle | undefined }`
 * so that both void and uint256 call sites remain assignable.
 */
export type CoreCallResult<TSig extends string> = string extends TSig
  ? { readonly result: OutputHandle<'uint256'> | undefined }
  : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }
    ? { readonly result: undefined }
    : ParseAbiItem<TSig> extends {
          type: 'function';
          outputs: readonly [{ type: 'uint256' }];
        }
      ? { readonly result: OutputHandle<'uint256'> }
      : { readonly result: UnsupportedReturnTypeError<'core.call', TSig> };

/**
 * Conditional output type for `core.staticCall`. staticCall always reads a
 * value, so void signatures are also rejected:
 *
 * - `returns (uint256)` → `{ result: OutputHandle }`
 * - No returns / other  → `{ result: "[TypeError] ..." }` (compile-time error)
 */
export type StaticCallResult<TSig extends string> = string extends TSig
  ? { readonly result: OutputHandle<'uint256'> }
  : ParseAbiItem<TSig> extends {
        type: 'function';
        outputs: readonly [{ type: 'uint256' }];
      }
    ? { readonly result: OutputHandle<'uint256'> }
    : ParseAbiItem<TSig> extends { type: 'function'; outputs: readonly [] }
      ? {
          readonly result: `[TypeError] core.staticCall: function has no return value — staticCall requires a 'returns (uint256)' signature`;
        }
      : {
          readonly result: UnsupportedReturnTypeError<'core.staticCall', TSig>;
        };

/**
 * Parses a Solidity function signature and returns parameter names in order.
 * Delegates to abitype's runtime `parseAbiItem` — the same parser that powers
 * the compile-time `ParseAbiItem<TSig>` type — so runtime and type-level
 * parsing can never diverge.
 *
 * @throws if the signature is not a valid function signature or a parameter is unnamed
 */
export const parseFunctionParams = (functionSignature: string): string[] => {
  const parsed = parseAbiItem(functionSignature);
  if (parsed.type !== 'function') {
    throw new Error(
      `core.call: expected a function signature, got "${parsed.type}": "${functionSignature}"`,
    );
  }

  return parsed.inputs.map((input, index) => {
    if (!input.name) {
      throw new Error(
        `core.call: parameter at index ${index} in signature has no name. All parameters must be named for named binds to work.`,
      );
    }
    return input.name;
  });
};

/**
 * Converts any `AnyBindable` to a `Ref` (`{ $ref: string }`).
 * Handles carry a `_tag` property and are converted via `handleToRef`;
 * `Ref` objects pass through as-is.
 */
export const toBindRef = (bindable: AnyBindable): Ref => {
  if ('_tag' in bindable) return handleToRef(bindable);
  return bindable;
};

export interface BuildCallWireFormatInput {
  readonly resource?: AnyBindable;
  readonly bind: Record<string, AnyBindable>;
  readonly config: Record<string, unknown>;
  readonly guards?: readonly TypedGuard[];
}

export interface CallWireResult {
  readonly op: 'core.call' | 'core.invoke';
  readonly bind: Record<string, Ref>;
  readonly config: Record<string, unknown>;
  readonly guards?: readonly TypedGuard[];
}

/**
 * Parses a function signature from `config`, validates bind keys against it,
 * and converts named binds to positional `$ref` args.
 */
const resolvePositionalArgs = (
  label: string,
  bind: Record<string, AnyBindable>,
  config: Record<string, unknown>,
): Ref[] => {
  const functionSignature = config.functionSignature;
  if (typeof functionSignature !== 'string') {
    throw new Error(
      `${label}: config.functionSignature is required and must be a string`,
    );
  }

  const paramNames = parseFunctionParams(functionSignature);
  const paramSet = new Set(paramNames);
  const bindKeys = Object.keys(bind);
  const bindSet = new Set(bindKeys);

  const missing = paramNames.filter((n) => !bindSet.has(n));
  const extra = bindKeys.filter((k) => !paramSet.has(k));

  if (missing.length > 0 || extra.length > 0) {
    throw new Error(
      `${label}: bind keys [${bindKeys.join(
        ', ',
      )}] do not match signature parameters [${paramNames.join(
        ', ',
      )}]. Missing: ${missing.join(', ') || '(none)'}. Extra: ${
        extra.join(', ') || '(none)'
      }.`,
    );
  }

  return paramNames.map((name) => {
    const bindable = bind[name];
    if (bindable === undefined) {
      throw new Error(
        `${label}: bind value for parameter "${name}" is undefined`,
      );
    }
    return toBindRef(bindable);
  });
};

/**
 * Transforms the user-facing `resource` + named `bind` API into the wire
 * format that the backend expects:
 * - With resource: `core.call` with `bind: { input: <resourceRef> }`
 * - Without resource: `core.invoke` with `bind: {}`
 * - `config: { ...userConfig, args: [positional refs...] }`
 */
export const buildCallWireFormat = (
  args: BuildCallWireFormatInput,
): CallWireResult => {
  const positionalArgs = resolvePositionalArgs(
    'core.call',
    args.bind,
    args.config,
  );
  const hasResource = args.resource !== undefined;

  return {
    op: hasResource ? 'core.call' : 'core.invoke',
    bind: hasResource ? { input: toBindRef(args.resource) } : {},
    config: { ...args.config, args: positionalArgs },
    ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
  };
};

export interface BuildStaticCallWireFormatInput {
  readonly bind: Record<string, AnyBindable>;
  readonly config: Record<string, unknown>;
  readonly guards?: readonly TypedGuard[];
}

/**
 * Transforms the user-facing named `bind` API into the wire format that the
 * backend expects for `core.staticCall`. Same as `buildCallWireFormat` but
 * with no `resource` field — staticCall has no resource inputs.
 */
export const buildStaticCallWireFormat = (
  args: BuildStaticCallWireFormatInput,
): CallArgs => {
  const positionalArgs = resolvePositionalArgs(
    'core.staticCall',
    args.bind,
    args.config,
  );

  return {
    bind: {},
    config: { ...args.config, args: positionalArgs },
    ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
  };
};
