/**
 * Admin deal approval workflows.
 *
 * - listPendingDeals: paginated list of PENDING_APPROVAL deals, oldest first.
 * - approveDeal: set deal_state=ACTIVE, write admin_actions audit row, send email.
 * - rejectDeal: set deal_state=REJECTED, store reason+detail, write audit row, send email.
 *
 * No raw SQL. All inputs are zod-validated at the API boundary before reaching here.
 * PII is never logged.
 */

import { eq, asc, count, desc, and, ilike, inArray, sql, or } from 'drizzle-orm';
void or;
import type { DrizzleClient } from '@/server/db/client.js';
import { sendDealApproved, sendDealRejected } from '@/server/services/email';
import { type ResendEnv } from '@/server/services/email';
import type { PushEnv } from '@/server/push/send.js';
import type { DoClient } from '@/server/services/types.js';
import { deals, vendors, llmJobs, dealTranslations } from '@/server/db/schema.js';
import * as dealQueries from '@/server/db/queries/deals.js';
import { recordAdminAction } from '@/server/db/queries/admin-actions.js';
import { approveDealImagesForDeal } from '@/server/db/queries/deal-images.js';
import { activateVendorOnFirstApproval } from '@/server/db/queries/vendors.js';
import { captureCaught } from '@/server/observability/capture.server';
import { enqueueDealTranslation } from '@/server/translation/jobs/enqueue.js';
import { pinSourceSlug } from '@/server/translation/fields/_helpers.js';

// ─── Types ────────────────────────────────────────────────────────────────────

export interface DealApprovalDeps {
  db: DrizzleClient;
  doClient: DoClient;
  email: ResendEnv;
  /** Optional push env — when provided, favoriting users are notified on approval. */
  push?: PushEnv;
}

export type RejectionReason =
  | 'WRONG_PRICE'
  | 'MISSING_BAD_IMAGE'
  | 'CATEGORY_MISMATCH'
  | 'CONTENT_POLICY'
  | 'UNCLEAR_DESCRIPTION'
  | 'OTHER';

// ─── listDeals ────────────────────────────────────────────────────────────────

export interface ListDealsOptions {
  limit: number;
  offset: number;
  /** Filter by deal state. When omitted, all states are returned. */
  state?: string;
  /** Substring match on vendor display name (case-insensitive). */
  vendorSearch?: string;
  /** Substring match on deal title (case-insensitive). */
  search?: string;
}

export interface PendingDeal {
  id: string;
  vendorId: string;
  vendorName: string;
  dealType: string;
  title: string;
  description: string;
  categoryId: string | null;
  originalPrice: string;
  discountPercent: number;
  discountedPrice: string;
  quantityTotal: number;
  windowStart: Date | null;
  windowEnd: Date | null;
  commissionRate: string;
  isPersonalDeal: boolean;
  personalDealForUserId: string | null;
  pickupAddress: string;
  specialInstructions: string | null;
  createdAt: Date;
  /** LLM flag reason - populated when the deal was flagged by AI moderation. */
  llmFlagReason: string | null;
  /** Status of any vendor appeal on this deal. */
  appealStatus: string | null;
  /** Current deal state. */
  dealState: string;
}

export async function listDeals(
  db: DrizzleClient,
  options: ListDealsOptions,
): Promise<{ data: PendingDeal[]; total: number }> {
  const { limit, offset, state, vendorSearch, search } = options;

  // Build where conditions
  const conditions = [];
  if (state) {
    // Cast state string to the enum type at query level
    conditions.push(sql`${deals.dealState} = ${state}::deal_state`);
  }
  if (vendorSearch) {
    conditions.push(ilike(vendors.displayName, `%${vendorSearch}%`));
  }
  if (search) {
    conditions.push(ilike(deals.title, `%${search}%`));
  }
  const whereClause = conditions.length > 0 ? and(...conditions) : undefined;

  const [rows, [countRow]] = await Promise.all([
    db
      .select({
        id: deals.id,
        vendorId: deals.vendorId,
        vendorName: vendors.displayName,
        dealType: deals.dealType,
        title: deals.title,
        description: deals.description,
        categoryId: deals.categoryId,
        originalPrice: sql<string>`(SELECT original_price FROM deal_skus WHERE deal_id = ${deals.id} AND option_ids_hash = 'default' LIMIT 1)`,
        discountPercent: sql<number>`(SELECT discount_percent FROM deal_skus WHERE deal_id = ${deals.id} AND option_ids_hash = 'default' LIMIT 1)`,
        discountedPrice: sql<string>`(SELECT discounted_price FROM deal_skus WHERE deal_id = ${deals.id} AND option_ids_hash = 'default' LIMIT 1)`,
        quantityTotal: sql<number>`(SELECT quantity_total FROM deal_skus WHERE deal_id = ${deals.id} AND option_ids_hash = 'default' LIMIT 1)`,
        windowStart: deals.windowStart,
        windowEnd: deals.windowEnd,
        commissionRate: deals.commissionRate,
        isPersonalDeal: deals.isPersonalDeal,
        personalDealForUserId: deals.personalDealForUserId,
        pickupAddress: deals.pickupAddress,
        specialInstructions: deals.specialInstructions,
        createdAt: deals.createdAt,
        appealStatus: deals.appealStatus,
        dealState: deals.dealState,
      })
      .from(deals)
      .innerJoin(vendors, eq(deals.vendorId, vendors.id))
      .where(whereClause)
      .orderBy(asc(deals.createdAt))
      .limit(limit)
      .offset(offset),
    whereClause
      ? db
          .select({ count: count() })
          .from(deals)
          .innerJoin(vendors, eq(deals.vendorId, vendors.id))
          .where(whereClause)
      : db.select({ count: count() }).from(deals),
  ]);

  // Fetch LLM flag reasons for all returned deals in one query
  const dealIds = rows.map((r) => r.id);
  const llmReasons = new Map<string, string>();
  if (dealIds.length > 0) {
    // Most recent job per deal in one query (DISTINCT ON target_id, latest first)
    const jobRows = await db
      .selectDistinctOn([llmJobs.targetId], {
        targetId: llmJobs.targetId,
        flagReason: llmJobs.flagReason,
      })
      .from(llmJobs)
      .where(inArray(llmJobs.targetId, dealIds))
      .orderBy(llmJobs.targetId, desc(llmJobs.createdAt));
    for (const jr of jobRows) {
      if (jr.flagReason) llmReasons.set(jr.targetId, jr.flagReason);
    }
  }

  const data: PendingDeal[] = rows.map((r) => ({
    ...r,
    llmFlagReason: llmReasons.get(r.id) ?? null,
    appealStatus: r.appealStatus ?? null,
  }));

  return { data, total: countRow?.count ?? 0 };
}

// ─── listPendingDeals ─────────────────────────────────────────────────────────

export interface ListPendingDealsOptions {
  limit: number;
  offset: number;
}

export async function listPendingDeals(
  db: DrizzleClient,
  options: ListPendingDealsOptions,
): Promise<{ data: PendingDeal[]; total: number }> {
  return listDeals(db, { ...options, state: 'PENDING_APPROVAL' });
}

// ─── approveDeal ──────────────────────────────────────────────────────────────

export interface ApproveDealInput {
  /** Null when approved by AI_AGENT. */
  adminId: string | null;
  dealId: string;
  note?: string;
}

export async function approveDeal(deps: DealApprovalDeps, input: ApproveDealInput): Promise<void> {
  const { db } = deps;
  const { adminId, dealId, note } = input;

  // Load deal - verify it exists and is in the right state
  const [deal] = await db
    .select({
      id: deals.id,
      dealState: deals.dealState,
      vendorId: deals.vendorId,
      title: deals.title,
      description: deals.description,
      sourceLanguage: deals.sourceLanguage,
    })
    .from(deals)
    .where(eq(deals.id, dealId))
    .limit(1);

  if (!deal) {
    throw new DealApprovalError('DEAL_NOT_FOUND', 'Deal not found');
  }
  // Admin can approve from UNDER_REVIEW (LLM moderation in-flight) or PENDING_APPROVAL
  // (LLM completed → human queue). Approving from UNDER_REVIEW overrides pending LLM job.
  if (deal.dealState !== 'PENDING_APPROVAL' && deal.dealState !== 'UNDER_REVIEW') {
    throw new DealApprovalError('INVALID_STATE', `Cannot approve deal in state: ${deal.dealState}`);
  }

  const now = new Date();

  // Update deal state — use query layer so alarm is armed
  await dealQueries.approveDeal(
    db,
    { doClient: deps.doClient },
    dealId,
    adminId ? 'HUMAN' : 'AI_AGENT',
  );
  void now; // now is no longer used directly; approveDeal sets approvedAt internally

  // Activate vendor on first approval: PENDING_FIRST_APPROVAL → ACTIVE.
  // Without this the vendor's deals are filtered out of the public storefront.
  await activateVendorOnFirstApproval(db, deal.vendorId);

  // Enqueue translation jobs for all active target locales (best-effort, non-fatal)
  try {
    await enqueueDealTranslation(db, dealId);
  } catch (err) {
    captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'warning' });
    // Non-fatal: translation enqueue failure should not roll back the approval
  }

  // Create source-locale deal_translations row with slug (best-effort, non-fatal)
  try {
    await pinSourceSlug(db, dealId, deal.title ?? '', deal.description ?? '', deal.sourceLanguage);
  } catch (err) {
    captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'warning' });
  }

  // Approve all associated images - images stay in sync with deal approval
  await approveDealImagesForDeal(db, dealId);

  // Write audit row
  await recordAdminAction(db, {
    adminId,
    targetType: 'DEAL',
    targetId: dealId,
    action: 'APPROVE',
    note: note ?? null,
  });

  // Resolve HE slug for vendor notification URLs
  const [heSlugRow] = await db
    .select({ slug: dealTranslations.slug })
    .from(dealTranslations)
    .where(and(eq(dealTranslations.dealId, deal.id), eq(dealTranslations.locale, 'he')))
    .limit(1);
  const dealCanonicalUrl = heSlugRow?.slug
    ? `https://multideal.co.il/deals/${heSlugRow.slug}`
    : 'https://multideal.co.il/deals';
  const dealCanonicalPath = heSlugRow?.slug ? `/deals/${heSlugRow.slug}` : '/deals';

  // Send email to vendor (best-effort - do not throw on failure)
  try {
    const [vendor] = await db
      .select({ id: vendors.id, email: vendors.email, displayName: vendors.displayName })
      .from(vendors)
      .where(eq(vendors.id, deal.vendorId))
      .limit(1);

    if (vendor?.email) {
      await sendDealApproved(deps.email, {
        to: vendor.email,
        dealTitle: deal.title,
        dealUrl: dealCanonicalUrl,
        vendorName: vendor.displayName,
      });
    }
  } catch (err) {
    captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'warning' });
    // Non-fatal: email failure should not roll back the approval
  }

  // Notify users who favorited this vendor (best-effort, non-fatal)
  if (deps.push) {
    try {
      const { getUsersToNotifyForVendor } = await import('../db/queries/favorite-vendors.js');
      const userIds = await getUsersToNotifyForVendor(db, deal.vendorId);
      if (userIds.length > 0) {
        const { sendToUser } = await import('../push/send.js');
        for (const userId of userIds) {
          await sendToUser(
            db,
            deps.push,
            userId,
            {
              title: 'New deal from a vendor you follow!',
              body: deal.title,
              url: dealCanonicalPath,
              tag: `vendor-new-deal-${deal.id}`,
              data: {
                eventType: 'FAVORITE_VENDOR_NEW_DEAL',
                dealId: deal.id,
                vendorId: deal.vendorId,
              },
            },
            'reminder_new_deals',
          ).catch((err) => {
            captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'info' });
          });
        }
      }
    } catch (err) {
      captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'warning' });
      // Non-fatal
    }
  }
}

// ─── rejectDeal ───────────────────────────────────────────────────────────────

export interface RejectDealInput {
  adminId: string;
  dealId: string;
  reason: RejectionReason;
  detail?: string;
}

export async function rejectDeal(deps: DealApprovalDeps, input: RejectDealInput): Promise<void> {
  const { db } = deps;
  const { adminId, dealId, reason, detail } = input;

  const [deal] = await db
    .select({
      id: deals.id,
      dealState: deals.dealState,
      vendorId: deals.vendorId,
      title: deals.title,
    })
    .from(deals)
    .where(eq(deals.id, dealId))
    .limit(1);

  if (!deal) {
    throw new DealApprovalError('DEAL_NOT_FOUND', 'Deal not found');
  }
  // Mirror approve: admin can reject from UNDER_REVIEW or PENDING_APPROVAL.
  if (deal.dealState !== 'PENDING_APPROVAL' && deal.dealState !== 'UNDER_REVIEW') {
    throw new DealApprovalError('INVALID_STATE', `Cannot reject deal in state: ${deal.dealState}`);
  }

  // Update deal state — use query layer so alarm is disarmed
  await dealQueries.cancelDeal(db, { doClient: deps.doClient }, dealId, reason);
  if (detail) {
    await dealQueries.setDealRejectionDetail(db, dealId, detail);
  }

  await recordAdminAction(db, {
    adminId,
    targetType: 'DEAL',
    targetId: dealId,
    action: 'REJECT',
    note: detail ?? reason,
  });

  // Send email to vendor (best-effort)
  try {
    const [vendor] = await db
      .select({ id: vendors.id, email: vendors.email, displayName: vendors.displayName })
      .from(vendors)
      .where(eq(vendors.id, deal.vendorId))
      .limit(1);

    if (vendor?.email) {
      await sendDealRejected(deps.email, {
        to: vendor.email,
        dealTitle: deal.title,
        rejectionReason: reason,
        rejectionDetail: detail,
        dashboardUrl: 'https://multideal.co.il/vendor/dashboard',
        vendorName: vendor.displayName,
      });
    }
  } catch (err) {
    captureCaught(err, { scope: 'server.admin.deal-approval', severity: 'warning' });
    // Non-fatal
  }
}

// ─── Error ────────────────────────────────────────────────────────────────────

export class DealApprovalError extends Error {
  constructor(
    public readonly code: 'DEAL_NOT_FOUND' | 'INVALID_STATE',
    message: string,
  ) {
    super(message);
    this.name = 'DealApprovalError';
  }
}
