import type {
  AppliedGuard,
  Call,
  Flow,
  FlowInput,
  Ref,
  Resource,
  SolType,
} from '@lifi/compose-spec';

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

import type {
  InputHandle,
  OutputHandle,
  ResourceInputHandle,
} from './handles.js';
import { handleToRef } from './handles.js';
import { ref } from './raw.js';

/**
 * Arguments for calling an operation node in a flow.
 */
export interface CallArgs {
  /** Maps parameter names to input/output handles or raw `$ref` pointers. */
  readonly bind: Record<string, AnyBindable>;
  /** Optional operation-specific configuration. */
  readonly config?: object;
  /** Optional guards (e.g. slippage checks) applied to this operation's outputs. */
  readonly guards?: readonly TypedGuard[];
}

/**
 * Any value that can be bound to an operation parameter: an input handle,
 * an output handle from a previous operation, or a typed `$ref` pointer.
 *
 * Use `raw.ref<T>()` to create a typed ref from a raw path string.
 */
export type AnyBindable = InputHandle | OutputHandle | TypedRef;

const toRef = (bindable: AnyBindable): Ref => {
  if ('_tag' in bindable) return handleToRef(bindable);
  return bindable;
};

/**
 * Provides references to runtime context values available inside a flow.
 * Access via `builder.context`.
 */
export interface ContextAccessor {
  /** A `$ref` pointing to the transaction sender (signer) address. */
  readonly sender: TypedRef<'address'>;
  /** A `$ref` pointing to the on-chain execution (proxy) address. */
  readonly executionAddress: TypedRef<'address'>;
}

type InputHandleOf<D extends InputDecl> = D extends Resource
  ? ResourceInputHandle
  : D extends SolType
    ? InputHandle<D>
    : InputHandle;

/**
 * A mapped type that converts an input schema into typed handles.
 * Resource inputs produce {@link ResourceInputHandle}; scalar inputs produce {@link InputHandle}.
 *
 * @typeParam T - The flow's input schema.
 */
export type InputHandles<T extends InputSchema> = {
  readonly [K in keyof T]: InputHandleOf<T[K]>;
};

/**
 * Options for creating a new flow builder.
 *
 * @typeParam T - The flow's input schema.
 */
export interface FlowOptions<T extends InputSchema> {
  /** Optional human-readable name for the flow. Defaults to a random UUID. */
  readonly name?: string;
  /** Input declarations mapping names to resource or scalar types. */
  readonly inputs: T;
}

/**
 * A Flow document branded with its input schema for type-safe request building.
 *
 * The `__inputs` field is a phantom type — it exists only at compile time for
 * TypeScript inference and is never present at runtime.
 */
export type TypedFlow<T extends InputSchema = InputSchema> = Flow & {
  readonly __inputs?: T;
};

/**
 * Core flow builder interface providing low-level access to flow construction.
 *
 * For most use cases, prefer the {@link FlowBuilder} type returned by `sdk.flow()`
 * which adds generated operation methods and a `compile` method.
 *
 * @typeParam T - The flow's input schema.
 */
export interface FlowBuilderCore<T extends InputSchema = InputSchema> {
  /** Runtime context references (sender address, execution address). */
  readonly context: ContextAccessor;
  /** Typed handles for each declared flow input, used to bind inputs to operations. */
  readonly inputs: InputHandles<T>;
  /**
   * Appends an untyped operation node to the flow. This is an escape hatch
   * for operations not yet covered by the generated methods.
   *
   * Use `raw.ref<T>()` to reference this node's outputs in subsequent typed
   * operations.
   *
   * @param id - Unique node identifier within the flow.
   * @param op - The operation name (e.g. `"custom.vaultQuery"`).
   * @param args - Bind map, config, and optional guards for the node.
   * @throws Error if a node with the same `id` already exists in the flow.
   */
  readonly untypedOp: (
    id: string,
    op: string,
    args: {
      readonly bind: Record<string, Ref>;
      readonly config: Record<string, unknown>;
      readonly guards?: readonly AppliedGuard[];
    },
  ) => void;
  /** Serialises the builder state into a {@link TypedFlow} document. */
  readonly build: () => TypedFlow<T>;
}

/** Maps output port names to their output kind for compile-time and runtime validation. */
export type OutputSpec = Record<string, OutputKind>;

/** Internal interface exposed to bindGeneratedOps — not part of the public API. */
export interface FlowBuilderInternal<
  T extends InputSchema = InputSchema,
> extends FlowBuilderCore<T> {
  readonly call: <O extends OutputSpec>(
    nodeId: string,
    op: string,
    args: CallArgs,
    outputs: O,
  ) => { readonly [K in keyof O & string]: OutputHandle<O[K]> };
}

const isResource = (decl: InputDecl): decl is Resource =>
  typeof decl === 'object' && decl !== null && 'kind' in decl;

/**
 * Creates a new flow builder targeting the given chain.
 *
 * @param chainId - The EVM chain ID (e.g. `1` for Ethereum mainnet).
 * @param options - Flow configuration including input declarations.
 * @returns A {@link FlowBuilderInternal} instance.
 * @internal
 */
export const createFlowBuilderCore = <T extends InputSchema>(
  chainId: number,
  options: FlowOptions<T>,
): FlowBuilderInternal<T> => {
  const id = options.name ?? crypto.randomUUID();
  const flowInputs: FlowInput[] = [];
  const nodes: Call[] = [];
  const nodeIds = new Set<string>();

  const inputHandles = {} as Record<string, InputHandle | ResourceInputHandle>;
  for (const [name, decl] of Object.entries(options.inputs)) {
    if (isResource(decl)) {
      if (decl.chainId !== chainId) {
        throw new Error(
          `Input "${name}" has chainId ${decl.chainId} but flow targets chain ${chainId}`,
        );
      }
      flowInputs.push({ name, resource: decl });
      inputHandles[name] = { _tag: 'input', inputName: name, resource: decl };
    } else {
      flowInputs.push({ name, type: decl });
      inputHandles[name] = { _tag: 'input', inputName: name };
    }
  }

  const call = <O extends OutputSpec>(
    nodeId: string,
    op: string,
    args: CallArgs,
    outputs: O,
  ): { readonly [K in keyof O & string]: OutputHandle<O[K]> } => {
    if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
    nodeIds.add(nodeId);

    const bind: Record<string, Ref> = {};
    for (const [key, val] of Object.entries(args.bind)) {
      bind[key] = toRef(val);
    }

    const node: Call = {
      id: nodeId,
      op,
      bind,
      config: (args.config ?? {}) as Record<string, unknown>,
      ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
    };
    nodes.push(node);

    type Result = { readonly [K in keyof O & string]: OutputHandle<O[K]> };
    return new Proxy({} as Result, {
      get: (_target, prop): OutputHandle | undefined => {
        if (typeof prop !== 'string' || prop === 'then') return undefined;
        if (!(prop in outputs)) {
          throw new Error(
            `Op "${op}" has no output port "${prop}". Valid ports: ${Object.keys(
              outputs,
            ).join(', ')}`,
          );
        }
        return { _tag: 'output', nodeId, portName: prop };
      },
    });
  };

  const untypedOp = (
    nodeId: string,
    op: string,
    args: {
      readonly bind: Record<string, Ref>;
      readonly config: Record<string, unknown>;
      readonly guards?: readonly AppliedGuard[];
    },
  ): void => {
    if (nodeIds.has(nodeId)) throw new Error(`Duplicate node id: "${nodeId}"`);
    nodeIds.add(nodeId);
    const node: Call = {
      id: nodeId,
      op,
      bind: args.bind,
      config: args.config,
      ...(args.guards && args.guards.length > 0 && { guards: args.guards }),
    };
    nodes.push(node);
  };

  const build = (): TypedFlow<T> => ({
    version: 1,
    id,
    chainId,
    inputs: [...flowInputs],
    nodes: [...nodes],
  });

  const context: ContextAccessor = {
    sender: ref<'address'>('context.sender'),
    executionAddress: ref<'address'>('context.executionAddress'),
  };

  return {
    context,
    inputs: inputHandles as InputHandles<T>,
    call,
    untypedOp,
    build,
  };
};
