import {
  type Address,
  checksumAddress,
  type Hex,
  isAddress,
  isHex,
  stringToBytes,
  toHex,
} from "viem"
import { type AnyColumn, type Column, createColumn, isColumn } from "./column"
import { DECIMAL_UNIT, decimalToUnits, unitsToDecimal } from "./decimal"
import { InvalidValueError, MissingValueError, UntypedValueError } from "./errors"
import {
  type AddrValue,
  type ArkivValue,
  type BoolValue,
  type BytesValue,
  type DecValue,
  type I32Value,
  isArkivValue,
  type KeyValue,
  makeValue,
  type StrValue,
  type TypeTag,
  type U64Value,
  type U256Value,
} from "./types"

/** The widest `i32`. */
export const I32_MAX = 2147483647
/** The narrowest `i32`. */
export const I32_MIN = -2147483648
/** The widest `u64`. */
export const U64_MAX = 2n ** 64n - 1n
/** The widest `u256`. */
export const U256_MAX = 2n ** 256n - 1n
/**
 * The longest `str`, in UTF-8 bytes
 */
export const MAX_STRING_BYTES = 128
/**
 * The longest `bytes` value, in bytes. The cap applies to each `bytes` attribute separately, but
 * all the `bytes` values of one create or patch also share the `MAX_ENTITY_BYTES` budget.
 */
export const MAX_BYTES_LENGTH = 128 * 1024

/**
 * Finds the first lone UTF-16 surrogate — a string the node would reject, since it has no UTF-8
 * encoding. `TextEncoder` would quietly replace it with U+FFFD instead.
 */
function findLoneSurrogate(value: string): number | undefined {
  for (let index = 0; index < value.length; index++) {
    const code = value.charCodeAt(index)
    if (code < 0xd800 || code > 0xdfff) continue
    const next = value.charCodeAt(index + 1)
    if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
      index++
      continue
    }
    return index
  }
  return undefined
}

/** A `bool` column, indexed for equality once `.indexed()`. */
export function bool(): Column<"bool", false, false, false>
/**
 * A boolean.
 *
 * This is the default type for a bare boolean, so `{ flagged: true }` and
 * `{ flagged: bool(true) }` are the same value.
 *
 * @example
 * bool(true) // { type: "bool", value: true }
 */
export function bool(value: boolean): BoolValue
export function bool(...args: [] | [boolean]): BoolValue | Column<"bool", false, false, false> {
  if (args.length === 0) return createColumn("bool", toTypedValue)
  const [value] = args
  if (typeof value !== "boolean") {
    throw new InvalidValueError("bool", value, "not a boolean")
  }
  return makeValue("bool", value)
}

/** An `i32` column, indexed for equality and range queries once `.indexed()`. */
export function i32(): Column<"i32", false, false, false>
/**
 * A 32-bit signed integer. This is the default type for a bare number, so `{ level: 10 }` and
 * `{ level: i32(10) }` are the same value.
 *
 * @param value - An integer in [-2147483648, 2147483647].
 * @throws {InvalidValueError} If the value is not an integer, or does not fit an `i32`.
 *
 * @example
 * i32(10)   // { type: "i32", value: 10 }
 * i32(-42)  // negative values are fine — i32 is the signed integer
 */
export function i32(value: number | bigint): I32Value
export function i32(
  ...args: [] | [number | bigint]
): I32Value | Column<"i32", false, false, false> {
  if (args.length === 0) return createColumn("i32", toTypedValue)
  const [value] = args
  if (typeof value === "bigint") {
    if (value > BigInt(I32_MAX) || value < BigInt(I32_MIN)) {
      throw new InvalidValueError("i32", value, outOfI32Range(value >= 0n))
    }
    return makeValue("i32", Number(value))
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new InvalidValueError("i32", value, "not a number")
  }
  if (!Number.isInteger(value)) {
    throw new InvalidValueError(
      "i32",
      value,
      "not an integer",
      `Use dec(${JSON.stringify(String(value))}) for a fixed-point decimal.`,
    )
  }
  if (value > I32_MAX || value < I32_MIN) {
    throw new InvalidValueError("i32", value, outOfI32Range(value >= 0))
  }
  return makeValue("i32", value)
}

function outOfI32Range(nonNegative: boolean): string {
  const range = `outside the i32 range [${I32_MIN}, ${I32_MAX}]`
  return nonNegative
    ? `${range} — use u64(...) for large counters, timestamps and block heights (Date.now() does ` +
        `not fit an i32), or u256(...) beyond 2**64 - 1`
    : `${range} — use dec(...) for values that need more range than an i32`
}

/** A `u256` column, indexed for equality and range queries once `.indexed()`. */
export function u256(): Column<"u256", false, false, false>
/**
 * A 256-bit unsigned integer. The type for token amounts and anything else that can outgrow 2⁶⁴ —
 * reach for {@link u64} first for counters, timestamps and block heights.
 *
 * @param value - A non-negative integer, as a `bigint`, a safe `number`, or a decimal or `0x` hex
 * string.
 * @throws {InvalidValueError} If the value is negative, non-integral, or wider than 256 bits.
 *
 * @example
 * u256(1_000_000n)
 * u256(Date.now())        // safe integers are accepted
 * u256("0xf4240")         // hex, as it comes back over JSON-RPC
 */
export function u256(value: bigint | number | string): U256Value
export function u256(
  ...args: [] | [bigint | number | string]
): U256Value | Column<"u256", false, false, false> {
  if (args.length === 0) return createColumn("u256", toTypedValue)
  return makeValue("u256", toUnsigned("u256", args[0], U256_MAX))
}

/** A `u64` column, indexed for equality and range queries once `.indexed()`. */
export function u64(): Column<"u64", false, false, false>
/**
 * A 64-bit unsigned integer.
 *
 * This is the width the protocol counts in: block heights, `$expiresAt` and the entity-nonce are
 * all `u64`, so it is the type to reach for when a value has to line up with one of them. Use
 * {@link u256} for token amounts and anything else that can exceed 2⁶⁴.
 *
 * @param value - A non-negative integer, as a `bigint`, a safe `number`, or a decimal or `0x` hex
 * string.
 * @throws {InvalidValueError} If the value is negative, non-integral, or wider than 64 bits.
 *
 * @example
 * u64(1_200_000n)     // a block height
 * u64(Date.now())     // safe integers are accepted
 * u64("0x124f80")     // hex, as it comes back over JSON-RPC
 */
export function u64(value: bigint | number | string): U64Value
export function u64(
  ...args: [] | [bigint | number | string]
): U64Value | Column<"u64", false, false, false> {
  if (args.length === 0) return createColumn("u64", toTypedValue)
  return makeValue("u64", toUnsigned("u64", args[0], U64_MAX))
}

/**
 * Parses and range-checks an unsigned integer for `tag`, whose widest value is `max`.
 *
 * Shared by {@link u64} and {@link u256} so the two differ in exactly one thing — their width —
 * rather than in how they read a string or how they explain a failure.
 */
function toUnsigned(tag: "u64" | "u256", value: bigint | number | string, max: bigint): bigint {
  const parsed = toUnsignedBigint(tag, value)
  if (parsed < 0n) {
    throw new InvalidValueError(
      tag,
      value,
      `negative — ${tag} is unsigned`,
      "Use i32(...) for small signed integers, or dec(...) for a signed fixed-point value.",
    )
  }
  if (parsed > max) {
    const bits = tag === "u64" ? 64 : 256
    throw new InvalidValueError(
      tag,
      value,
      `wider than ${bits} bits`,
      tag === "u64" ? "Use u256(...) for values beyond 2**64 - 1." : undefined,
    )
  }
  return parsed
}

function toUnsignedBigint(tag: "u64" | "u256", value: bigint | number | string): bigint {
  if (typeof value === "bigint") return value
  if (typeof value === "number") {
    if (!Number.isInteger(value)) {
      throw new InvalidValueError(
        tag,
        value,
        "not an integer",
        `Use dec(${JSON.stringify(String(value))}) for a fixed-point decimal.`,
      )
    }
    if (!Number.isSafeInteger(value)) {
      throw new InvalidValueError(
        tag,
        value,
        "beyond the safe integer range, so it may already have lost precision",
        "Pass it as a bigint literal (e.g. 12345678901234567890n) or a string instead.",
      )
    }
    return BigInt(value)
  }
  if (typeof value === "string") {
    if (!/^(0x[0-9a-fA-F]+|\d+)$/.test(value)) {
      throw new InvalidValueError(tag, value, "not a decimal or 0x-hex integer")
    }
    return BigInt(value)
  }
  throw new InvalidValueError(tag, value, "not a bigint, number or string")
}

/** A `dec` column, indexed for equality and range queries once `.indexed()`. */
export function dec(): Column<"dec", false, false, false>
/**
 * A signed fixed-point decimal with exactly 18 decimal places.
 *
 * Pass fractional values as **strings**. A JavaScript number cannot represent most decimals
 * exactly, so accepting `dec(0.1 + 0.2)` would quietly store `0.30000000000000004`; requiring the
 * string keeps what you write and what is stored the same thing. Excess precision is likewise an
 * error rather than a rounding.
 *
 * @param value - A decimal string, or an integral `number` / `bigint`.
 * @throws {InvalidValueError} If the value is malformed, has more than 18 fractional digits, or
 * falls outside the `int256` range.
 *
 * @example
 * dec("3.5")
 * dec("-0.000000000000000001")  // the smallest step
 * dec(5)                        // integers may be passed as a number
 */
export function dec(value: string | number | bigint): DecValue
export function dec(
  ...args: [] | [string | number | bigint]
): DecValue | Column<"dec", false, false, false> {
  if (args.length === 0) return createColumn("dec", toTypedValue)
  const [value] = args
  if (typeof value === "string") {
    return makeValue("dec", unitsToDecimal(decimalToUnits(value)))
  }
  if (typeof value === "bigint") {
    return decFromUnits(value * DECIMAL_UNIT)
  }
  if (typeof value !== "number" || !Number.isFinite(value)) {
    throw new InvalidValueError("dec", value, "not a number")
  }
  if (!Number.isInteger(value)) {
    throw new InvalidValueError(
      "dec",
      value,
      "a fractional number cannot be represented exactly in binary floating point",
      `Pass it as a string instead: dec(${JSON.stringify(String(value))}).`,
    )
  }
  return decFromUnits(BigInt(value) * DECIMAL_UNIT)
}

/**
 * The exact scaled integer behind a `dec` — the value multiplied by 10 ** 18, which is what the
 * protocol stores and compares. Use it when you need exact arithmetic; JavaScript has no native
 * decimal type, so the SDK carries the canonical string and leaves the maths to you.
 *
 * @example
 * decUnits(dec("3.5"))  // 3500000000000000000n
 */
export function decUnits(value: DecValue): bigint {
  return decimalToUnits(value.value)
}

/**
 * A `dec` built from its scaled integer units — the inverse of {@link decUnits}.
 *
 * @example
 * decFromUnits(3_500_000_000_000_000_000n)  // dec("3.5")
 */
export function decFromUnits(units: bigint): DecValue {
  return makeValue("dec", unitsToDecimal(units))
}

/** A `str` column, indexed for equality and prefix (`STARTSWITH`) matching once `.indexed()`. */
export function str(): Column<"str", false, false, false>
/**
 * A UTF-8 string of at most 128 bytes.
 *
 * The limit is on **bytes**, not characters: a string of 128 emoji does not fit. Any character
 * is allowed, control characters included. To store raw bytes, use {@link bytes}, or hex in a
 * `str` when they must be indexed.
 *
 * @throws {InvalidValueError} If the value is not a string, holds a lone surrogate (so it has no
 * UTF-8 encoding), or exceeds 128 UTF-8 bytes.
 *
 * @example
 * str("Bob")
 */
export function str(value: string): StrValue
export function str(...args: [] | [string]): StrValue | Column<"str", false, false, false> {
  if (args.length === 0) return createColumn("str", toTypedValue)
  const [value] = args
  if (typeof value !== "string") {
    throw new InvalidValueError("str", value, "not a string")
  }
  const surrogate = findLoneSurrogate(value)
  if (surrogate !== undefined) {
    throw new InvalidValueError(
      "str",
      value,
      `contains a lone surrogate at index ${surrogate}, so it is not valid UTF-8`,
    )
  }
  const bytes = stringToBytes(value).length
  if (bytes > MAX_STRING_BYTES) {
    throw new InvalidValueError(
      "str",
      value,
      `${bytes} UTF-8 bytes exceeds the ${MAX_STRING_BYTES}-byte limit on a str`,
      "Store the long form in a bytes column and keep a short, queryable key here.",
    )
  }
  return makeValue("str", value)
}

/** An `addr` column, indexed for equality once `.indexed()`. */
export function addr(): Column<"addr", false, false, false>
/**
 * A 20-byte Ethereum address.
 *
 * All-lowercase and all-uppercase input carries no checksum and is accepted as-is; mixed-case
 * input is checked against its EIP-55 checksum, so a mistyped address is caught here rather than
 * being written to a wrong account. The stored value is always EIP-55 checksummed.
 *
 * @throws {InvalidValueError} If the value is not 20 hex bytes, or fails its EIP-55 checksum.
 *
 * @example
 * addr("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
 */
export function addr(value: string): AddrValue
export function addr(...args: [] | [string]): AddrValue | Column<"addr", false, false, false> {
  if (args.length === 0) return createColumn("addr", toTypedValue)
  const [value] = args
  if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) {
    throw new InvalidValueError("addr", value, "not a 20-byte 0x-prefixed hex address")
  }
  const body = value.slice(2)
  const cased = body !== body.toLowerCase() && body !== body.toUpperCase()
  if (cased && !isAddress(value, { strict: true })) {
    throw new InvalidValueError(
      "addr",
      value,
      "fails its EIP-55 checksum, so a digit is probably mistyped",
      `If those hex digits are right, the correctly checksummed form is ` +
        `${checksumAddress(value as Address)}.`,
    )
  }
  return makeValue("addr", checksumAddress(value as Address))
}

/** A `key` column — a reference to another entity — indexed for equality once `.indexed()`. */
export function key(): Column<"key", false, false, false>
/**
 * A reference to another entity.
 *
 * References are **weak**: nothing checks that the target exists when you write it, and a
 * reference to a deleted or expired entity is allowed to dangle.
 *
 * @param value - A 32-byte entity key, as `0x` hex or raw bytes.
 * @throws {InvalidValueError} If the value is not exactly 32 bytes.
 *
 * @example
 * key(parentEntityKey)
 */
export function key(value: Hex | Uint8Array): KeyValue
export function key(
  ...args: [] | [Hex | Uint8Array]
): KeyValue | Column<"key", false, false, false> {
  if (args.length === 0) return createColumn("key", toTypedValue)
  return makeValue("key", asFixedHex("key", args[0], 32))
}

/**
 * A `bytes` column — a blob. Bytes are stored but never indexed, so a bytes column has no
 * `.indexed()`: it can be selected, not filtered on. A schema may have several, within the
 * `MAX_ATTRIBUTES` cap on columns — but the `bytes` values of one create or patch share a single
 * `MAX_ENTITY_BYTES` budget.
 */
export function bytes(): Column<"bytes", false, false, false>
/**
 * Opaque bytes of any length up to 128 KiB — a document, an image, a serialized object. This is the
 * default type for a bare `Uint8Array`, so `{ body: buf }` and `{ body: bytes(buf) }` are the same
 * value.
 *
 * @param value - The bytes, raw or as `0x` hex. Hex is taken as the bytes it spells, never as text.
 * @throws {InvalidValueError} If the value is not bytes or hex, or exceeds 128 KiB.
 *
 * @example
 * bytes(new TextEncoder().encode("# Hello"))
 * bytes("0xdeadbeef")
 */
export function bytes(value: Hex | Uint8Array): BytesValue
export function bytes(
  ...args: [] | [Hex | Uint8Array]
): BytesValue | Column<"bytes", false, false, false> {
  if (args.length === 0) return createColumn("bytes", toTypedValue)
  const [value] = args
  const hex = asHex("bytes", value)
  const length = (hex.length - 2) / 2
  if (length > MAX_BYTES_LENGTH) {
    throw new InvalidValueError(
      "bytes",
      `<${length} bytes>`,
      `${length} bytes exceeds the ${MAX_BYTES_LENGTH}-byte limit on a bytes value`,
      "Split it across several entities, or store the bulk elsewhere and keep a reference.",
    )
  }
  return makeValue("bytes", hex)
}

/** Normalizes hex-or-bytes input of a known width into lowercase `0x` hex. */
function asFixedHex(tag: "key", value: Hex | Uint8Array, size: number): Hex {
  if (value instanceof Uint8Array) {
    if (value.length !== size) {
      throw new InvalidValueError(tag, `<${value.length} bytes>`, `not exactly ${size} bytes`)
    }
    return toHex(value)
  }
  const hex = asHex(tag, value)
  if (hex.length !== 2 + size * 2) {
    throw new InvalidValueError(
      tag,
      value,
      `${(hex.length - 2) / 2} bytes, not exactly ${size} bytes`,
    )
  }
  return hex
}

/** Normalizes hex-or-bytes input into lowercase `0x` hex of a whole number of bytes. */
function asHex(tag: "key" | "bytes", value: Hex | Uint8Array): Hex {
  if (value instanceof Uint8Array) return toHex(value)
  if (typeof value !== "string" || !isHex(value, { strict: true })) {
    throw new InvalidValueError(tag, value, "not 0x-prefixed hex")
  }
  if (value.length % 2 !== 0) {
    throw new InvalidValueError(tag, value, "an odd number of hex digits, so not whole bytes")
  }
  return value.toLowerCase() as Hex
}

/**
 * A JavaScript value accepted wherever a typed value is expected: either a tagged value, or one of
 * the five bare forms that map unambiguously onto a single type.
 *
 * | bare form    | type    |
 * | ------------ | ------- |
 * | `boolean`    | `bool`  |
 * | `number`     | `i32`   |
 * | `bigint`     | `u256`  |
 * | `string`     | `str`   |
 * | `Uint8Array` | `bytes` |
 *
 * Anything else — a large number, a decimal, an address, an entity key — must name its type, which
 * is what keeps `"0x1234..."` a string rather than silently becoming a key.
 *
 * These defaults apply identically when writing and when querying, so a value written bare and
 * queried bare always agrees with itself. The mismatch to watch for is a bare value where its
 * column has another type: a bare `bigint` is a `u256`, not a {@link u64}. Through a schema that is
 * caught — a write throws `SchemaMismatchError` and a query throws `InvalidPredicateError` — but a
 * query without a schema sends the predicate as written, and the engine, for which a type mismatch
 * is not an error, simply matches nothing.
 */
export type ValueInput = ArkivValue | boolean | number | bigint | string | Uint8Array

/**
 * The type a {@link ValueInput} resolves to — the tag of a tagged value, or the default of a bare
 * form. `never` for anything else.
 */
export type TagOf<input> = input extends { readonly type: infer tag extends TypeTag }
  ? tag
  : input extends boolean
    ? "bool"
    : input extends number
      ? "i32"
      : input extends bigint
        ? "u256"
        : input extends string
          ? "str"
          : input extends Uint8Array
            ? "bytes"
            : never

/**
 * The bare JavaScript form that defaults to `tag`, where there is one — `boolean` for `bool`,
 * `number` for `i32`, `bigint` for `u256`, `string` for `str`, `Uint8Array` for `bytes`. The other
 * types — `u64`, `dec`, `addr` and `key` — must be written tagged.
 */
export type BareOf<tag extends TypeTag> = {
  bool: boolean
  i32: number
  u64: never
  u256: bigint
  dec: never
  bytes: Uint8Array
  str: string
  addr: never
  key: never
}[tag]

/**
 * Resolves a {@link ValueInput} to a typed value, applying the bare-form defaults above. Tagged
 * values pass through untouched.
 *
 * @param input - The value to resolve.
 * @param attributeName - The attribute this value belongs to, used to make errors name it.
 *
 * @throws {MissingValueError} If the input is `undefined` or `null` — an attribute is either set
 * with a value and a type, or not set at all.
 * @throws {UntypedValueError} If the input is not a tagged value or a bare boolean, number, bigint,
 * string or `Uint8Array`.
 * @throws {InvalidValueError} If a bare value does not fit the type it defaults to.
 *
 * @example
 * toValue(10)          // i32(10)
 * toValue(10n)         // u256(10n)
 * toValue("hello")     // str("hello")
 * toValue(dec("3.5"))  // unchanged
 */
export function toValue(input: ValueInput, attributeName?: string): ArkivValue {
  // `undefined` and `null` are rejected before the type switch. They are not assignable to
  // ValueInput, but they still arrive at runtime — from a spread of an optional-shaped object, or
  // from data that never went through the type system — and "cannot infer a type for undefined"
  // does not tell you the fix, which is always to leave the attribute out.
  if (input === undefined || input === null) {
    throw new MissingValueError(input, attributeName)
  }
  switch (typeof input) {
    case "boolean":
      return bool(input)
    case "number":
      return i32(input)
    case "bigint":
      return u256(input)
    case "string":
      return str(input)
    default: {
      if (input instanceof Uint8Array) return bytes(input)
      if (isArkivValue(input)) return revalidate(input)
      // Widened: a column is not a ValueInput, but it is the one mix-up the dual-mode constructors
      // invite — `str()` where `str("...")` was meant — so it is named rather than left untyped.
      const candidate: unknown = input
      if (isColumn(candidate)) throw columnGivenAsValue(candidate)
      throw new UntypedValueError(input)
    }
  }
}

/**
 * Resolves `input` to a value of type `tag` — a tagged value of that type, or whatever `tag`'s
 * constructor accepts. What a column's `.default(...)` runs its argument through.
 *
 * @throws {InvalidValueError} If the input does not make a valid value of type `tag`.
 */
function toTypedValue(tag: TypeTag, input: unknown): ArkivValue {
  if (isArkivValue(input)) {
    const value = revalidate(input)
    if (value.type !== tag) {
      throw new InvalidValueError(tag, input, `a ${value.type} value, not a ${tag}`)
    }
    return value
  }
  if (isColumn(input)) throw columnGivenAsValue(input)
  // Through the constructor, so every type accepts exactly what its constructor does.
  const construct = CONSTRUCTORS[tag] as (value: unknown) => ArkivValue
  return construct(input)
}

function columnGivenAsValue(column: AnyColumn): InvalidValueError {
  return new InvalidValueError(
    column.type,
    `<${column.type} column>`,
    "a column, not a value",
    `Pass the value itself, e.g. ${column.type}(...) with an argument — ${column.type}() with ` +
      "none declares a schema column.",
  )
}

/** Each tag's constructor, called with a value. */
const CONSTRUCTORS: { readonly [tag in TypeTag]: (value: never) => ArkivValue } = {
  bool,
  i32,
  u64,
  u256,
  dec,
  bytes,
  str,
  addr,
  key,
}

/**
 * Re-runs a tagged value through its own constructor.
 *
 * {@link isArkivValue} can only check shape — the `validated` brand is a type-level construct with
 * no runtime existence, so a hand-written `{ type: "u256", value: "1000" }` (from `JSON.parse`, a
 * config file, an `as any` boundary) is indistinguishable from a real `u256(1000n)`. Passing it
 * through unchecked was silent corruption rather than an error: the ABI encoder would take the
 * *string* "1000" and write its UTF-8 bytes into the word, storing 2.27e76 on chain.
 *
 * Running the constructor again is what makes the brand's promise — "every value crossing the wire
 * has been range-, length- and format-checked" — true at runtime. For a value that really did come
 * from a constructor it is a no-op that returns an equal value.
 */
function revalidate(value: ArkivValue): ArkivValue {
  switch (value.type) {
    case "bool":
      return bool(value.value)
    case "i32":
      return i32(value.value)
    case "u64":
      return u64(value.value)
    case "u256":
      return u256(value.value)
    case "dec":
      return dec(value.value)
    case "bytes":
      return bytes(value.value)
    case "str":
      return str(value.value)
    case "addr":
      return addr(value.value)
    case "key":
      return key(value.value)
  }
}
