/**
 * The `Snowflake` module provides compact, sortable identifiers for cluster
 * resources and events. A snowflake id is a branded `bigint` made from a
 * millisecond timestamp, a machine id, and a per-machine sequence number.
 *
 * **Common use cases**
 *
 * - Creating ids without coordinating through a central database
 * - Ordering cluster events, entity ids, or log records by generation time
 * - Encoding ids as strings at service boundaries with {@link SnowflakeFromString}
 * - Decoding a generated id into timestamp, machine id, and sequence parts with {@link toParts}
 *
 * **Gotchas**
 *
 * - Uniqueness depends on each concurrent generator using a distinct machine id
 * - Generated ids are time-sortable, but they are not random or secret values
 * - The default generator prevents local clock drift from moving ids backward
 * - More than 4096 ids in the same millisecond advance the logical timestamp
 *
 * @since 4.0.0
 */
import type * as Brand from "../../Brand.ts"
import { Clock } from "../../Clock.ts"
import * as Context from "../../Context.ts"
import * as DateTime from "../../DateTime.ts"
import * as Effect from "../../Effect.ts"
import { identity } from "../../Function.ts"
import * as Layer from "../../Layer.ts"
import * as Schema from "../../Schema.ts"
import * as Transformation from "../../SchemaTransformation.ts"
import type { MachineId } from "./MachineId.ts"

/**
 * Runtime brand identifier for cluster snowflake ids.
 *
 * @category type IDs
 * @since 4.0.0
 */
export const TypeId = "~effect/cluster/Snowflake"

/**
 * Type-level representation of the cluster snowflake brand identifier.
 *
 * @category type IDs
 * @since 4.0.0
 */
export type TypeId = typeof TypeId

/**
 * Branded bigint identifier composed from a timestamp, machine id, and per-machine
 * sequence number.
 *
 * @category models
 * @since 4.0.0
 */
export type Snowflake = Brand.Branded<bigint, TypeId>

/**
 * Constructs a branded cluster snowflake id from a bigint or bigint-compatible
 * string.
 *
 * @category constructors
 * @since 4.0.0
 */
export const Snowflake = (input: string | bigint): Snowflake =>
  typeof input === "string" ? BigInt(input) as Snowflake : input as Snowflake

/**
 * Namespace containing support types for snowflake parts and generators.
 *
 * @since 4.0.0
 */
export declare namespace Snowflake {
  /**
   * Decoded components of a snowflake id: Unix timestamp milliseconds, machine id,
   * and sequence number.
   *
   * @category models
   * @since 4.0.0
   */
  export interface Parts {
    readonly timestamp: number
    readonly machineId: MachineId
    readonly sequence: number
  }

  /**
   * Stateful generator for runner-local snowflake ids, exposing an unsafe
   * synchronous `nextUnsafe` operation and an effectful machine id setter.
   *
   * @category models
   * @since 4.0.0
   */
  export interface Generator {
    readonly nextUnsafe: () => Snowflake
    readonly setMachineId: (machineId: MachineId) => Effect.Effect<void>
  }
}

/**
 * Schema type for snowflake ids represented as branded bigints.
 *
 * @category schemas
 * @since 4.0.0
 */
export interface SnowflakeFromBigInt extends Schema.brand<Schema.BigInt, TypeId> {}

/**
 * Schema for snowflake ids represented as branded bigints.
 *
 * @category schemas
 * @since 4.0.0
 */
export const SnowflakeFromBigInt: SnowflakeFromBigInt = Schema.BigInt.pipe(Schema.brand(TypeId))

/**
 * Schema type for snowflake ids decoded from strings into branded bigints.
 *
 * @category schemas
 * @since 4.0.0
 */
export interface SnowflakeFromString extends Schema.decodeTo<SnowflakeFromBigInt, Schema.String> {}

/**
 * Schema that decodes snowflake ids from strings into branded bigints and encodes
 * them back to strings.
 *
 * @category schemas
 * @since 4.0.0
 */
export const SnowflakeFromString: SnowflakeFromString = Schema.String.pipe(
  Schema.decodeTo(SnowflakeFromBigInt, Transformation.bigintFromString)
)

/**
 * Defines the custom snowflake epoch in Unix milliseconds.
 *
 * @category constants
 * @since 4.0.0
 */
export const constEpochMillis: number = Date.UTC(2025, 0, 1)

const sinceUnixEpoch = constEpochMillis - Date.UTC(1970, 0, 1)
const constBigInt12 = BigInt(12)
const constBigInt22 = BigInt(22)
const constBigInt1024 = BigInt(1024)
const constBigInt4096 = BigInt(4096)

/**
 * Creates a branded snowflake id from a timestamp, machine id, and sequence number,
 * using the custom snowflake epoch and 10-bit machine id and 12-bit sequence
 * fields.
 *
 * **When to use**
 *
 * Use to pack known timestamp, machine id, and sequence parts into a branded
 * snowflake id when you already control id allocation.
 *
 * **Gotchas**
 *
 * Machine id values are encoded modulo 1024, and sequence values modulo 4096;
 * values outside those ranges wrap instead of being rejected.
 *
 * @see {@link toParts} for the inverse operation that decodes a snowflake id into timestamp, machine id, and sequence parts
 * @see {@link makeGenerator} for generating ids with Clock-backed timestamp and sequence management
 *
 * @category constructors
 * @since 4.0.0
 */
export const make = (options: {
  readonly machineId: MachineId
  readonly sequence: number
  readonly timestamp: number
}): Snowflake =>
  (BigInt(options.timestamp - constEpochMillis) << constBigInt22
    | (BigInt(options.machineId % 1024) << constBigInt12)
    | BigInt(options.sequence % 4096)) as Snowflake

/**
 * Extracts the Unix timestamp in milliseconds from a snowflake id.
 *
 * @category parts
 * @since 4.0.0
 */
export const timestamp = (snowflake: Snowflake): number => Number(snowflake >> constBigInt22) + sinceUnixEpoch

/**
 * Extracts the timestamp from a snowflake id as a `DateTime.Utc`.
 *
 * @category parts
 * @since 4.0.0
 */
export const dateTime = (snowflake: Snowflake): DateTime.Utc => DateTime.makeUnsafe(timestamp(snowflake))

/**
 * Extracts the machine id component from a snowflake id.
 *
 * @category parts
 * @since 4.0.0
 */
export const machineId = (snowflake: Snowflake): MachineId =>
  Number((snowflake >> constBigInt12) % constBigInt1024) as MachineId

/**
 * Extracts the per-machine sequence component from a snowflake id.
 *
 * @category parts
 * @since 4.0.0
 */
export const sequence = (snowflake: Snowflake): number => Number(snowflake % constBigInt4096)

/**
 * Decomposes a snowflake id into its timestamp, machine id, and sequence parts.
 *
 * @category parts
 * @since 4.0.0
 */
export const toParts = (snowflake: Snowflake): Snowflake.Parts => ({
  timestamp: timestamp(snowflake),
  machineId: machineId(snowflake),
  sequence: sequence(snowflake)
})

/**
 * Creates a stateful snowflake generator using `Clock`.
 *
 * **Details**
 *
 * The generator starts with a random machine id, never moves generated timestamps
 * backward, resets the sequence each millisecond, and advances the timestamp when
 * more than 4096 ids are requested in the same millisecond.
 *
 * @category Generator
 * @since 4.0.0
 */
export const makeGenerator: Effect.Effect<Snowflake.Generator> = Effect.gen(function*() {
  let machineId = Math.floor(Math.random() * 1024) as MachineId
  const clock = yield* Clock

  let sequence = 0
  let sequenceAt = Math.floor(clock.currentTimeMillisUnsafe())

  return identity<Snowflake.Generator>({
    setMachineId: (newMachineId) =>
      Effect.sync(() => {
        machineId = newMachineId
      }),
    nextUnsafe() {
      let now = Math.floor(clock.currentTimeMillisUnsafe())

      // account for clock drift, only allow time to move forward
      if (now < sequenceAt) {
        now = sequenceAt
      } else if (now > sequenceAt) {
        // reset sequence if we're in a new millisecond
        sequence = 0
        sequenceAt = now
      } else if (sequence >= 4096) {
        // if we've hit the max sequence for this millisecond, go to the next
        // millisecond
        sequenceAt++
        sequence = 0
      }

      return make({
        machineId,
        sequence: sequence++,
        timestamp: sequenceAt
      })
    }
  })
})

/**
 * Context service for a stateful snowflake id generator.
 *
 * @category Generator
 * @since 4.0.0
 */
export class Generator extends Context.Service<
  Generator,
  Snowflake.Generator
>()("effect/cluster/Snowflake/Generator") {}

/**
 * Layer that provides the default snowflake `Generator` service.
 *
 * @category Generator
 * @since 4.0.0
 */
export const layerGenerator: Layer.Layer<Generator> = Layer.effect(Generator)(makeGenerator)
