/**
 * @title Batching requests with RequestResolver
 *
 * Define request types with `Request.Class`, resolve them in batches with `RequestResolver`.
 */
import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect"

export class User extends Schema.Class<User>("User")({
  id: Schema.Number,
  name: Schema.String,
  email: Schema.String
}) {}

export class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()("UserNotFound", {
  id: Schema.Number
}) {}

export class Users extends Context.Service<Users, {
  getUserById(id: number): Effect.Effect<User, UserNotFound>
}>()("app/Users") {
  static readonly layer = Layer.effect(
    Users,
    Effect.gen(function*() {
      // Request classes model a single external lookup.
      class GetUserById extends Request.Class<
        { readonly id: number },
        User, // The success type of the request
        UserNotFound, // The error type of the request
        never // The requirements type of the request, if any
      > {}

      // Simulate an external data source that supports batched lookup.
      const usersTable = new Map<number, User>([
        [1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })],
        [2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })],
        [3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })]
      ])

      const resolver = yield* RequestResolver.make<GetUserById>(Effect.fn(function*(entries) {
        for (const entry of entries) {
          const user = usersTable.get(entry.request.id)

          // If the request had requirements, you can access them with
          // `entry.context`
          const requestSpan = Context.getOption(entry.context, Tracer.ParentSpan)
          console.log("Request span", requestSpan)

          if (user) {
            // Complete requests with .completeUnsafe and pass in an Exit value
            entry.completeUnsafe(Exit.succeed(user))
          } else {
            entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id })))
          }
        }
      })).pipe(
        // Control the delay before the resolver is executed. This allows more
        // requests to be batched together, but also adds latency to the first
        // request.
        RequestResolver.setDelay("10 millis"),
        // RequestResolver.withSpan adds a span around the resolver execution,
        // and also sets up span links for each request
        RequestResolver.withSpan("Users.getUserById.resolver"),
        // RequestResolver.withCache adds a simple LRU cache to avoid repeated
        // lookups for the same ID.
        RequestResolver.withCache({ capacity: 1024 })
      )

      // Wrap the resolver in a service method. The resolver batches calls to
      // `getUserById` that occur within the delay window.
      const getUserById = (id: number) =>
        Effect.request(new GetUserById({ id }), resolver).pipe(
          Effect.withSpan("Users.getUserById", { attributes: { userId: id } })
        )

      return { getUserById } as const
    })
  )
}

// Run multiple lookups concurrently. The resolver receives one batch and
// internally deduplicates repeated IDs for the external call.
export const batchedLookupExample = Effect.gen(function*() {
  const { getUserById } = yield* Users

  // This will only trigger a single call to the resolver with the unique IDs [1, 2, 3].
  yield* Effect.forEach([1, 2, 1, 3, 2], getUserById, {
    concurrency: "unbounded"
  })
})
