import type { Resource, StaticSolType } from '@lifi/compose-spec';

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

/**
 * A handle referencing a flow input declared in the flow's input schema.
 * The phantom `__outputKind` field carries the type parameter through
 * TypeScript's structural type system without affecting runtime shape.
 */
export interface InputHandle<T extends OutputKind = OutputKind> {
  readonly _tag: 'input';
  readonly inputName: string;
  readonly __outputKind?: T;
}

/**
 * An input handle for a resource (token) input, carrying the resource
 * declaration alongside the reference.
 */
export interface ResourceInputHandle extends InputHandle<'resource'> {
  readonly resource: Resource;
}

/**
 * A handle referencing an output port of a previously declared operation node.
 * The phantom `__outputKind` field carries the type parameter through
 * TypeScript's structural type system without affecting runtime shape.
 */
export interface OutputHandle<T extends OutputKind = OutputKind> {
  readonly _tag: 'output';
  readonly nodeId: string;
  readonly portName: string;
  readonly __outputKind?: T;
}

/** Discriminated union of all handle types that can be bound to operation arguments. */
export type AnyHandle = InputHandle | OutputHandle;

/** Type guard that narrows an {@link AnyHandle} to an {@link InputHandle}. */
export const isInputHandle = (h: AnyHandle): h is InputHandle =>
  h._tag === 'input';

/** Type guard that narrows an {@link AnyHandle} to an {@link OutputHandle}. */
export const isOutputHandle = (h: AnyHandle): h is OutputHandle =>
  h._tag === 'output';

/**
 * Converts a handle to a JSON `$ref` pointer used in the flow document.
 * Input handles become `"input.<name>"`; output handles become `"<nodeId>.<port>"`.
 */
export const handleToRef = (h: AnyHandle): { $ref: string } => {
  if (h._tag === 'input') {
    if (!h.inputName) throw new Error('InputHandle has empty inputName');
    return { $ref: `input.${h.inputName}` };
  }
  if (!h.nodeId) throw new Error('OutputHandle has empty nodeId');
  if (!h.portName) throw new Error('OutputHandle has empty portName');
  return { $ref: `${h.nodeId}.${h.portName}` };
};

/**
 * For a bind slot expecting type T, widen the set of accepted handle types:
 * - `'bytes32'` is the raw 32-byte word type: accept any static-32-byte handle
 *   (`uint256`/`uintN`/`address`/`bool`/`bytes32`) plus `'resource'` (a uint256
 *   amount). At the IR1 boundary every value is a type-erased word, so any
 *   static-32-byte handle is a sound bind for a `bytes32` payload port.
 * - `'uint256'` keeps its existing widening to also accept `'resource'`-tagged
 *   handles (resources are uint256 amounts).
 * - every other type keeps exact-SolType matching.
 */
type SlotAssignable<T extends OutputKind> = T extends 'bytes32'
  ? StaticSolType | 'resource'
  : T extends 'uint256'
    ? T | 'resource'
    : T;

/**
 * The set of values accepted by a typed bind slot.
 * - `OutputHandle<T>` / `InputHandle<T>` (same type)
 * - For `'uint256'` slots, also `'resource'`-tagged handles
 * - For `'bytes32'` slots, any static-32-byte handle (and `'resource'`)
 * - `TypedRef<T>` (typed context refs, subject to `SlotAssignable`)
 *
 * Plain `{ $ref: '...' }` objects are **not** accepted — use `raw.ref<T>()`
 * to create a typed ref when you need to reference a raw path.
 */
export type Bindable<T extends OutputKind> =
  | OutputHandle<SlotAssignable<T>>
  | InputHandle<SlotAssignable<T>>
  | TypedRef<SlotAssignable<T>>;
