/**
 * Review workflows - FDS §6.9.
 *
 * Decider/executor split:
 *   - domain/review/machine.ts    pure validation + effect emission
 *   - domain/review/apply-effects.ts   executes DB/push/queue effects
 *   - this file                   loads context, runs decider, applies effects, manages AI prescore
 */

import { eq } from 'drizzle-orm';
import type { DrizzleClient } from '@/server/db/client.js';
import type { PushClient } from '@/server/push/types.js';
import * as reviewQueries from '@/server/db/queries/reviews.js';
import * as purchaseQueries from '@/server/db/queries/purchases.js';
import { reviews } from '@/server/db/schema.js';
import { prescoreViaAi } from '@/server/admin/resources/review-moderation/actions.js';
import type { ReviewRemovalDeps } from '@/server/admin/resources/review-moderation/types.js';
import { getAiAgents, type Env as AiEnv } from '@/server/ai/registry.js';
import { getSystemConfigCached } from '@/server/db/queries/system-config.js';
import { captureCaught } from '@/server/observability/capture.server';
import {
  decide,
  type ReviewContext,
  type PurchaseContext,
} from '@/server/domain/review/machine.js';
import { applyEffects } from '@/server/domain/review/apply-effects.js';

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------

export interface ReviewDeps {
  db: DrizzleClient;
  push: PushClient;
  aiEnv?: AiEnv;
}

export interface SubmitStandardReviewInput {
  purchaseId: string;
  userId: string;
  rating: number;
  body: string;
}

export interface SubmitTechnicalReviewInput {
  purchaseId: string;
  userId: string;
  body: string;
}

export interface ReplyToReviewInput {
  reviewId: string;
  vendorId: string;
  reply: string;
}

export interface RequestReviewRemovalInput {
  reviewId: string;
  vendorId: string;
  reason: string;
}

// ---------------------------------------------------------------------------
// Typed errors
// ---------------------------------------------------------------------------

export class ReviewError extends Error {
  constructor(
    public readonly code:
      | 'PURCHASE_NOT_FOUND'
      | 'NOT_REVIEW_ELIGIBLE'
      | 'ALREADY_REVIEWED'
      | 'REVIEW_NOT_FOUND'
      | 'VENDOR_NOT_OWNER'
      | 'ALREADY_REMOVAL_REQUESTED',
    message: string,
  ) {
    super(message);
    this.name = 'ReviewError';
  }
}

// ---------------------------------------------------------------------------
// Helpers — context loading
// ---------------------------------------------------------------------------

async function loadPurchaseContext(
  db: DrizzleClient,
  purchaseId: string,
): Promise<PurchaseContext | null> {
  const purchase = await purchaseQueries.findById(db, purchaseId);
  if (!purchase) return null;

  return {
    id: purchase.id,
    userId: purchase.userId ?? '',
    vendorId: purchase.vendorId,
    dealId: purchase.dealId,
    reviewEligible: purchase.reviewEligible,
    redemptionStatus: purchase.redemptionStatus,
  };
}

async function existingReviewForPurchase(
  db: DrizzleClient,
  vendorId: string,
  purchaseId: string,
  userId: string,
  reviewType: 'STANDARD' | 'TECHNICAL',
  dealId?: string,
): Promise<boolean> {
  if (reviewType === 'STANDARD') {
    const all = await reviewQueries.listByVendor(db, vendorId);
    return all.some((r) => r.purchaseId === purchaseId && r.userId === userId);
  }
  const technical = await reviewQueries.listTechnicalByVendor(db, vendorId);
  return technical.some((r) => r.authorId === userId && (dealId == null || r.dealId === dealId));
}

async function loadReviewContext(
  db: DrizzleClient,
  reviewId: string,
): Promise<ReviewContext | null> {
  const [row] = await db.select().from(reviews).where(eq(reviews.id, reviewId)).limit(1);
  if (!row) return null;
  return {
    id: row.id,
    vendorId: row.vendorId,
    removalRequested: row.removalRequested ?? false,
  };
}

function rejectAsError(code: string, message: string): never {
  throw new ReviewError(code as ReviewError['code'], message);
}

// ---------------------------------------------------------------------------
// §6.9 - Standard review (after redemption)
// ---------------------------------------------------------------------------

export async function submitStandardReview(
  deps: ReviewDeps,
  input: SubmitStandardReviewInput,
): Promise<{ reviewId: string }> {
  const purchase = await loadPurchaseContext(deps.db, input.purchaseId);
  const existing =
    purchase !== null
      ? await existingReviewForPurchase(
          deps.db,
          purchase.vendorId,
          input.purchaseId,
          input.userId,
          'STANDARD',
        )
      : false;

  const result = decide(
    { kind: 'submit-standard', purchase, existingReviewForPurchase: existing },
    {
      kind: 'standard_review_submitted',
      purchaseId: input.purchaseId,
      userId: input.userId,
      vendorId: purchase?.vendorId ?? '',
      rating: input.rating,
      body: input.body,
      at: new Date(),
    },
  );

  if (!result.ok) rejectAsError(result.error, result.message);

  const applied = await applyEffects({ db: deps.db, push: deps.push }, result.effects);
  if (!applied.reviewId) throw new Error('insert-review effect did not produce a review id');
  return { reviewId: applied.reviewId };
}

// ---------------------------------------------------------------------------
// §6.9 - Technical review (after expiry / non-redemption)
// ---------------------------------------------------------------------------

export async function submitTechnicalReview(
  deps: ReviewDeps,
  input: SubmitTechnicalReviewInput,
): Promise<{ reviewId: string }> {
  const purchase = await loadPurchaseContext(deps.db, input.purchaseId);
  const existing =
    purchase !== null
      ? await existingReviewForPurchase(
          deps.db,
          purchase.vendorId,
          input.purchaseId,
          input.userId,
          'TECHNICAL',
          purchase.dealId,
        )
      : false;

  const result = decide(
    { kind: 'submit-technical', purchase, existingReviewForPurchase: existing },
    {
      kind: 'technical_review_submitted',
      purchaseId: input.purchaseId,
      userId: input.userId,
      vendorId: purchase?.vendorId ?? '',
      body: input.body,
      at: new Date(),
    },
  );

  if (!result.ok) rejectAsError(result.error, result.message);

  const applied = await applyEffects({ db: deps.db, push: deps.push }, result.effects);
  if (!applied.reviewId) throw new Error('insert-review effect did not produce a review id');
  return { reviewId: applied.reviewId };
}

// ---------------------------------------------------------------------------
// §6.9 - Vendor reply to a review
// ---------------------------------------------------------------------------

export async function replyToReview(deps: ReviewDeps, input: ReplyToReviewInput): Promise<void> {
  const review = await loadReviewContext(deps.db, input.reviewId);

  const result = decide(
    { kind: 'reply', review },
    {
      kind: 'reply_added',
      reviewId: input.reviewId,
      vendorId: input.vendorId,
      reply: input.reply,
      at: new Date(),
    },
  );

  if (!result.ok) rejectAsError(result.error, result.message);

  await applyEffects({ db: deps.db, push: deps.push }, result.effects);
}

// ---------------------------------------------------------------------------
// §6.9 - Vendor requests review removal
// ---------------------------------------------------------------------------

export async function requestReviewRemoval(
  deps: ReviewDeps,
  input: RequestReviewRemovalInput,
): Promise<void> {
  const review = await loadReviewContext(deps.db, input.reviewId);

  const result = decide(
    { kind: 'removal', review, aiEnabled: !!deps.aiEnv },
    {
      kind: 'removal_requested',
      reviewId: input.reviewId,
      vendorId: input.vendorId,
      reason: input.reason,
      at: new Date(),
    },
  );

  if (!result.ok) rejectAsError(result.error, result.message);

  await applyEffects(
    {
      db: deps.db,
      push: deps.push,
      schedulePrescore: (queueId) => {
        // Fire-and-forget AI prescoring - must not block or fail the vendor response.
        (async () => {
          try {
            const model = await getSystemConfigCached(deps.db, 'llm_model');
            const agents = getAiAgents(deps.aiEnv!, model);
            const aiDeps: ReviewRemovalDeps = {
              db: deps.db,
              email: { RESEND_API_KEY: '', RESEND_FROM_EMAIL: '' },
              aiReviewRemoval: agents.reviewRemoval,
            };
            await prescoreViaAi(aiDeps, queueId);
          } catch (err) {
            captureCaught(err, { scope: 'server.workflows.review', severity: 'warning' });
            // AI prescoring failure must never surface to the caller.
          }
        })();
      },
    },
    result.effects,
  );
}
