import type {
  HandlePort,
  Port,
  ResourceOutputPort,
  ResourcePort,
} from './flow.js';
import type { HandleInput, Ref, ResourceInput, SolType } from './flowSchema.js';
import type { Availability } from './resource.js';
import { erc20Resource, nativeResource } from './resource.js';
import type { HandleUnits } from './zodSchemas.js';

export interface OpHandleOptions<Opt extends boolean = boolean> {
  readonly expose?: boolean;
  readonly units?: HandleUnits;
  readonly optional?: Opt;
}

export const inputRef = (port: string): Ref => ({ $ref: `input.${port}` });

export const outputRef = (node: string, port: string): Ref => ({
  $ref: `${node}.${port}`,
});

/** Returns `{ [key]: value }` when `value` is not nullish, else `{}`. */
const optionalProp = <K extends string, V>(
  key: K,
  value: V | undefined | null,
): Partial<Record<K, V>> =>
  value !== undefined && value !== null
    ? ({ [key]: value } as Partial<Record<K, V>>)
    : {};

export const linear = <N extends string, T extends SolType>(
  name: N,
  type: T,
): Port & { readonly name: N; readonly type: T; readonly mode: 'linear' } => ({
  name,
  type,
  mode: 'linear',
  availability: 'now',
});

export const copy = <N extends string, T extends SolType>(
  name: N,
  type: T,
): Port & { readonly name: N; readonly type: T; readonly mode: 'copy' } => ({
  name,
  type,
  mode: 'copy',
  availability: 'now',
});

export const handle = <N extends string, T extends SolType>(
  name: N,
  type: T,
): HandleInput & { readonly name: N; readonly type: T } => ({ name, type });

export const native = <N extends string>(
  name: N,
  chainId: number,
): ResourceInput & { readonly name: N } => ({
  name,
  resource: nativeResource(chainId),
});

export const erc20 = <N extends string>(
  name: N,
  token: string,
  chainId: number,
): ResourceInput & { readonly name: N } => ({
  name,
  resource: erc20Resource(token, chainId),
});

// `NoInfer` on the return type is load-bearing: these ports land in tuples
// contextually typed by `ResourcePort`/`HandlePort`, whose `optional?: boolean`
// would otherwise win the inference race and widen `Opt` to `boolean`,
// collapsing the optional-aware binding types downstream.
export const resource = <N extends string, Opt extends boolean = false>(
  name: N,
  accepts: 'erc20' | 'native' | 'any',
  options?: { readonly optional?: Opt },
): ResourcePort & { readonly name: N; readonly optional: NoInfer<Opt> } =>
  ({
    kind: 'resource',
    name,
    accepts,
    mode: 'linear',
    ...optionalProp('optional', options?.optional),
  }) as ResourcePort & { readonly name: N; readonly optional: Opt };

export const readResource = <N extends string>(
  name: N,
  accepts: 'erc20' | 'native' | 'any',
): ResourcePort & { readonly name: N; readonly mode: 'copy' } => ({
  kind: 'resource',
  name,
  accepts,
  mode: 'copy',
});

export const opHandle = <
  N extends string,
  T extends SolType,
  Opt extends boolean = false,
>(
  name: N,
  type: T,
  options?: OpHandleOptions<Opt>,
): HandlePort & {
  readonly name: N;
  readonly type: T;
  readonly optional: NoInfer<Opt>;
} =>
  ({
    kind: 'handle',
    name,
    type,
    mode: 'copy',
    ...optionalProp('expose', options?.expose),
    ...optionalProp('units', options?.units),
    ...optionalProp('optional', options?.optional),
  }) as HandlePort & {
    readonly name: N;
    readonly type: T;
    readonly optional: Opt;
  };

// Options accepted by `resourceOutput`: the bare `Availability` shorthand or
// the full record form.
type ResourceOutputOptions =
  | Availability
  | {
      readonly availability?: Availability;
      readonly providesMinimum?: boolean;
      readonly omitIfZero?: boolean;
      readonly deliveryAddressInput?: string;
    };

// Resolve the *type-level* availability of a resource output from the options
// the caller passed. Preserving the availability literal is load-bearing:
// `defineOp` extracts the `'future'` port names from this literal, so widening
// it here would erase the future/now distinction the lowering contract needs.
//
// The runtime `availability` property may be ABSENT — the `'now'` cases omit it
// (see `optionalProp` below) because a missing availability already means
// `'now'` everywhere it is read. The type nonetheless reports `'now'`: a
// documented type-level fiction that treats the missing property as its
// effective value rather than surfacing `undefined`.
//
// A widened `Availability` value (neither literal) stays widened. It is not the
// `'future'` literal, so downstream treats it as non-future — a handle is
// required — which is the conservative choice (supplying a handle is always
// allowed).
type OutputAvailability<O> = [O] extends ['future']
  ? 'future'
  : [O] extends ['now']
    ? 'now'
    : [O] extends [string]
      ? Availability
      : O extends { readonly availability: infer A }
        ? [A] extends ['future']
          ? 'future'
          : [A] extends ['now']
            ? 'now'
            : Availability
        : 'now';

export const resourceOutput = <
  N extends string,
  const O extends ResourceOutputOptions = 'now',
>(
  name: N,
  options?: O,
): ResourceOutputPort & {
  readonly name: N;
  readonly availability: OutputAvailability<O>;
} => {
  // Widen away the generic before narrowing: `typeof` cannot narrow a value
  // of generic type `O`, so the union members would keep their string arms.
  const raw: ResourceOutputOptions | undefined = options;
  const opts = typeof raw === 'string' ? { availability: raw } : raw;
  return {
    kind: 'resource_output',
    name,
    mode: 'linear',
    ...optionalProp('availability', opts?.availability),
    ...optionalProp('providesMinimum', opts?.providesMinimum),
    ...optionalProp('omitIfZero', opts?.omitIfZero),
    ...optionalProp('deliveryAddressInput', opts?.deliveryAddressInput),
  } as ResourceOutputPort & {
    readonly name: N;
    readonly availability: OutputAvailability<O>;
  };
};
