/**
 * Resolves and validates a function call against a contract's ABI.
 *
 * Rules:
 *   - Function must exist on the ABI
 *   - Function must be `view` or `pure` (read-only — no state changes, no value transfers)
 *   - Args must match the input arity
 *
 * If the user provides a function signature (e.g. "balanceOf(address)") we
 * match by signature; otherwise we match by name (and refuse if overloaded).
 */

import { type AbiFunction } from 'viem';

export type ResolvedFunction = {
  abiItem: AbiFunction;
  signature: string; // canonical: "name(type1,type2)"
};

export type ResolveError = {
  error: string;
  hint?: string;
};

function canonicalSignature(item: AbiFunction): string {
  return `${item.name}(${item.inputs.map((i) => i.type).join(',')})`;
}

export function resolveFunction(
  abi: readonly any[],
  fn: string,
): ResolvedFunction | ResolveError {
  const target = fn.trim();
  const fns = abi.filter((it: any) => it.type === 'function') as AbiFunction[];

  let candidates: AbiFunction[];

  if (target.includes('(')) {
    // Signature path
    const wanted = target.replace(/\s+/g, '');
    candidates = fns.filter((it) => canonicalSignature(it) === wanted);
    if (candidates.length === 0) {
      return {
        error: `Function signature not found on contract ABI: ${target}`,
        hint: 'Try a known signature like "isAgentActive(address)" or just the name "isAgentActive".',
      };
    }
  } else {
    // Name path
    candidates = fns.filter((it) => it.name === target);
    if (candidates.length === 0) {
      return {
        error: `Function name not found on contract ABI: ${target}`,
        hint: 'Pass the full signature like "balanceOf(address)" if the function is overloaded.',
      };
    }
    if (candidates.length > 1) {
      return {
        error: `Function name "${target}" is overloaded (${candidates.length} variants).`,
        hint: `Pass the full signature, e.g. "${canonicalSignature(candidates[0])}".`,
      };
    }
  }

  const item = candidates[0];

  // Read-only enforcement
  if (item.stateMutability !== 'view' && item.stateMutability !== 'pure') {
    return {
      error: `Function "${item.name}" is not read-only (stateMutability=${item.stateMutability}).`,
      hint: 'Only view/pure functions are callable through this endpoint. State-changing calls require a real transaction signed by the caller.',
    };
  }

  return { abiItem: item, signature: canonicalSignature(item) };
}

/**
 * Coerce a JS arg value to the type expected by the ABI input.
 * Handles the common cases: address (string), uint or int (string|number to bigint),
 * bool, bytes/bytesN (0x-hex string), string, and arrays of these.
 */
export function coerceArg(value: unknown, type: string): unknown {
  if (type.endsWith('[]')) {
    const innerType = type.slice(0, -2);
    if (!Array.isArray(value)) {
      throw new Error(`Expected array for type ${type}, got ${typeof value}`);
    }
    return value.map((v) => coerceArg(v, innerType));
  }

  if (type === 'address') {
    if (typeof value !== 'string' || !/^0x[a-fA-F0-9]{40}$/.test(value)) {
      throw new Error(`Invalid address: ${value}`);
    }
    return value;
  }

  if (type === 'bool') {
    if (typeof value === 'boolean') return value;
    if (value === 'true') return true;
    if (value === 'false') return false;
    throw new Error(`Invalid bool: ${value}`);
  }

  if (type.startsWith('uint') || type.startsWith('int')) {
    if (typeof value === 'bigint') return value;
    if (typeof value === 'number') return BigInt(value);
    if (typeof value === 'string') {
      // accept "0x..." hex or decimal
      return BigInt(value);
    }
    throw new Error(`Invalid integer for ${type}: ${value}`);
  }

  if (type === 'string' || type.startsWith('bytes')) {
    if (typeof value !== 'string') {
      throw new Error(`Expected string for type ${type}`);
    }
    return value;
  }

  // tuple or unknown — pass through
  return value;
}

export function coerceArgs(
  rawArgs: unknown[],
  inputs: readonly { type: string; name?: string }[],
): unknown[] {
  if (rawArgs.length !== inputs.length) {
    throw new Error(
      `Argument count mismatch: function expects ${inputs.length}, got ${rawArgs.length}`,
    );
  }
  return rawArgs.map((v, i) => coerceArg(v, inputs[i].type));
}
