import { blake2b } from "@noble/hashes/blake2";
import { Effect as Eff, Equal, FastCheck, Hash, Inspectable, ParseResult, Schema } from "effect";
import * as CBOR from "./CBOR.js";
import * as CostModel from "./CostModel.js";
import * as Data from "./Data.js";
import * as Redeemer from "./Redeemer.js";
import * as ScriptDataHash from "./ScriptDataHash.js";
// ============================================================================
// Shared helpers
// ============================================================================
const arrayEquals = (a, b) => {
  if (a.length !== b.length) return false;
  for (let i = 0; i < a.length; i++) {
    if (!Equal.equals(a[i], b[i])) return false;
  }
  return true;
};
const arrayHash = arr => {
  let hash = 0;
  for (const item of arr) {
    hash = Hash.combine(hash)(Hash.hash(item));
  }
  return hash;
};
/**
 * Create a string key from a RedeemerKey for lookup convenience.
 *
 * @since 2.0.0
 * @category utilities
 */
export const keyToString = ([tag, index]) => `${tag}:${index}`;
// ============================================================================
// Map entry value type
// ============================================================================
/**
 * A redeemer map entry value: `[data, ex_units]`.
 *
 * Mirrors the CDDL: `[data : plutus_data, ex_units : ex_units]`
 *
 * @since 2.0.0
 * @category model
 */
export class RedeemerValue extends /*#__PURE__*/Schema.Class("RedeemerValue")({
  data: /*#__PURE__*/Schema.typeSchema(Data.DataSchema),
  exUnits: Redeemer.ExUnits
}) {
  [Equal.symbol](that) {
    return that instanceof RedeemerValue && Data.equals(this.data, that.data) && Equal.equals(this.exUnits, that.exUnits);
  }
  [Hash.symbol]() {
    return Hash.cached(this, Hash.combine(Hash.hash(this.data))(Hash.hash(this.exUnits)));
  }
}
// ============================================================================
// Domain types — discriminated union (Credential pattern)
// ============================================================================
/**
 * Redeemers in map format (Conway recommended).
 *
 * Mirrors the CDDL exactly:
 * ```
 * { + [tag : redeemer_tag, index : uint .size 4] => [ data : plutus_data, ex_units : ex_units ] }
 * ```
 *
 * The map is keyed by `[tag, index]` tuples. Note: JS Map uses reference
 * equality for non-primitive keys, so lookups by tuple won't work — use
 * `get()` or `toArray()` helpers instead.
 *
 * @since 2.0.0
 * @category model
 */
export class RedeemerMap extends /*#__PURE__*/Schema.TaggedClass()("RedeemerMap", {
  value: /*#__PURE__*/Schema.Map({
    key: /*#__PURE__*/Schema.Tuple(Redeemer.RedeemerTag, Schema.BigIntFromSelf),
    value: /*#__PURE__*/Schema.typeSchema(RedeemerValue)
  })
}) {
  /**
   * Look up a redeemer entry by tag and index.
   *
   * @since 2.0.0
   * @category accessors
   */
  get(tag, index) {
    for (const [[t, i], v] of this.value) {
      if (t === tag && i === index) return v;
    }
    return undefined;
  }
  /**
   * Number of redeemer entries.
   *
   * @since 2.0.0
   * @category accessors
   */
  get size() {
    return this.value.size;
  }
  /**
   * Convert to an array of `Redeemer` objects (convenience for consumers).
   *
   * @since 2.0.0
   * @category conversions
   */
  toArray() {
    const result = [];
    for (const [[tag, index], {
      data,
      exUnits
    }] of this.value) {
      result.push(new Redeemer.Redeemer({
        tag,
        index,
        data,
        exUnits
      }));
    }
    return result;
  }
  toJSON() {
    return {
      _tag: "RedeemerMap",
      entries: Array.from(this.value.entries()).map(([[tag, index], {
        data,
        exUnits
      }]) => ({
        key: {
          tag,
          index: index.toString()
        },
        value: {
          data,
          exUnits: exUnits.toJSON()
        }
      }))
    };
  }
  toString() {
    return Inspectable.format(this.toJSON());
  }
  [Inspectable.NodeInspectSymbol]() {
    return this.toJSON();
  }
  [Equal.symbol](that) {
    if (!(that instanceof RedeemerMap)) return false;
    if (this.value.size !== that.value.size) return false;
    // Order-insensitive: sort both by [tag, index] then compare Redeemer objects
    // (Redeemer is a TaggedClass with proper Equal support, unlike raw Data.Data)
    const sortKey = r => `${r.tag}:${r.index}`;
    const sortedThis = [...this.toArray()].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
    const sortedThat = [...that.toArray()].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
    return arrayEquals(sortedThis, sortedThat);
  }
  [Hash.symbol]() {
    // Order-insensitive: sort by key then hash the sorted array
    const sortKey = r => `${r.tag}:${r.index}`;
    const sorted = [...this.toArray()].sort((a, b) => sortKey(a).localeCompare(sortKey(b)));
    return Hash.cached(this, arrayHash(sorted));
  }
}
/**
 * Create a `RedeemerMap` from an array of `Redeemer` objects.
 *
 * @since 2.0.0
 * @category constructors
 */
export const makeRedeemerMap = redeemers => {
  const map = new Map();
  for (const r of redeemers) {
    const key = [r.tag, r.index];
    // Detect semantic duplicates (same tag + index)
    for (const [existingKey] of map) {
      if (existingKey[0] === key[0] && existingKey[1] === key[1]) {
        throw new Error(`Duplicate redeemer key: [${key[0]}, ${key[1]}]`);
      }
    }
    map.set(key, new RedeemerValue({
      data: r.data,
      exUnits: r.exUnits
    }));
  }
  return new RedeemerMap({
    value: map
  });
};
/**
 * Redeemers in legacy array format.
 *
 * Mirrors the CDDL:
 * ```
 * [ + redeemer ]
 * ```
 *
 * Backwards compatible — will be deprecated in the next era.
 * Prefer `RedeemerMap` for new transactions.
 *
 * @since 2.0.0
 * @category model
 */
export class RedeemerArray extends /*#__PURE__*/Schema.TaggedClass()("RedeemerArray", {
  value: /*#__PURE__*/Schema.Array(Redeemer.Redeemer)
}) {
  /**
   * Number of redeemer entries.
   *
   * @since 2.0.0
   * @category accessors
   */
  get size() {
    return this.value.length;
  }
  /**
   * Convert to an array of `Redeemer` objects (identity for array format).
   *
   * @since 2.0.0
   * @category conversions
   */
  toArray() {
    return this.value;
  }
  toJSON() {
    return {
      _tag: "RedeemerArray",
      value: this.value.map(r => r.toJSON())
    };
  }
  toString() {
    return Inspectable.format(this.toJSON());
  }
  [Inspectable.NodeInspectSymbol]() {
    return this.toJSON();
  }
  [Equal.symbol](that) {
    return that instanceof RedeemerArray && arrayEquals(this.value, that.value);
  }
  [Hash.symbol]() {
    return Hash.cached(this, arrayHash(this.value));
  }
}
/**
 * Union schema for redeemers — accepts either map or array format.
 * Follows the Credential pattern: `Credential = Union(KeyHash, ScriptHash)`.
 *
 * @since 2.0.0
 * @category schemas
 */
export const Redeemers = /*#__PURE__*/Schema.Union(RedeemerMap, RedeemerArray);
// ============================================================================
// CDDL schemas — one per wire format
// ============================================================================
/**
 * CDDL schema for array format: `[ + redeemer ]`
 *
 * @since 2.0.0
 * @category schemas
 */
export const ArrayCDDLSchema = /*#__PURE__*/Schema.Array(Redeemer.CDDLSchema);
/**
 * CDDL transformation for array format → `RedeemerArray`.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromArrayCDDL = /*#__PURE__*/Schema.transformOrFail(ArrayCDDLSchema, /*#__PURE__*/Schema.typeSchema(RedeemerArray), {
  strict: true,
  encode: toA => Eff.all(toA.value.map(r => ParseResult.encode(Redeemer.FromCDDL)(r))),
  decode: fromA => Eff.gen(function* () {
    const value = yield* Eff.all(fromA.map(tuple => ParseResult.decode(Redeemer.FromCDDL)(tuple)));
    return new RedeemerArray({
      value
    });
  })
});
/**
 * Map key schema: `[tag, index]`
 *
 * @since 2.0.0
 * @category schemas
 */
const MapKeyCDDLSchema = /*#__PURE__*/Schema.Tuple(CBOR.Integer, CBOR.Integer);
/**
 * Map value schema: `[data, ex_units]`
 *
 * @since 2.0.0
 * @category schemas
 */
const MapValueCDDLSchema = /*#__PURE__*/Schema.Tuple(Data.CDDLSchema, /*#__PURE__*/Schema.Tuple(CBOR.Integer, CBOR.Integer));
/**
 * CDDL schema for map format: `{ + [tag, index] => [data, ex_units] }`
 *
 * Uses `MapFromSelf` (not `Map`) so the Encoded type is a JS Map — matching
 * how `CBOR.FromBytes` represents CBOR major-type-5 maps at runtime.
 * This is the same pattern used by Withdrawals, Mint, MultiAsset, CostModel.
 *
 * @since 2.0.0
 * @category schemas
 */
export const MapCDDLSchema = /*#__PURE__*/Schema.MapFromSelf({
  key: MapKeyCDDLSchema,
  value: MapValueCDDLSchema
});
/**
 * CDDL transformation for map format → `RedeemerMap`.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromMapCDDL = /*#__PURE__*/Schema.transformOrFail(MapCDDLSchema, /*#__PURE__*/Schema.typeSchema(RedeemerMap), {
  strict: true,
  encode: toA => Eff.gen(function* () {
    const entries = [];
    for (const [[tag, index], {
      data,
      exUnits
    }] of toA.value) {
      const tagInteger = Redeemer.tagToInteger(tag);
      const dataCBOR = yield* ParseResult.encode(Data.FromCDDL)(data);
      entries.push([[tagInteger, index], [dataCBOR, [exUnits.mem, exUnits.steps]]]);
    }
    return new Map(entries);
  }),
  decode: fromA => Eff.gen(function* () {
    const entries = [];
    for (const [[tagInteger, index], [dataCBOR, [mem, steps]]] of fromA.entries()) {
      const tag = Redeemer.integerToTag(tagInteger);
      const data = yield* ParseResult.decode(Data.FromCDDL)(dataCBOR);
      entries.push([[tag, index], new RedeemerValue({
        data,
        exUnits: new Redeemer.ExUnits({
          mem,
          steps
        })
      })]);
    }
    return new RedeemerMap({
      value: new Map(entries)
    });
  })
});
/**
 * Default CDDL schema (map format — Conway recommended).
 *
 * @since 2.0.0
 * @category schemas
 */
export const CDDLSchema = MapCDDLSchema;
/**
 * Default CDDL transformation (map format).
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromCDDL = FromMapCDDL;
// ============================================================================
// CBOR bytes / hex schemas
// ============================================================================
/**
 * CBOR bytes schema for array format.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromCBORBytes = (options = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose(CBOR.FromBytes(options), FromArrayCDDL).annotations({
  identifier: "Redeemers.FromCBORBytes",
  title: "Redeemers from CBOR Bytes (Array)",
  description: "Transforms CBOR bytes to RedeemerArray"
});
/**
 * CBOR hex schema for array format.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromCBORHex = (options = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose(Schema.Uint8ArrayFromHex, FromCBORBytes(options)).annotations({
  identifier: "Redeemers.FromCBORHex",
  title: "Redeemers from CBOR Hex (Array)",
  description: "Transforms CBOR hex string to RedeemerArray"
});
/**
 * CBOR bytes schema for map format.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromCBORBytesMap = (options = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose(CBOR.FromBytes(options), FromMapCDDL).annotations({
  identifier: "Redeemers.FromCBORBytesMap",
  title: "Redeemers from CBOR Bytes (Map)",
  description: "Transforms CBOR bytes to RedeemerMap"
});
/**
 * CBOR hex schema for map format.
 *
 * @since 2.0.0
 * @category schemas
 */
export const FromCBORHexMap = (options = CBOR.CML_DEFAULT_OPTIONS) => Schema.compose(Schema.Uint8ArrayFromHex, FromCBORBytesMap(options)).annotations({
  identifier: "Redeemers.FromCBORHexMap",
  title: "Redeemers from CBOR Hex (Map)",
  description: "Transforms CBOR hex string to RedeemerMap"
});
// ============================================================================
// Arbitrary
// ============================================================================
/**
 * FastCheck arbitrary for Redeemers — generates both map and array variants.
 *
 * @since 2.0.0
 * @category arbitrary
 */
export const arbitrary = /*#__PURE__*/FastCheck.array(Redeemer.arbitrary, {
  maxLength: 5
}).chain(redeemers => FastCheck.constantFrom(makeRedeemerMap(redeemers), new RedeemerArray({
  value: redeemers
})));
// ============================================================================
// Convenience parse / encode functions
// ============================================================================
/**
 * Parse from CBOR bytes (array format).
 *
 * @since 2.0.0
 * @category parsing
 */
export const fromCBORBytes = (bytes, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.decodeSync(FromCBORBytes(options))(bytes);
/**
 * Parse from CBOR hex string (array format).
 *
 * @since 2.0.0
 * @category parsing
 */
export const fromCBORHex = (hex, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.decodeSync(FromCBORHex(options))(hex);
/**
 * Parse from CBOR bytes (map format).
 *
 * @since 2.0.0
 * @category parsing
 */
export const fromCBORBytesMap = (bytes, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.decodeSync(FromCBORBytesMap(options))(bytes);
/**
 * Parse from CBOR hex string (map format).
 *
 * @since 2.0.0
 * @category parsing
 */
export const fromCBORHexMap = (hex, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.decodeSync(FromCBORHexMap(options))(hex);
/**
 * Encode to CBOR bytes (array format).
 *
 * @since 2.0.0
 * @category encoding
 */
export const toCBORBytes = (data, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.encodeSync(FromCBORBytes(options))(data);
/**
 * Encode to CBOR hex string (array format).
 *
 * @since 2.0.0
 * @category encoding
 */
export const toCBORHex = (data, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.encodeSync(FromCBORHex(options))(data);
/**
 * Encode to CBOR bytes (map format).
 *
 * @since 2.0.0
 * @category encoding
 */
export const toCBORBytesMap = (data, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.encodeSync(FromCBORBytesMap(options))(data);
/**
 * Encode to CBOR hex string (map format).
 *
 * @since 2.0.0
 * @category encoding
 */
export const toCBORHexMap = (data, options = CBOR.CML_DEFAULT_OPTIONS) => Schema.encodeSync(FromCBORHexMap(options))(data);
// ============================================================================
// Hashing
// ============================================================================
/**
 * Encode an array of datums as tag(258) set.
 * Each datum is encoded individually, then wrapped in a definite-length array with tag 258.
 */
const encodeDatumsTaggedSet = (datums, options = CBOR.CML_DATA_DEFAULT_OPTIONS) => {
  const items = datums.map(d => Data.toCBORBytes(d, options));
  const arr = CBOR.encodeArrayAsDefinite(items);
  return CBOR.encodeTaggedValue(258, arr);
};
/**
 * Concatenate multiple Uint8Arrays into one.
 */
const concatBytes = (...arrays) => {
  const totalLen = arrays.reduce((sum, arr) => sum + arr.length, 0);
  const result = new Uint8Array(totalLen);
  let offset = 0;
  for (const arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
  }
  return result;
};
/**
 * Compute script_data_hash using standard module encoders.
 *
 * Accepts the concrete `Redeemers` union type — encoding format is determined
 * by `_tag` (`RedeemerMap` → map CBOR, `RedeemerArray` → array CBOR).
 *
 * The payload format per CDDL spec is raw concatenation (not a CBOR structure):
 * ```
 * redeemers_bytes || datums_bytes || language_views_bytes
 * ```
 *
 * @since 2.0.0
 * @category hashing
 */
export const toScriptDataHash = (redeemers, costModels, datums, options = CBOR.CML_DEFAULT_OPTIONS) => {
  const hasDatums = Array.isArray(datums) && datums.length > 0;
  // Language views encoding (handles PlutusV1 indefinite-length quirk per spec)
  const langViewsBytes = CostModel.languageViewsEncoding(costModels);
  let payload;
  if (hasDatums && redeemers.size === 0) {
    // Special case (CDDL): [ A0 | tag(258) datums | A0 ]
    const datumsBytes = encodeDatumsTaggedSet(datums);
    payload = concatBytes(new Uint8Array([0xa0]),
    // Empty map
    datumsBytes, new Uint8Array([0xa0]) // Empty map
    );
  } else {
    // Encode redeemers based on concrete type
    const redeemersBytes = redeemers._tag === "RedeemerMap" ? toCBORBytesMap(redeemers, options) : toCBORBytes(redeemers, options);
    const datumsBytes = hasDatums ? encodeDatumsTaggedSet(datums) : undefined;
    payload = datumsBytes ? concatBytes(redeemersBytes, datumsBytes, langViewsBytes) : concatBytes(redeemersBytes, langViewsBytes);
  }
  const digest = blake2b(payload, {
    dkLen: 32
  });
  return new ScriptDataHash.ScriptDataHash({
    hash: digest
  });
};
//# sourceMappingURL=Redeemers.js.map