import z from 'zod';

import { BYTES32_LOWER_HEX } from './hexPatterns.js';

// A positive integer chain id. Zod mirror of the Effect `ChainIdSchema` in
// `flowSchema.ts`, single-sourced so the wire schemas stay consistent.
export const chainIdZod = z.number().int().positive();

const SolTypeZod = z.enum([
  'uint8',
  'uint16',
  'uint32',
  'uint64',
  'uint128',
  'uint256',
  'int128',
  'int256',
  'address',
  'bool',
  'bytes',
  'bytes4',
  'bytes32',
  'string',
]);

export const ResourcePortZod = z.object({
  kind: z.literal('resource'),
  name: z.string(),
  accepts: z.enum(['erc20', 'native', 'any']),
  mode: z.enum(['linear', 'copy']),
  optional: z.boolean().optional(),
});

export const HANDLE_UNITS = [
  'raw',
  'wad',
  'ray',
  'bps',
  'token-decimals',
] as const;

// NOTE: the uint members below must stay in sync with the `UintN` union in
// `ts/packages/lifi_ir1_ts/src/types/ValueHandle.ts`. When adding a new
// uintN width, update both locations.
export const STATIC_SOL_TYPES = [
  'uint256',
  'uint128',
  'uint64',
  'uint32',
  'uint16',
  'uint8',
  'address',
  'bool',
  'bytes32',
] as const;

export const HandleUnitsZod = z.enum(HANDLE_UNITS);

export const StaticSolTypeZod = z.enum(STATIC_SOL_TYPES);

const STATIC_SOL_TYPE_SET: ReadonlySet<string> = new Set(STATIC_SOL_TYPES);

export const isStaticSolType = (value: string): value is StaticSolType =>
  STATIC_SOL_TYPE_SET.has(value);

export const HandlePortZod = z.object({
  kind: z.literal('handle'),
  name: z.string(),
  type: SolTypeZod,
  mode: z.enum(['linear', 'copy']),
  expose: z.boolean().optional(),
  units: HandleUnitsZod.optional(),
  optional: z.boolean().optional(),
});

export const InputPortZod = z.discriminatedUnion('kind', [
  ResourcePortZod,
  HandlePortZod,
]);

export const ResourceOutputPortZod = z.object({
  kind: z.literal('resource_output'),
  name: z.string(),
  mode: z.enum(['linear', 'copy']),
  availability: z.enum(['now', 'future']).optional(),
  providesMinimum: z.boolean().optional(),
  omitIfZero: z.boolean().optional(),
  deliveryAddressInput: z.string().optional(),
});

export const OutputPortZod = z.discriminatedUnion('kind', [
  ResourceOutputPortZod,
  HandlePortZod,
]);

export const ManifestOperationZod = z.object({
  id: z.string(),
  description: z.string().optional(),
  inputs: z.array(InputPortZod),
  outputs: z.array(OutputPortZod),
  configSchema: z.unknown().optional(),
});

const PortKindEnum = z.enum(['resource', 'resource_output', 'handle']);

const SelectorMatchZod = z.object({
  kind: z.union([PortKindEnum, z.array(PortKindEnum)]),
  mode: z.enum(['linear', 'copy']).optional(),
  type: z.enum(['erc20', 'native', 'any']).optional(),
});

const ConfigSelectionZod = z.object({
  kind: z.literal('config'),
  configKey: z.string(),
  cardinality: z.enum(['one', 'many']),
});

const AllMatchingSelectionZod = z.object({
  kind: z.literal('all_matching'),
});

const SelectorSelectionZod = z.discriminatedUnion('kind', [
  ConfigSelectionZod,
  AllMatchingSelectionZod,
]);

export const GuardSelectorZod = z.object({
  binding: z.string(),
  source: z.enum(['inputs', 'outputs']),
  match: SelectorMatchZod,
  selection: SelectorSelectionZod,
});

export const GuardCompatibilityZod = z.object({
  selectors: z.array(GuardSelectorZod),
});

export const ManifestGuardZod = z.object({
  kind: z.string(),
  description: z.string().optional(),
  configSchema: z.unknown().optional(),
  compatibility: GuardCompatibilityZod.optional(),
});

export const ManifestMaterialiserZod = z.object({
  kind: z.string(),
  description: z.string().optional(),
  accepts: z.enum(['resource', 'handle', 'any']),
  configSchema: z.unknown().optional(),
});

export const ManifestPreconditionZod = z.object({
  type: z.string(),
  description: z.string().optional(),
  configSchema: z.unknown().optional(),
});

export const ComposeManifestZod = z.object({
  manifestVersion: z.number(),
  manifestHash: z.string(),
  flowSchema: z.object({}).passthrough(),
  operations: z.array(ManifestOperationZod),
  guards: z.array(ManifestGuardZod),
  materialisers: z.array(ManifestMaterialiserZod),
  preconditions: z.array(ManifestPreconditionZod).optional(),
});

// HTTP-layer mirror of `Phase1ArtifactSchema` (the Effect schema in
// `continuation.ts`). The Effect schema validates the Flow; this Zod schema
// validates the `/compose` response. `transaction` carries an optional
// `transactionRequest` only if a venue later needs it; for v1 it is just
// `{ kind: "transaction" }` and the client funds via the existing
// response-level `transactionRequest`.
export const Phase1ArtifactZod = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('transaction'),
    transactionRequest: z
      .object({
        to: z.string(),
        data: z.string(),
        value: z.string(),
      })
      .optional(),
  }),
  z.object({
    kind: z.literal('signables'),
    typedData: z.array(z.unknown()),
  }),
  z.object({
    kind: z.literal('none'),
  }),
]);

// HTTP-layer mirror of `CheckSchema` (the Effect schema in `continuation.ts`).
// A readiness predicate the client can poll and that is also embedded into
// `nextFlow` as an on-chain assertion. Amounts are decimal strings on the wire.
// Address/bytes fields require a `0x` prefix (lenient, mirroring `CheckSchema`).
const hexStringZod = z.string().startsWith('0x');

// Mirror of `AssertionOwnerSchema` (continuation.ts): a concrete hex address or
// one of the two symbolic owners resolved at settlement. Applies ONLY to the
// derivation-only delta members — the wire `Check` members keep hex-only
// owners (see the note in continuation.ts).
const assertionOwnerZod = z.union([
  hexStringZod,
  z.enum(['escrow', 'delivery']),
]);

export const CheckZod = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('erc20BalanceGte'),
    chainId: chainIdZod,
    token: hexStringZod,
    owner: hexStringZod,
    minAmount: z.string(),
  }),
  z.object({
    kind: z.literal('nativeBalanceGte'),
    chainId: chainIdZod,
    owner: hexStringZod,
    minAmount: z.string(),
  }),
  z.object({
    kind: z.literal('call'),
    chainId: chainIdZod,
    to: hexStringZod,
    calldata: hexStringZod,
    comparator: z.enum(['gte', 'eq', 'lte']),
    value: z.string(),
  }),
]);

// HTTP-layer mirror of `AssertionSpecSchema` (continuation.ts): the three
// `Check` members plus the two derivation-only members. Reuses `CheckZod`'s
// options so the shared members cannot drift from `Check`.
export const AssertionSpecZod = z.discriminatedUnion('kind', [
  ...CheckZod.options,
  z.object({
    kind: z.literal('erc20BalanceDeltaGte'),
    chainId: z.number().int().positive(),
    token: hexStringZod,
    owner: assertionOwnerZod,
    minDelta: z.string(),
  }),
  z.object({
    kind: z.literal('nativeBalanceDeltaGte'),
    chainId: z.number().int().positive(),
    owner: assertionOwnerZod,
    minDelta: z.string(),
  }),
]);

export type AssertionSpecWire = z.infer<typeof AssertionSpecZod>;

// HTTP-layer mirror of the GD-3 descriptor union `AssertedPredicateSchema`
// (descriptor.ts, seam C4). It is the disclosure-path widening of
// `AssertionSpecZod`: the two balance-`Gte` members accept a symbolic owner
// (`assertionOwnerZod`) as well as hex, because the derivation resolves symbolic
// owners to validator-injected slots at settlement. The wire `Check` members
// stay hex-only (`CheckZod`); this widening is confined to the descriptor. The
// `call` member and the two derivation-only delta members are reused verbatim by
// selecting them from `AssertionSpecZod.options` by their `kind` literal, so
// they cannot drift from the source union.
const assertionSpecOptionByKind = (kind: string) => {
  const option = AssertionSpecZod.options.find(
    (o) => o.shape.kind.value === kind,
  );
  if (option === undefined) {
    throw new Error(`AssertionSpecZod has no member with kind "${kind}"`);
  }
  return option;
};

export const AssertedPredicateZod = z.discriminatedUnion('kind', [
  z.object({
    kind: z.literal('erc20BalanceGte'),
    chainId: z.number().int().positive(),
    token: hexStringZod,
    owner: assertionOwnerZod,
    minAmount: z.string(),
  }),
  z.object({
    kind: z.literal('nativeBalanceGte'),
    chainId: z.number().int().positive(),
    owner: assertionOwnerZod,
    minAmount: z.string(),
  }),
  assertionSpecOptionByKind('call'),
  assertionSpecOptionByKind('erc20BalanceDeltaGte'),
  assertionSpecOptionByKind('nativeBalanceDeltaGte'),
]);

export const VerificationDescriptorZod = z.object({
  validationProgramHash: z.string().regex(BYTES32_LOWER_HEX),
  // The C5 echo's second hash (GD-8): the params-vector content hash
  // (`paramsHash`, C1). Required and jointly necessary with the value-blind
  // `validationProgramHash`.
  paramsHash: z.string().regex(BYTES32_LOWER_HEX),
  assertedPredicate: z.array(AssertedPredicateZod),
});

export type AssertedPredicateWire = z.infer<typeof AssertedPredicateZod>;
export type VerificationDescriptorWire = z.infer<
  typeof VerificationDescriptorZod
>;

// HTTP-layer mirror of `ContinuationPlanSchema`. `nextFlow` is typed
// `z.unknown()` rather than mirroring the full Flow schema in Zod: the client
// treats it as an opaque, re-submittable document and POSTs it back to
// `/compose`, where the Effect `FlowSchema` validates it. The internal
// `ContinuationPlan` type keeps `nextFlow: Flow` precisely typed.
export const ContinuationPlanZod = z.object({
  chainId: chainIdZod,
  nextFlow: z.unknown(),
  check: CheckZod,
  validity: z.object({ expiresAtMs: z.number() }),
  correlationId: z.string().optional(),
});

export type ResourcePort = z.infer<typeof ResourcePortZod>;
export type HandlePort = z.infer<typeof HandlePortZod>;
export type HandleUnits = z.infer<typeof HandleUnitsZod>;
export type StaticSolType = z.infer<typeof StaticSolTypeZod>;
export type OpInputPort = z.infer<typeof InputPortZod>;
export type ResourceOutputPort = z.infer<typeof ResourceOutputPortZod>;
export type OpOutputPort = z.infer<typeof OutputPortZod>;
export type ManifestOperation = z.infer<typeof ManifestOperationZod>;
export type GuardSelector = z.infer<typeof GuardSelectorZod>;
export type GuardCompatibility = z.infer<typeof GuardCompatibilityZod>;
export type ManifestGuard = z.infer<typeof ManifestGuardZod>;
export type ManifestMaterialiser = z.infer<typeof ManifestMaterialiserZod>;
export type ManifestPrecondition = z.infer<typeof ManifestPreconditionZod>;
export type ComposeManifest = z.infer<typeof ComposeManifestZod>;
export type Phase1ArtifactWire = z.infer<typeof Phase1ArtifactZod>;
export type ContinuationPlanWire = z.infer<typeof ContinuationPlanZod>;
