import { and, eq } from 'drizzle-orm'
import type { Querier } from '@platform-modules/db'
import {
  ImmutableFieldError,
  NotPurchasedError,
  ReviewValidationError,
  ReviewsConfigError,
  type ImmutableField,
} from './errors.js'
import { review, type ReviewsSchema } from './schema.js'
import { rowToReview } from './row-to-review.js'
import type { ModerationAdapter, Review, ReviewRating, ReviewStatus, VerifiedPurchasePort } from './types.js'

export interface SubmitReviewInput {
  productId: string
  userId: string
  vendorId: string | null
  rating: number
  body?: string | null
  purchaseId: string
}

export interface SubmitReviewOptions {
  verifiedPurchase: VerifiedPurchasePort
  moderation?: ModerationAdapter
}

export const REVIEW_BODY_MAX = 5000

function assertValidRating(rating: number): asserts rating is ReviewRating {
  if (!Number.isInteger(rating) || rating < 1 || rating > 5) {
    throw new ReviewValidationError('rating')
  }
}

function assertValidBody(body: string | null | undefined): void {
  if (body != null && body.length > REVIEW_BODY_MAX) {
    throw new ReviewValidationError('body')
  }
}

function assertImmutableUnchanged(field: ImmutableField, provided: unknown, existing: unknown): void {
  if (provided !== undefined && provided !== existing) {
    throw new ImmutableFieldError(field)
  }
}

async function resolveStatus(
  body: string | null | undefined,
  moderation?: ModerationAdapter,
): Promise<ReviewStatus> {
  if (!moderation) {
    return 'pending'
  }

  const result = await moderation.score(body ?? '')
  if (result.action === 'approve') {
    return 'approved'
  }
  if (result.action === 'reject') {
    return 'rejected'
  }
  return 'pending'
}

export async function submitReview(
  q: Querier<ReviewsSchema>,
  input: SubmitReviewInput,
  opts: SubmitReviewOptions,
): Promise<Review> {
  assertValidRating(input.rating)
  assertValidBody(input.body)
  if (!input.purchaseId) throw new ReviewValidationError('purchaseId')

  if (!opts.verifiedPurchase) throw new ReviewsConfigError('verifiedPurchase port required')
  const purchased = await opts.verifiedPurchase.hasPurchased(input.userId, input.productId, input.purchaseId)
  if (!purchased) {
    throw new NotPurchasedError(input.userId, input.productId)
  }

  const status = await resolveStatus(input.body, opts?.moderation)
  const now = new Date()
  const body = input.body ?? null
  const vendorId = input.vendorId

  const [existing] = await q
    .select()
    .from(review)
    .where(and(eq(review.userId, input.userId), eq(review.productId, input.productId), eq(review.purchaseId, input.purchaseId)))
    .limit(1)

  if (existing) {
    assertImmutableUnchanged('userId', input.userId, existing.userId)
    assertImmutableUnchanged('productId', input.productId, existing.productId)
    assertImmutableUnchanged('vendorId', vendorId, existing.vendorId)
    assertImmutableUnchanged('purchaseId', input.purchaseId, existing.purchaseId)
  }

  const [row] = await q
    .insert(review)
    .values({
      productId: input.productId,
      userId: input.userId,
      vendorId,
      purchaseId: input.purchaseId,
      rating: input.rating,
      body,
      status,
      createdAt: now,
      updatedAt: now,
    })
    .onConflictDoUpdate({
      target: [review.userId, review.productId, review.purchaseId],
      set: {
        rating: input.rating,
        body,
        status,
        updatedAt: now,
      },
    })
    .returning()

  if (!row) {
    throw new ReviewValidationError('insert')
  }

  return rowToReview(row)
}
