import type { Address, Hex } from "viem"
import type { decUnits } from "./values"

/**
 * The protocol's attribute type vocabulary, mapping each short **tag** (the name used by the
 * client, the query language and this SDK) to its **typeId** (the byte carried on the wire and in
 * the entity record).
 *
 * The typeIds are consensus-critical: they are written into entity records and mixed into index
 * keys, so this table must never drift from the node's `AttributeType`.
 *
 * | tag     | typeId | ABI wire type        | when indexed              |
 * | ------- | ------ | -------------------- | ------------------------- |
 * | `bool`  | 1      | `bool`               | equality + range          |
 * | `i32`   | 2      | `int32`              | equality + range          |
 * | `u64`   | 3      | `uint64`             | equality + range          |
 * | `u256`  | 4      | `uint256`            | equality + range          |
 * | `dec`   | 5      | `int256` (18 dp)     | equality + range          |
 * | —       | 6      | `bytes32`            | reserved                  |
 * | `bytes` | 7      | `bytes` (<= 128 KiB) | never indexed             |
 * | `str`   | 8      | `string` (<= 128 B)  | equality + range + prefix |
 * | `addr`  | 9      | `address`            | equality + range          |
 * | `key`   | 10     | `bytes32`            | equality + range          |
 *
 * typeId 6 is the node's `bytes32` type, which this SDK does not expose: it is reserved here, so
 * `TYPE_IDS` leaves it out, and a value of that type is read as a `key`.
 *
 * On the wire the typeId shares its byte with the indexed flag — see {@link INDEXED_FLAG}.
 */
export const TYPE_IDS = {
  bool: 1,
  i32: 2,
  u64: 3,
  u256: 4,
  dec: 5,
  bytes: 7,
  str: 8,
  addr: 9,
  key: 10,
} as const

/**
 * Bit 7 of the `typeId` byte: set when the node should index the attribute. The low seven bits are
 * the type itself, so `str` travels as `8` unindexed and `0x88` indexed.
 */
export const INDEXED_FLAG = 0x80

/** Every type tag. */
export type TypeTag = keyof typeof TYPE_IDS

/**
 * The tags an attribute may be indexed with — every type except `bytes`, which is stored but never
 * indexed, and so can never be filtered on.
 */
export type IndexableTypeTag = Exclude<TypeTag, "bytes">

/** Whether attributes of this type may be indexed — everything except `bytes`. */
export function isIndexable(tag: TypeTag): tag is IndexableTypeTag {
  return tag !== "bytes"
}

/**
 * Marks a value as having passed its constructor's validation. The symbol is module-private, so a
 * hand-written object literal is not assignable to {@link ArkivValue} — every value crossing the
 * wire has been range-, length- and format-checked by `i32`, `u256`, `dec`, and friends.
 */
declare const validated: unique symbol

/**
 * A typed attribute value: the tag it was created with, plus the JavaScript representation of that
 * type. Constructed only by the tagged constructors in `@arkiv-network/sdk/schema`.
 *
 * @typeParam tag - The type tag, which also discriminates the {@link ArkivValue} union.
 * @typeParam value - The decoded JavaScript representation.
 */
export type Value<tag extends TypeTag, value> = {
  readonly type: tag
  readonly value: value
  readonly [validated]: tag
}

/** A boolean. */
export type BoolValue = Value<"bool", boolean>
/** A 32-bit signed integer — the default for an untagged number. */
export type I32Value = Value<"i32", number>
/**
 * A 64-bit unsigned integer — block heights, the
 * entity-nonce and `$expiresAt` are all `u64` */
export type U64Value = Value<"u64", bigint>
/** A 256-bit unsigned integer. */
export type U256Value = Value<"u256", bigint>
/**
 * A fixed-point decimal with 18 decimal places, held as its canonical decimal string (`"3.5"`).
 * Use {@link decUnits} for the exact scaled integer when you need to do arithmetic.
 */
export type DecValue = Value<"dec", string>
/** A UTF-8 string of at most 128 bytes. */
export type StrValue = Value<"str", string>
/** A 20-byte Ethereum address, as EIP-55 checksummed hex. */
export type AddrValue = Value<"addr", Address>
/** A reference to another entity. Weak by spec: dangling references are permitted. */
export type KeyValue = Value<"key", Hex>
/**
 * Opaque variable-length bytes, as lowercase `0x` hex — a blob. Stored but never indexed, so it can
 * be selected but not filtered on.
 */
export type BytesValue = Value<"bytes", Hex>

/** Any attribute value. */
export type ArkivValue =
  | BoolValue
  | I32Value
  | U64Value
  | U256Value
  | DecValue
  | BytesValue
  | StrValue
  | AddrValue
  | KeyValue

/** Any value of a type that can be indexed, and so can appear in a query — everything but `bytes`. */
export type IndexableValue = Exclude<ArkivValue, BytesValue>

/** The value type for a given tag — e.g. `ValueOf<"u256">` is {@link U256Value}. */
export type ValueOf<tag extends TypeTag> = Extract<ArkivValue, { type: tag }>

/** The JavaScript representation carried by a value — e.g. `Decoded<U256Value>` is `bigint`. */
export type Decoded<value extends ArkivValue> = value["value"]

/**
 * Builds a validated value. Module-internal: the constructors in `values.ts` call this **after**
 * validating, which is what makes the {@link validated} brand meaningful.
 */
export function makeValue<tag extends TypeTag>(
  type: tag,
  value: Decoded<ValueOf<tag>>,
): ValueOf<tag> {
  return { type, value } as unknown as ValueOf<tag>
}

/**
 * Whether `input` has the *shape* of an {@link ArkivValue} — a known type tag and a value.
 */
export function isArkivValue(input: unknown): input is ArkivValue {
  return (
    typeof input === "object" &&
    input !== null &&
    "type" in input &&
    "value" in input &&
    typeof (input as { type: unknown }).type === "string" &&
    // `Object.hasOwn`, not `in`: `in` walks the prototype chain, so `{ type: "constructor" }` would
    // be accepted as a typed value and then decode to nothing.
    Object.hasOwn(TYPE_IDS, (input as { type: string }).type)
  )
}
