import type { SimulateResult, SimulationRevert } from '@lifi/compose-spec';
import z from 'zod';

/**
 * Zod-backed parsers for Compose API response bodies.
 *
 * Server responses are untrusted input crossing into the SDK, so the shapes the
 * client reads back are validated here rather than asserted with `as` casts. The
 * schemas are module-private (the package builds with `--isolatedDeclarations`,
 * which forbids exporting un-annotated complex values); the public surface is
 * the typed `parse*` helpers, whose explicit return types double as the contract.
 *
 * The simulate union is the one response the SDK fully owns and parses itself
 * (it is un-enveloped, unlike `/compose`), so it gets a complete schema kept in
 * lockstep with the canonical `SimulateResult` in `@lifi/compose-spec` by the
 * compile-time assertion below — the same anchoring `@lifi/api-schemas` uses for
 * its server-side simulate schema, so the two cannot silently drift.
 */

const balanceEntrySchema = z.object({
  token: z.string(),
  owner: z.string(),
  amount: z.string(),
});

const simulateOkSchema = z.object({
  status: z.literal('ok'),
  block: z.number(),
  timestamp: z.number(),
  balancesBefore: z.array(balanceEntrySchema),
  balancesAfter: z.array(balanceEntrySchema),
  deltas: z.array(balanceEntrySchema),
  gasUsed: z.string(),
});

const simulateRevertDecodeResultSchema = z.object({
  errorCandidates: z
    .array(
      z.object({
        decodedErrorSignature: z.string(),
        decodedParams: z.array(z.string()),
      }),
    )
    .optional(),
  error: z.string().optional(),
});

const simulateRevertSchema = z.object({
  status: z.literal('revert'),
  block: z.number(),
  timestamp: z.number(),
  revertReason: z.string().optional(),
  code: z.number().optional(),
  rawErrorBytes: z.string().optional(),
  decodeResult: simulateRevertDecodeResultSchema.optional(),
});

const simulateSetupErrorSchema = z.object({
  status: z.literal('error'),
  message: z.string(),
});

const simulateResultSchema = z.discriminatedUnion('status', [
  simulateOkSchema,
  simulateRevertSchema,
  simulateSetupErrorSchema,
]);

// The type the schema actually parses out. Kept private (referencing it from an
// exported declaration would break `--isolatedDeclarations`) and pinned to the
// canonical `SimulateResult` inside `parseSimulateResult` below.
type SimulateResultWire = z.infer<typeof simulateResultSchema>;

/**
 * Parses a `POST /simulate` body into a {@link SimulateResult}. Returns `null`
 * when the body does not match the `ok`/`revert`/`error` union, letting the
 * caller raise a transport-level error.
 */
export const parseSimulateResult = (body: unknown): SimulateResult | null => {
  const parsed = simulateResultSchema.safeParse(body);
  if (!parsed.success) return null;
  // Pin the schema to the canonical contract in both directions, so it cannot
  // silently drift (mirroring the conformance assertion `@lifi/api-schemas` runs
  // on its server-side simulate schema). Assigning the inferred wire value to a
  // `SimulateResult` checks wire→spec; `satisfies SimulateResultWire` checks
  // spec→wire. A missing or mistyped field on either side fails to compile.
  const result: SimulateResult = parsed.data;
  return result satisfies SimulateResultWire;
};

/**
 * The validated members of an HTTP 206 partial compile envelope. `data` is
 * verified to be an object but kept untyped — compose-spec owns its full shape
 * (`ComposeCompilePartialData`) as a hand-authored type, so the caller narrows
 * it. `error` is fully validated.
 */
export interface CompilePartialEnvelope {
  readonly data: Record<string, unknown>;
  readonly error: { readonly kind: string; readonly message: string };
}

const compilePartialEnvelopeSchema = z.object({
  data: z.record(z.string(), z.unknown()),
  error: z.object({ kind: z.string(), message: z.string() }),
});

/** Parses an HTTP 206 partial compile envelope; `null` when it does not match. */
export const parseCompilePartialEnvelope = (
  body: unknown,
): CompilePartialEnvelope | null => {
  const parsed = compilePartialEnvelopeSchema.safeParse(body);
  return parsed.success ? parsed.data : null;
};

/** Error envelope returned on non-2xx responses. */
export interface ServerErrorBody {
  readonly error?: {
    readonly kind?: string;
    readonly message?: string;
    readonly path?: string;
    readonly details?: SimulationRevert;
  };
}

// `details` is passed through (`z.custom`) rather than parsed: the SDK does not
// own the `SimulationRevert` schema and forwards the field verbatim onto
// `ComposeError`.
const serverErrorBodySchema = z.object({
  error: z
    .object({
      kind: z.string().optional(),
      message: z.string().optional(),
      path: z.string().optional(),
      details: z.custom<SimulationRevert>().optional(),
    })
    .optional(),
});

/** Validates an already-parsed error envelope; `null` when it does not match. */
export const parseServerErrorBody = (json: unknown): ServerErrorBody | null => {
  const parsed = serverErrorBodySchema.safeParse(json);
  return parsed.success ? parsed.data : null;
};
