import type {
  NotFoundError,
  NotificationId,
  OrganizationId,
  ProjectId,
  RepositoryError,
  SqlClient,
  UserId,
} from "@domain/shared"
import { UserRepository } from "@domain/users"
import { Effect } from "effect"
import type { Notification, NotificationKind } from "../entities/notification.ts"
import { shouldSendEmail } from "../entities/notification-preferences.ts"
import { NotificationRepository } from "../ports/notification-repository.ts"

export interface CreateNotificationInput {
  readonly organizationId: OrganizationId
  readonly userId: UserId
  readonly kind: NotificationKind
  readonly idempotencyKey: string
  readonly payload: Record<string, unknown>
  /** Pre-generated by the producer so retries don't generate fresh ids. */
  readonly notificationId: NotificationId
  /**
   * Optional project anchor. `null` for kinds with no natural project
   * (today: `custom.message`). When set, the row is cleaned up on
   * `ProjectDeleted`.
   */
  readonly projectId: ProjectId | null
}

export interface CreateNotificationResult {
  /**
   * The row that landed in the table. `null` when the insert hit
   * `ON CONFLICT DO NOTHING` (an earlier producer attempt already wrote
   * this row — the consumer is idempotent at this layer).
   */
  readonly notification: Notification | null
  /**
   * Whether the user's preferences allow an email for this notification.
   * `false` when the row was already created, or when the user opted out
   * of the kind's group, or when the user record is missing.
   */
  readonly emailEligible: boolean
}

export type CreateNotificationError = RepositoryError | NotFoundError

/**
 * Creator step. Idempotently inserts a single notification row for the
 * given user, then resolves the user's email preference for the kind's
 * group. Returns the channel-eligibility decision so the caller (worker)
 * can fan out to channel-specific delivery jobs.
 */
export const createNotificationUseCase = (input: CreateNotificationInput) =>
  Effect.gen(function* () {
    yield* Effect.annotateCurrentSpan("kind", input.kind)
    yield* Effect.annotateCurrentSpan("userId", input.userId)
    yield* Effect.annotateCurrentSpan("organizationId", input.organizationId)
    yield* Effect.annotateCurrentSpan("idempotencyKey", input.idempotencyKey)

    const repo = yield* NotificationRepository

    const row: Notification = {
      id: input.notificationId,
      organizationId: input.organizationId,
      userId: input.userId,
      kind: input.kind,
      idempotencyKey: input.idempotencyKey,
      projectId: input.projectId,
      payload: input.payload,
      createdAt: new Date(),
      seenAt: null,
      emailedAt: null,
    }

    const inserted = yield* repo.insertIfAbsent(row)

    if (inserted === null) {
      yield* Effect.annotateCurrentSpan("dedup", "already-existed")
      return { notification: null, emailEligible: false } as const
    }

    // Read the recipient's prefs to decide email eligibility. Missing user
    // (impossible in practice — we just resolved them as a member) is
    // treated as "no email" rather than failing the insert.
    const users = yield* UserRepository
    const user = yield* users.findById(input.userId).pipe(Effect.catchTag("NotFoundError", () => Effect.succeed(null)))

    const emailEligible = user !== null && shouldSendEmail(user.notificationPreferences ?? null, input.kind)

    return { notification: inserted, emailEligible } as const
  }).pipe(Effect.withSpan("notifications.createNotification")) as Effect.Effect<
    CreateNotificationResult,
    CreateNotificationError,
    SqlClient | NotificationRepository | UserRepository
  >
