import type {
  ComposeCompilePartialData,
  ComposeCompileRequest,
  ComposeCompileResult,
  ComposeCompileSuccessData,
  ComposeManifest,
} from '@lifi/compose-spec';

import type { GetZapPacksOptions, ZapPackOverview } from './discovery.js';
import { ComposeError, errorFromHttpResponse } from './errors.js';

// __SDK_VERSION__ is a compile-time constant injected by tsup (via `define` in tsup.config.ts)
// and by vitest (via `define` in vitest.config.ts). Both read the version from package.json
// at build/test time and replace this identifier with the literal string value.
// It is sent as the `x-lifi-composer-sdk` request header so the server can identify the caller.
// Falls back to 'dev' when running via tsx without tsup substitution (e.g. the example harness).
declare const __SDK_VERSION__: string;

const SDK_VERSION: string =
  typeof __SDK_VERSION__ !== 'undefined' ? __SDK_VERSION__ : 'dev';

/**
 * Configuration for creating a low-level Compose API client.
 */
export interface ComposeClientOptions {
  /** Base URL of the Compose API. */
  readonly baseUrl: string;
  /** Optional custom `fetch` implementation. Defaults to `globalThis.fetch`. */
  readonly fetch?: typeof globalThis.fetch;
  /** Optional LI.FI API key. When set, sent as the `x-lifi-api-key` header on every request. */
  readonly apiKey?: string;
}

/**
 * Low-level HTTP client for the Compose API.
 *
 * Handles request serialization, SDK version headers, and error mapping.
 * Prefer using {@link ComposeSdk} for the full builder experience. Use this
 * directly when you need to decouple request building from submission — e.g.
 * build via `sdk.request()` then submit via `client.compile()` with custom
 * retry logic or request inspection.
 */
export interface ComposeClient {
  /**
   * Fetches the server's operation manifest describing all supported operations,
   * guards, materialisers, and preconditions.
   * @returns The manifest document.
   * @throws {@link ComposeError} on network, validation, or server errors.
   */
  readonly getManifest: () => Promise<ComposeManifest>;
  /**
   * Submits a compile request and returns the result.
   *
   * When the caller passes `simulationPolicy: 'allow-revert'` and the transaction
   * reverts in simulation, the server responds with HTTP 206 and the SDK returns a
   * partial result (`status: 'partial'`) instead of throwing. The partial result
   * includes the transaction (without `gasLimit`) and revert diagnostics.
   *
   * @param request - The full compile request including flow and run inputs.
   * @returns A discriminated result: `status: 'success'` or `status: 'partial'`.
   * @throws {@link ComposeError} on network, validation, or server errors.
   */
  readonly compile: (
    request: ComposeCompileRequest,
  ) => Promise<ComposeCompileResult>;
  /**
   * Fetches the available routing edges grouped by protocol.
   *
   * The edge catalog is dynamic — it reflects the current state of the
   * backend's routing snapshot (protocols, chains, token blacklists).
   * Results are not cached by the SDK; callers should cache as appropriate.
   *
   * @param options - Optional filter to restrict results to specific protocols.
   * @returns An array of {@link ZapPackOverview} objects, one per protocol.
   * @throws {@link ComposeError} on network or server errors (503 when the
   *   routing catalog is not yet initialized).
   */
  readonly getZapPacks: (
    options?: GetZapPacksOptions,
  ) => Promise<readonly ZapPackOverview[]>;
}

const bigintReplacer = (_key: string, value: unknown): unknown =>
  typeof value === 'bigint' ? value.toString() : value;

const isNonNullObject = (v: unknown): v is Record<string, unknown> =>
  typeof v === 'object' && v !== null;

const parseBody = async <T>(res: Response, url: string): Promise<T> => {
  const body = await res.json().catch((_) => null);
  if (!isNonNullObject(body) || !('data' in body)) {
    throw new ComposeError('UNKNOWN_ERROR', 'Unexpected response format', {
      url,
    });
  }
  return body.data as T;
};

const parseCompileSuccessBody = async (
  res: Response,
  url: string,
): Promise<ComposeCompileResult> => {
  const data = await parseBody<ComposeCompileSuccessData>(res, url);
  return { ...data, status: 'success' as const };
};

const parsePartialBody = async (
  res: Response,
  url: string,
): Promise<ComposeCompileResult> => {
  const body = await res.json().catch((_) => null);
  if (
    !isNonNullObject(body) ||
    !('data' in body) ||
    !isNonNullObject(body.data) ||
    !('error' in body) ||
    !isNonNullObject(body.error)
  ) {
    throw new ComposeError(
      'UNKNOWN_ERROR',
      'Unexpected partial response format',
      { url },
    );
  }
  const data = body.data as unknown as ComposeCompilePartialData;
  const error = body.error as unknown as { kind: string; message: string };
  return { ...data, status: 'partial' as const, error };
};

/**
 * Creates a low-level Compose API client.
 *
 * @param options - Client configuration including the API base URL.
 * @returns A {@link ComposeClient} instance.
 */
export const createComposeClient = (
  options: ComposeClientOptions,
): ComposeClient => {
  if (!options.baseUrl || !/^https?:\/\//i.test(options.baseUrl)) {
    throw new ComposeError(
      'VALIDATION_ERROR',
      `Invalid baseUrl: expected an HTTP(S) URL, got "${options.baseUrl}"`,
    );
  }
  const fetchFn = options.fetch ?? globalThis.fetch;
  const base = options.baseUrl.replace(/\/$/, '');

  const trimmedApiKey = options.apiKey?.trim() || undefined;

  const baseHeaders: Record<string, string> = {
    Accept: 'application/json',
    'x-lifi-composer-sdk': SDK_VERSION,
    ...(trimmedApiKey ? { 'x-lifi-api-key': trimmedApiKey } : {}),
  };

  const getManifest = async (): Promise<ComposeManifest> => {
    const url = `${base}/compose/manifest`;
    let res: Response;
    try {
      res = await fetchFn(url, {
        method: 'GET',
        headers: { ...baseHeaders },
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      throw new ComposeError('NETWORK_ERROR', message, { cause: err });
    }
    if (!res.ok) {
      const body = await res.text();
      throw errorFromHttpResponse(res.status, body, url);
    }
    return await parseBody<ComposeManifest>(res, url);
  };

  const compile = async (
    request: ComposeCompileRequest,
  ): Promise<ComposeCompileResult> => {
    const url = `${base}/compose`;
    let res: Response;
    try {
      res = await fetchFn(url, {
        method: 'POST',
        headers: { ...baseHeaders, 'Content-Type': 'application/json' },
        body: JSON.stringify(request, bigintReplacer),
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      throw new ComposeError('NETWORK_ERROR', message, { cause: err });
    }
    if (res.status === 206) {
      return await parsePartialBody(res, url);
    }
    if (!res.ok) {
      const body = await res.text();
      throw errorFromHttpResponse(res.status, body, url);
    }
    return await parseCompileSuccessBody(res, url);
  };

  const getZapPacks = async (
    options?: GetZapPacksOptions,
  ): Promise<readonly ZapPackOverview[]> => {
    const params = new URLSearchParams();
    if (options?.protocols !== undefined) {
      // Backend expects a single comma-separated value, not repeated keys.
      const raw = options.protocols;
      const list = typeof raw === 'string' ? raw : raw.join(',');
      params.set('protocols', list);
    }
    const qs = params.toString();
    const url = `${base}/compose/zap-packs${qs ? `?${qs}` : ''}`;
    let res: Response;
    try {
      res = await fetchFn(url, {
        method: 'GET',
        headers: { ...baseHeaders },
      });
    } catch (err) {
      const message = err instanceof Error ? err.message : String(err);
      throw new ComposeError('NETWORK_ERROR', message, { cause: err });
    }
    if (!res.ok) {
      const body = await res.text();
      throw errorFromHttpResponse(res.status, body, url);
    }
    return await parseBody<readonly ZapPackOverview[]>(res, url);
  };

  return { getManifest, compile, getZapPacks };
};
