/**
 * Defines the low-level tracing model used by Effect.
 *
 * A span records the lifetime of an operation, including its name, parent,
 * attributes, links, annotations, sampling decision, kind, and completion
 * status. The module also defines the tracer service, parent-span context,
 * external span support, trace propagation settings, and the default in-memory
 * span implementation.
 *
 * @since 2.0.0
 */
import * as Context from "./Context.ts"
import type * as Exit from "./Exit.ts"
import type { Fiber } from "./Fiber.ts"
import { constFalse, type LazyArg } from "./Function.ts"
import type * as core from "./internal/core.ts"
import type { LogLevel } from "./LogLevel.ts"
import * as Option from "./Option.ts"

/**
 * A tracing backend used by Effect to create spans. Custom tracers implement
 * `span` to allocate a span from the supplied name, parent, annotations,
 * links, start time, kind, root flag, and sampling decision.
 *
 * @category models
 * @since 2.0.0
 */
export interface Tracer {
  span(this: Tracer, options: {
    readonly name: string
    readonly parent: Option.Option<AnySpan>
    readonly annotations: Context.Context<never>
    readonly links: Array<SpanLink>
    readonly startTime: bigint
    readonly kind: SpanKind
    readonly root: boolean
    readonly sampled: boolean
  }): Span
  readonly context?:
    | (<X>(primitive: EffectPrimitive<X>, fiber: Fiber<any, any>) => X)
    | undefined
}

const evaluate = "~effect/Effect/evaluate" satisfies core.evaluate

/**
 * A low-level Effect primitive that can be evaluated by a tracer-specific
 * context for the current fiber.
 *
 * @category models
 * @since 4.0.0
 */
export interface EffectPrimitive<X> {
  [evaluate](this: EffectPrimitive<X>, fiber: Fiber<any, any>): X
}

/**
 * Lifecycle state of a span, where `Started` records the start time and
 * `Ended` records the start time, end time, and exit value with which the span
 * completed.
 *
 * **Example** (Creating span statuses)
 *
 * ```ts
 * import { Exit } from "effect"
 * import type { Tracer } from "effect"
 *
 * const startTime = 1_000_000_000n
 * const endTime = 1_500_000_000n
 *
 * const startedStatus: Tracer.SpanStatus = {
 *   _tag: "Started",
 *   startTime
 * }
 *
 * const endedStatus: Tracer.SpanStatus = {
 *   _tag: "Ended",
 *   startTime,
 *   endTime,
 *   exit: Exit.succeed("result")
 * }
 *
 * console.log(startedStatus._tag) // "Started"
 * console.log(endedStatus.endTime - endedStatus.startTime) // 500000000n
 * ```
 *
 * @category models
 * @since 2.0.0
 */
export type SpanStatus = {
  _tag: "Started"
  startTime: bigint
} | {
  _tag: "Ended"
  startTime: bigint
  endTime: bigint
  exit: Exit.Exit<unknown, unknown>
}

/**
 * A span value that can participate in tracing, either an Effect-managed
 * `Span` or an `ExternalSpan` propagated from another tracing system.
 *
 * **Example** (Accepting any span)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Function that accepts any span type
 * const logSpan = (span: Tracer.AnySpan) => {
 *   console.log(`Span ID: ${span.spanId}, Trace ID: ${span.traceId}`)
 *   return Effect.succeed(span)
 * }
 *
 * // Works with both Span and ExternalSpan
 * const externalSpan = Tracer.externalSpan({
 *   spanId: "span-123",
 *   traceId: "trace-456"
 * })
 * ```
 *
 * @category models
 * @since 2.0.0
 */
export type AnySpan = Span | ExternalSpan

/**
 * Defines the string key for the parent-span context service.
 *
 * **When to use**
 *
 * Use when you need the raw context key for parent span lookup in lower-level
 * tracing code.
 *
 * **Example** (Reading the parent span key)
 *
 * ```ts
 * import { Tracer } from "effect"
 *
 * // The key used to identify parent spans in the context
 * console.log(Tracer.ParentSpanKey) // "effect/Tracer/ParentSpan"
 * ```
 *
 * @category constants
 * @since 4.0.0
 */
export const ParentSpanKey = "effect/Tracer/ParentSpan"

/**
 * Context service containing the `Span` or `ExternalSpan` to use as the parent
 * of newly-created child spans.
 *
 * **Example** (Accessing the parent span)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Access the parent span from the context
 * const program = Effect.gen(function*() {
 *   const parentSpan = yield* Effect.service(Tracer.ParentSpan)
 *   console.log(`Parent span: ${parentSpan.spanId}`)
 * })
 * ```
 *
 * @category services
 * @since 2.0.0
 */
export class ParentSpan extends Context.Service<ParentSpan, AnySpan>()(ParentSpanKey) {}

/**
 * Represents a span created outside Effect's tracer, carrying trace and span
 * identifiers, sampling state, and annotations so it can be used as a parent or
 * link in Effect tracing.
 *
 * **Example** (Creating an external span value)
 *
 * ```ts
 * import { Context } from "effect"
 * import type { Tracer } from "effect"
 *
 * // Create an external span from another tracing system
 * const externalSpan: Tracer.ExternalSpan = {
 *   _tag: "ExternalSpan",
 *   spanId: "span-abc-123",
 *   traceId: "trace-xyz-789",
 *   sampled: true,
 *   annotations: Context.empty()
 * }
 *
 * console.log(`External span: ${externalSpan.spanId}`)
 * ```
 *
 * @category models
 * @since 2.0.0
 */
export interface ExternalSpan {
  readonly _tag: "ExternalSpan"
  readonly spanId: string
  readonly traceId: string
  readonly sampled: boolean
  readonly annotations: Context.Context<never>
}

/**
 * Options accepted by span-creating APIs, combining span metadata such as
 * attributes, links, parent/root selection, kind, sampling, and trace level
 * with stack trace capture settings.
 *
 * **Example** (Configuring span options)
 *
 * ```ts
 * import { Effect } from "effect"
 * import type { Tracer } from "effect"
 *
 * // Create an effect with span options
 * const options: Tracer.SpanOptions = {
 *   attributes: { "user.id": "123", "operation": "data-processing" },
 *   kind: "internal",
 *   root: false,
 *   captureStackTrace: true
 * }
 *
 * const program = Effect.succeed("Hello World").pipe(
 *   Effect.withSpan("my-operation", options)
 * )
 * ```
 *
 * @category options
 * @since 3.1.0
 */
export interface SpanOptions extends SpanOptionsNoTrace, TraceOptions {}

/**
 * Span creation options that do not control stack trace capture, including
 * attributes, links, parent or root selection, annotations, span kind,
 * sampling, and the trace level used for filtering.
 *
 * @category options
 * @since 4.0.0
 */
export interface SpanOptionsNoTrace {
  readonly attributes?: Record<string, unknown> | undefined
  readonly links?: ReadonlyArray<SpanLink> | undefined
  readonly parent?: AnySpan | undefined
  readonly root?: boolean | undefined
  readonly annotations?: Context.Context<never> | undefined
  readonly kind?: SpanKind | undefined
  readonly sampled?: boolean | undefined
  readonly level?: LogLevel | undefined
}

/**
 * Options that control stack trace capture for tracing wrappers.
 * `captureStackTrace` can disable capture or provide a lazy stack string.
 *
 * @category options
 * @since 4.0.0
 */
export interface TraceOptions {
  readonly captureStackTrace?: boolean | LazyArg<string | undefined> | undefined
}

/**
 * OpenTelemetry-style role describing the kind of operation represented by a
 * span: internal work, server handling, client calls, producing, or consuming.
 *
 * **Example** (Configuring span kinds)
 *
 * ```ts
 * import { Effect } from "effect"
 * import type { Tracer } from "effect"
 *
 * // Different span kinds for different operations
 * const serverSpan = Effect.withSpan("handle-request", {
 *   kind: "server" as Tracer.SpanKind
 * })
 *
 * const clientSpan = Effect.withSpan("api-call", {
 *   kind: "client" as Tracer.SpanKind
 * })
 *
 * const internalSpan = Effect.withSpan("internal-process", {
 *   kind: "internal" as Tracer.SpanKind
 * })
 * ```
 *
 * @category models
 * @since 3.1.0
 */
export type SpanKind = "internal" | "server" | "client" | "producer" | "consumer"

/**
 * A span created by an Effect tracer. It carries trace identity, parent,
 * annotations, attributes, links, sampling and kind information, lifecycle
 * status, and methods to end the span or add attributes, events, and links.
 *
 * **Example** (Working with spans)
 *
 * ```ts
 * import { Context, Exit, Option } from "effect"
 * import type { Tracer } from "effect"
 *
 * const attributes = new Map<string, unknown>()
 * const links: Array<Tracer.SpanLink> = []
 * let status: Tracer.SpanStatus = {
 *   _tag: "Started",
 *   startTime: 1_000_000_000n
 * }
 *
 * const span: Tracer.Span = {
 *   _tag: "Span",
 *   name: "load-user",
 *   spanId: "span-1",
 *   traceId: "trace-1",
 *   parent: Option.none(),
 *   annotations: Context.empty(),
 *   get status() {
 *     return status
 *   },
 *   attributes,
 *   links,
 *   sampled: true,
 *   kind: "internal",
 *   end(endTime, exit) {
 *     status = { _tag: "Ended", startTime: status.startTime, endTime, exit }
 *   },
 *   attribute(key, value) {
 *     attributes.set(key, value)
 *   },
 *   event(name, startTime, eventAttributes = {}) {
 *     console.log(`${name} at ${startTime} with ${Object.keys(eventAttributes).length} attributes`)
 *   },
 *   addLinks(newLinks) {
 *     links.push(...newLinks)
 *   }
 * }
 *
 * span.attribute("user.id", "123")
 * span.end(1_500_000_000n, Exit.succeed("user"))
 *
 * console.log(span.name) // "load-user"
 * console.log(span.attributes.get("user.id")) // "123"
 * console.log(span.status._tag) // "Ended"
 * ```
 *
 * @category models
 * @since 2.0.0
 */
export interface Span {
  readonly _tag: "Span"
  readonly name: string
  readonly spanId: string
  readonly traceId: string
  readonly parent: Option.Option<AnySpan>
  readonly annotations: Context.Context<never>
  readonly status: SpanStatus
  readonly attributes: ReadonlyMap<string, unknown>
  readonly links: ReadonlyArray<SpanLink>
  readonly sampled: boolean
  readonly kind: SpanKind
  end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void
  attribute(key: string, value: unknown): void
  event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void
  addLinks(links: ReadonlyArray<SpanLink>): void
}

/**
 * A relationship from one span to another span, with attributes describing the
 * relationship.
 *
 * **Example** (Linking spans)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Create a span link to connect spans
 * const externalSpan = Tracer.externalSpan({
 *   spanId: "external-span-123",
 *   traceId: "trace-456"
 * })
 *
 * const link: Tracer.SpanLink = {
 *   span: externalSpan,
 *   attributes: { "link.type": "follows-from", "service": "external-api" }
 * }
 *
 * const program = Effect.succeed("result").pipe(
 *   Effect.withSpan("linked-operation", { links: [link] })
 * )
 * ```
 *
 * @category models
 * @since 2.0.0
 */
export interface SpanLink {
  readonly span: AnySpan
  readonly attributes: Readonly<Record<string, unknown>>
}

/**
 * Creates a `Tracer` value from a tracer implementation object.
 *
 * **When to use**
 *
 * Use to create a custom tracing backend value that Effect can use when
 * creating spans.
 *
 * **Details**
 *
 * `make` returns the supplied implementation object unchanged. The object must
 * satisfy the `Tracer` contract, including a `span` method that returns a
 * `Span`.
 *
 * @see {@link Span} for the span values returned by tracer implementations
 *
 * @category constructors
 * @since 2.0.0
 */
export const make = (options: Tracer): Tracer => options

/**
 * Creates an `ExternalSpan` from trace and span identifiers, defaulting
 * `sampled` to `true` and annotations to an empty context when they are not
 * provided.
 *
 * **Example** (Creating an external span)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Create an external span from another tracing system
 * const span = Tracer.externalSpan({
 *   spanId: "span-abc-123",
 *   traceId: "trace-xyz-789",
 *   sampled: true
 * })
 *
 * // Use the external span as a parent
 * const program = Effect.succeed("Hello").pipe(
 *   Effect.withSpan("child-operation", { parent: span })
 * )
 * ```
 *
 * @category constructors
 * @since 2.0.0
 */
export const externalSpan = (
  options: {
    readonly spanId: string
    readonly traceId: string
    readonly sampled?: boolean | undefined
    readonly annotations?: Context.Context<never> | undefined
  }
): ExternalSpan => ({
  _tag: "ExternalSpan",
  spanId: options.spanId,
  traceId: options.traceId,
  sampled: options.sampled ?? true,
  annotations: options.annotations ?? Context.empty()
})

/**
 * Context reference for disabling trace propagation.
 *
 * **When to use**
 *
 * Use to prevent spans in a scope from propagating tracing context.
 *
 * **Details**
 *
 * When enabled on fiber or span annotations, new spans are created as
 * non-propagating no-op spans and disabled spans are skipped when deriving a
 * parent span.
 *
 * **Example** (Disabling span propagation)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Disable span propagation for a specific effect
 * const program = Effect.gen(function*() {
 *   yield* Effect.log("This will not propagate parent span")
 * }).pipe(
 *   Effect.provideService(Tracer.DisablePropagation, true)
 * )
 * ```
 *
 * @category references
 * @since 3.12.0
 */
export const DisablePropagation = Context.Reference<boolean>(
  "effect/Tracer/DisablePropagation",
  { defaultValue: constFalse }
)

/**
 * Context reference for controlling the current trace level for dynamic filtering.
 *
 * **When to use**
 *
 * Use to set the default trace level for spans in a scope when span options do
 * not provide `level`.
 *
 * **Details**
 *
 * The default value is `"Info"`. Span creation uses `options.level ??
 * CurrentTraceLevel` before applying `MinimumTraceLevel`.
 *
 * @see {@link MinimumTraceLevel} for the threshold that decides whether spans at that level are sampled
 *
 * @category references
 * @since 4.0.0
 */
export const CurrentTraceLevel: Context.Reference<LogLevel> = Context.Reference<LogLevel>(
  "effect/Tracer/CurrentTraceLevel",
  { defaultValue: () => "Info" }
)

/**
 * Context reference for setting the minimum trace level threshold. Spans and their
 * descendants below this level will have their sampling decision forced to
 * false, preventing them from being exported.
 *
 * **When to use**
 *
 * Use to set the trace-level threshold that controls whether spans are sampled
 * by default.
 *
 * **Details**
 *
 * The default value is `"All"`. Span creation compares the span level from
 * `options.level ?? CurrentTraceLevel` against this threshold.
 *
 * **Gotchas**
 *
 * Explicit `options.sampled` bypasses threshold computation.
 *
 * @see {@link CurrentTraceLevel} for the default span level used when options do not specify one
 *
 * @category references
 * @since 4.0.0
 */
export const MinimumTraceLevel = Context.Reference<
  LogLevel
>("effect/Tracer/MinimumTraceLevel", { defaultValue: () => "All" })

/**
 * Defines the string key for the active tracer context reference.
 *
 * **When to use**
 *
 * Use when you need the raw context key for active tracer lookup in lower-level
 * tracing code.
 *
 * @category references
 * @since 4.0.0
 */
export const TracerKey = "effect/Tracer"

/**
 * Context reference for the active tracer service. By default it uses the
 * native tracer, which creates `NativeSpan` instances.
 *
 * **Example** (Accessing the current tracer)
 *
 * ```ts
 * import { Effect, Tracer } from "effect"
 *
 * // Access the current tracer from the context
 * const program = Effect.gen(function*() {
 *   const tracer = yield* Effect.service(Tracer.Tracer)
 *   console.log("Using current tracer")
 * })
 *
 * // Or use the built-in tracer effect
 * const tracerEffect = Effect.gen(function*() {
 *   const tracer = yield* Effect.tracer
 *   console.log("Current tracer obtained")
 * })
 * ```
 *
 * @category references
 * @since 2.0.0
 */
export const Tracer: Context.Reference<Tracer> = Context.Reference<Tracer>(TracerKey, {
  defaultValue: () =>
    make({
      span: (options) => new NativeSpan(options)
    })
})

/**
 * Default in-memory `Span` implementation used by the native tracer. It
 * generates span and trace identifiers, stores attributes, events, and links,
 * and records `Started` or `Ended` status.
 *
 * **Details**
 *
 * The constructor initializes the span with `Started` status, inherits the
 * parent trace id or generates a new one, and always generates a new span id.
 * Attributes, events, links, and status are then mutated through `Span` methods.
 *
 * @see {@link Span} for the interface implemented by native spans
 *
 * @category native tracer
 * @since 4.0.0
 */
export class NativeSpan implements Span {
  readonly _tag = "Span"
  readonly spanId: string
  readonly traceId: string = "native"
  readonly sampled: boolean

  readonly name: string
  readonly parent: Option.Option<AnySpan>
  readonly annotations: Context.Context<never>
  readonly links: Array<SpanLink>
  readonly startTime: bigint
  readonly kind: SpanKind

  status: SpanStatus
  attributes: Map<string, unknown>
  events: Array<[name: string, startTime: bigint, attributes: Record<string, unknown>]> = []

  constructor(options: {
    readonly name: string
    readonly parent: Option.Option<AnySpan>
    readonly annotations: Context.Context<never>
    readonly links: Array<SpanLink>
    readonly startTime: bigint
    readonly kind: SpanKind
    readonly sampled: boolean
  }) {
    this.name = options.name
    this.parent = options.parent
    this.annotations = options.annotations
    this.links = options.links
    this.startTime = options.startTime
    this.kind = options.kind
    this.sampled = options.sampled
    this.status = {
      _tag: "Started",
      startTime: options.startTime
    }
    this.attributes = new Map()
    this.traceId = Option.getOrUndefined(options.parent)?.traceId ?? randomHexString(32)
    this.spanId = randomHexString(16)
  }

  end(endTime: bigint, exit: Exit.Exit<unknown, unknown>): void {
    this.status = {
      _tag: "Ended",
      endTime,
      exit,
      startTime: this.status.startTime
    }
  }

  attribute(key: string, value: unknown): void {
    this.attributes.set(key, value)
  }

  event(name: string, startTime: bigint, attributes?: Record<string, unknown>): void {
    this.events.push([name, startTime, attributes ?? {}])
  }

  addLinks(links: ReadonlyArray<SpanLink>): void {
    // oxlint-disable-next-line no-restricted-syntax
    this.links.push(...links)
  }
}

const randomHexString = (function() {
  const characters = "abcdef0123456789"
  const charactersLength = characters.length
  return function(length: number) {
    let result = ""
    for (let i = 0; i < length; i++) {
      result += characters.charAt(Math.floor(Math.random() * charactersLength))
    }
    return result
  }
})()
