import type { Resource } from './resource.js';

/**
 * Per-chain native-asset semantics.
 *
 * Most EVM chains have an ETH-style native gas coin sent via `msg.value` (the
 * `evm-native` policy). A small number — currently only Tempo — express their
 * "native" coin as an ERC-20 token (the `as-erc20` policy). Code that needs to
 * lower or simulate a `Resource` of kind `"native"` should consult
 * `getChainCapabilities(chainId)` rather than hardcoding chain IDs.
 *
 * The compose compiler uses this registry to transparently normalise native
 * resources into ERC-20 resources on `as-erc20` chains at schema-decode time
 * (see `normalizeResource` below and the `ResourceSchema` transform in
 * `flowSchema.ts`).
 *
 * This union is the extension point for future native-asset policies. A chain
 * with no canonical native asset would get a `reject-native` arm, under which a
 * `{ kind: "native" }` resource becomes a validation error rather than being
 * rewritten. No such chain exists yet, so the arm is not defined here.
 */
export type ChainCapabilities =
  | { readonly kind: 'evm-native'; readonly chainId: number }
  | {
      readonly kind: 'as-erc20';
      readonly chainId: number;
      readonly nativeErc20: NativeErc20;
    };

export interface NativeErc20 {
  readonly token: `0x${string}`;
  readonly symbol: string;
  readonly decimals: number;
}

/**
 * The zero-address sentinel that represents the native gas coin on
 * EVM-native chains. Exported so tests and call-sites can reference it
 * by name rather than inlining 40 zeros.
 *
 * Inlined as a literal rather than imported from `@lifi/utils`: `compose-spec`
 * is the wire-format leaf package and carries no `@lifi/*` dependencies
 * (enforced by `scripts/check-compose-boundaries.mjs`).
 */
export const EVM_NATIVE_SENTINEL: `0x${string}` =
  // eslint-disable-next-line @lifi/prefer-utils-constants -- compose-spec is the wire-format leaf and must not import @lifi/utils (enforced by scripts/check-compose-boundaries.mjs); the literal is intentional here
  '0x0000000000000000000000000000000000000000';

/**
 * Whether `address` is the EVM-native zero-address sentinel, compared
 * case-insensitively. Module-local: the two call-sites below share this exact
 * comparison; it is not part of the package's public surface.
 */
const isNativeSentinel = (address: string): boolean =>
  address.toLowerCase() === EVM_NATIVE_SENTINEL.toLowerCase();

export const TEMPO_CHAIN_ID = 4217;

export const TEMPO_NATIVE_ERC20: NativeErc20 = {
  token: '0x20C0000000000000000000000000000000000000',
  symbol: 'pathUSD',
  decimals: 6,
};

const REGISTRY: ReadonlyMap<number, ChainCapabilities> = new Map<
  number,
  ChainCapabilities
>([
  [
    TEMPO_CHAIN_ID,
    {
      kind: 'as-erc20',
      chainId: TEMPO_CHAIN_ID,
      nativeErc20: TEMPO_NATIVE_ERC20,
    },
  ],
]);

export const getChainCapabilities = (chainId: number): ChainCapabilities =>
  REGISTRY.get(chainId) ?? { kind: 'evm-native', chainId };

export const isAsErc20Chain = (chainId: number): boolean =>
  getChainCapabilities(chainId).kind === 'as-erc20';

/**
 * The address that represents the chain's native asset.
 *
 * On EVM-native chains this is the zero-address sentinel `0x0…0`. On
 * `as-erc20` chains it is the chain's stand-in ERC-20 (e.g. PathUSD on Tempo).
 */
export const canonicalNativeAddress = (chainId: number): `0x${string}` => {
  const caps = getChainCapabilities(chainId);
  return caps.kind === 'as-erc20'
    ? caps.nativeErc20.token
    : EVM_NATIVE_SENTINEL;
};

/**
 * Whether `address` represents the native asset of `chainId`.
 *
 * On EVM-native chains, this is the case-insensitive zero-address check. On
 * `as-erc20` chains, the literal zero address returns `false` — only the
 * chain's stand-in ERC-20 address matches.
 */
export const isNativeAddressForChain = (
  chainId: number,
  address: string,
): boolean => {
  const caps = getChainCapabilities(chainId);
  const target =
    caps.kind === 'as-erc20' ? caps.nativeErc20.token : EVM_NATIVE_SENTINEL;
  return address.toLowerCase() === target.toLowerCase();
};

/**
 * Whether `address` on `chainId` is the genuine `msg.value`-style native coin:
 * an `evm-native` chain's zero-address sentinel.
 *
 * This is the single predicate that gates `msg.value` semantics — choosing a
 * `NativeBalance` over an `Erc20Balance` simulation requirement, and skipping
 * the ERC-20 approval a native input would otherwise need. It returns `false`
 * for every address on an `as-erc20` chain (including the zero sentinel),
 * because there the "native" coin is a real ERC-20 moved via `transferFrom`,
 * never `msg.value`. Use this rather than re-deriving
 * `!isAsErc20Chain(chainId) && address === EVM_NATIVE_SENTINEL` at call sites,
 * and rather than `isNativeAddressForChain`, which returns `true` for the
 * stand-in ERC-20 (PathUSD) on `as-erc20` chains and would wrongly route it to
 * `msg.value`.
 */
export const isMsgValueNative = (chainId: number, address: string): boolean =>
  !isAsErc20Chain(chainId) && isNativeSentinel(address);

/**
 * Rewrite a single `Resource` according to its chain's capabilities.
 *
 * On chains with the default `evm-native` policy, every resource is returned
 * unchanged. On `as-erc20` chains (currently only Tempo, chainId 4217), a
 * resource of kind `"native"` is rewritten into the equivalent ERC-20
 * resource pointing at the chain's stand-in token (PathUSD for Tempo).
 *
 * This is the rewrite primitive behind the `ResourceSchema` decode transform
 * (see `flowSchema.ts`), which is THE chokepoint: it runs every time a
 * `Resource` is parsed — flow inputs via `FlowSchema`, op configs via
 * `decodeConfig` — so every downstream pass (`normalizeFlow`, `resolve`,
 * `lower`, `simulate`) sees only ERC-20 resources on `as-erc20` chains.
 *
 * The function preserves referential identity when nothing changes (returns the
 * input `r` unchanged on evm-native chains and for ERC-20 resources) and is
 * idempotent: running it on an already-rewritten resource is a no-op.
 */
export const normalizeResource = (r: Resource): Resource => {
  if (r.kind !== 'native') return r;
  const caps = getChainCapabilities(r.chainId);
  if (caps.kind !== 'as-erc20') return r;
  return {
    kind: 'erc20',
    chainId: r.chainId,
    token: caps.nativeErc20.token,
  };
};

/**
 * Address-level counterpart of `normalizeResource` for wire fields that carry a
 * native asset as a bare token string (a continuation outcome's `token`). On
 * `as-erc20` chains the native sentinel is rewritten to the chain's stand-in
 * ERC-20; every other address passes through.
 */
export const normalizeNativeTokenAddress = (
  token: string,
  chainId: number,
): string =>
  isAsErc20Chain(chainId) && isNativeSentinel(token)
    ? canonicalNativeAddress(chainId)
    : token;
