import { Schema } from 'effect';

import { isAsErc20Chain } from './chainCapabilities.js';
import {
  asErc20BalanceGteCheck,
  AssertionOwnerSchema,
  CallCheckSpecSchema,
  Erc20BalanceDeltaGteSpecSchema,
  HexStringSchema,
  NativeBalanceDeltaGteSpecSchema,
} from './continuation.js';
import type { AssertionSpec } from './continuation.js';
import { ChainIdSchema } from './flowSchema.js';
import { BYTES32_LOWER_HEX } from './hexPatterns.js';

// The neutral per-leg verification descriptor (verified continuations, plan
// GD-3, seam C4). It is the wire-visible statement of what a committed
// validation program enforces, so a user funding phase 1 can give informed
// consent (PRD goal G8). It is backend-neutral (G7): no validator address, no
// constraint, no settlement-backend vocabulary — the only escrow-ish term is the
// symbolic owner `"escrow"`, explicitly allowlisted by the boundary check.
//
// The design trick that makes disclosure trustworthy-by-construction:
// `assertedPredicate[]` is NOT a new DSL. It is the canonical JSON serialization
// of the same `AssertionSpec[]` the validation program is derived from, so
// disclosure cannot drift from enforcement — both are projections of one array.

// `ChainIdSchema` (from `flowSchema.ts`) and `HexStringSchema` (from
// `continuation.ts`) are reused directly, so the widened members below differ
// from their readiness counterparts in exactly one field (`owner`) with no
// risk of a drifting re-declaration.

// The `validationProgramHash` (GD-2, seam C1): `keccak256` over the canonical
// program body. Strict 32-byte lowercase hex (`BYTES32_LOWER_HEX`) — see the
// rationale in `hexPatterns.ts`.
export const Bytes32HexSchema = Schema.String.pipe(
  Schema.pattern(BYTES32_LOWER_HEX),
);

// The disclosure-path widening of the two balance-`Gte` readiness members: same
// fields as their `continuation.ts` counterparts, but `owner` accepts a symbolic
// `"escrow"`/`"delivery"` too. The derivation genuinely produces such values —
// a captured invariant can carry a symbolic owner, which `resolveOwner` maps to
// a validator-injected slot at settlement (see the `uc1` invariant). The
// readiness `CheckSchema` stays hex-only on purpose (a polled `Check` has no
// injected slot), so this widening lives in the descriptor union only.
export const AssertedErc20BalanceGteSpecSchema = Schema.Struct({
  kind: Schema.Literal('erc20BalanceGte'),
  chainId: ChainIdSchema,
  token: HexStringSchema,
  owner: AssertionOwnerSchema,
  minAmount: Schema.String,
});

export const AssertedNativeBalanceGteSpecSchema = Schema.Struct({
  kind: Schema.Literal('nativeBalanceGte'),
  chainId: ChainIdSchema,
  owner: AssertionOwnerSchema,
  minAmount: Schema.String,
});

// The disclosed predicate: the five `AssertionSpec` kinds, with symbolic owners
// admitted on the two balance-`Gte` members. The `call` member and the two
// derivation-only delta members are reused verbatim from `continuation.ts` (the
// delta members already carry `AssertionOwnerSchema`).
const RawAssertedPredicateSchema = Schema.Union(
  AssertedErc20BalanceGteSpecSchema,
  AssertedNativeBalanceGteSpecSchema,
  CallCheckSpecSchema,
  Erc20BalanceDeltaGteSpecSchema,
  NativeBalanceDeltaGteSpecSchema,
);

// Same `nativeBalanceGte` → `erc20BalanceGte` rewrite as `CheckSchema` (see
// there), applied to the disclosed/committed predicate so the settlement-side
// re-assertion derives the identical program on `as-erc20` chains and the
// authoring↔settlement hashes stay equal. A committed action authored through
// the fixed `CheckSchema` already carries the erc20 form, so this pass is
// idempotent there; it additionally normalizes a hand-forged committed action
// that embeds a raw native check. The delta members (`nativeBalanceDeltaGte`)
// are intentionally untouched — they derive from already-normalized outcomes.
export const AssertedPredicateSchema = Schema.transform(
  RawAssertedPredicateSchema,
  Schema.typeSchema(RawAssertedPredicateSchema),
  {
    strict: true,
    decode: (pred) =>
      pred.kind === 'nativeBalanceGte' && isAsErc20Chain(pred.chainId)
        ? asErc20BalanceGteCheck(pred.chainId, pred.owner, pred.minAmount)
        : pred,
    encode: (pred) => pred,
  },
);

export const VerificationDescriptorSchema = Schema.Struct({
  validationProgramHash: Bytes32HexSchema,
  // The C5 echo's second hash (verified continuations, plan GD-8): `keccak256`
  // over the canonical params vector (`paramsHash`, GD-2 seam C1). Required and
  // jointly necessary with `validationProgramHash` — the program hash is
  // value-blind (it never binds the delivery address or amounts), so a descriptor
  // that enforces only it is a fund-redirect hole. Empty params still yield a
  // present `0x00…00`, so a real descriptor never omits it. Strict 32-byte
  // lowercase hex, same rule as `validationProgramHash`.
  paramsHash: Bytes32HexSchema,
  assertedPredicate: Schema.Array(AssertedPredicateSchema),
});

// Deterministic, key-order-pinned JSON serialization of an `AssertionSpec[]`.
// Canonicalization rules:
// - Object keys are emitted in recursive lexicographic order (sorted).
// - Compact separators — no whitespace anywhere.
// - Array element order is PRESERVED, never sorted: it is semantic (derivation
//   order — deltas, then invariants) and is a faithful projection of the
//   program's assertion order.
//
// All `AssertionSpec` leaf values are JSON-safe (decimal strings, `0x` hex
// strings, positive-int `chainId`), so lexicographic key sorting is total and
// stable with no number-canonicalization edge cases, and is self-maintaining
// when members gain fields. Embedding this string per fixture vector pins the
// serialization cross-repo (GD-5's consumer asserts it reproduces the string).
//
// Boundary-forced local mirror of `@lifi/utils`'s `canonicalJson`: compose-spec
// is the wire-format leaf and may not import `@lifi/utils` (enforced by the
// compose boundary check), so do not "fix" this duplication by importing across
// the boundary.
const canonicalJson = (value: unknown): string => {
  if (Array.isArray(value)) {
    return `[${value.map(canonicalJson).join(',')}]`;
  }
  if (value !== null && typeof value === 'object') {
    const record = value as Record<string, unknown>;
    const entries = Object.keys(record)
      .sort()
      .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`);
    return `{${entries.join(',')}}`;
  }
  return JSON.stringify(value);
};

export const canonicalAssertedPredicate = (
  specs: readonly AssertionSpec[],
): string => canonicalJson(specs);

export type VerificationDescriptor = typeof VerificationDescriptorSchema.Type;

// `AssertedPredicate` is structurally identical to `AssertionSpec` at the TS
// type level — `AssertionOwner` is a subtype of `string`, so widening `owner`
// changes only schema *decode* behaviour, not the static type. It is therefore
// exported as a plain alias, keeping `canonicalAssertedPredicate` and the
// derivation's `specs` array interchangeable without casts.
export type AssertedPredicate = AssertionSpec;
