/**
 * Image moderation cascade helpers.
 *
 * CASCADE_REGISTRY: per-purpose side effects run when an image is approved or rejected.
 * markImageApproved / markImageFlagged / rejectImageWithCascade: public API
 * consumed by dispatch.ts (AI queue) and image-moderation.ts (human review).
 *
 * Design rules:
 * - No emails on IMAGE_APPROVAL outcomes (non-blocking images).
 * - Only blocking-image rejections get outbox rows (vendor_hero handled by
 *   existing outbox handler; vendor_logo / avatar are blocking and use outbox).
 * - Non-blocking images (gallery, user_gallery, support_attachment) get
 *   approvalStatus updated but NO notification outbox row.
 * - adminActions rows use action='APPROVE'/'REJECT', targetType='IMAGE',
 *   adminId=null (AI_AGENT).
 */

import { eq } from 'drizzle-orm';
import { imageUploads } from '@/server/db/schema.js';
import { resolveImageNotifications } from '@/server/db/queries/notifications.js';
import { recordAdminAction } from '@/server/db/queries/admin-actions.js';
import {
  markImageFlaggedForAdminReview,
  markImageRejected,
  setImageApprovalStatus,
} from '@/server/db/queries/image-uploads.js';
import { insertOutboxEvent } from '@/server/db/queries/outbox.js';
import type { DrizzleClient } from '@/server/db/client.js';
import {
  approveAvatar,
  approveUserGallery,
  approveVendorGallery,
  approveVendorLogo,
  flagSupportAttachment,
  rejectAvatar,
  rejectUserGallery,
  rejectVendorGallery,
  rejectVendorLogo,
} from '@/server/db/queries/admin-resources/image-moderation.js';

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

export interface CascadeCtx {
  imageId: string;
  purpose: string;
  entityType: string | null;
  entityId: string | null;
  reason: string;
  uploaderUserId: string | null;
  uploaderVendorId: string | null;
}

export interface CascadePreview {
  entityRowsAffected: string[];
  publicFallback: string;
  ownerNotified: boolean;
  emailSent: false; // always false for IMAGE_APPROVAL
}

export interface CascadeEntry {
  execute: (db: DrizzleClient, ctx: CascadeCtx) => Promise<void>;
  promote: (db: DrizzleClient, ctx: CascadeCtx) => Promise<void>;
  preview: (ctx: CascadeCtx) => CascadePreview;
  i18nKey: string;
  fallbackComponent: 'gradient' | 'initial-letter' | 'omit' | 'default-icon' | 'hidden' | 'none';
}

// ─── Helpers ──────────────────────────────────────────────────────────────────

const noop = async () => {};

const noPreview = (): CascadePreview => ({
  entityRowsAffected: [],
  publicFallback: 'cascade_preview_none',
  ownerNotified: false,
  emailSent: false,
});

// ─── CASCADE_REGISTRY ─────────────────────────────────────────────────────────

export const CASCADE_REGISTRY: Record<string, CascadeEntry> = {
  vendor_hero: {
    // vendor_hero is handled by the vendor.hero.review outbox handler (existing flow).
    // This entry is a no-op for AI approval — the hero flow has its own state machine.
    execute: noop,
    promote: noop,
    preview: () => ({
      entityRowsAffected: [],
      publicFallback: 'cascade_preview_vendor_hero_fallback',
      ownerNotified: false,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_vendor_hero',
    fallbackComponent: 'gradient',
  },

  vendor_logo: {
    execute: async (db, ctx) => {
      if (!ctx.entityId) return;
      await rejectVendorLogo(db, ctx.entityId);
    },
    promote: async (db, ctx) => {
      if (!ctx.entityId) return;
      // Copy pending_logo_url → logo_url (column-to-column via sql helper)
      await approveVendorLogo(db, ctx.entityId);
    },
    preview: () => ({
      entityRowsAffected: [
        'vendors.logo_approval_status → REJECTED',
        'vendors.pending_logo_url → NULL',
      ],
      publicFallback: 'cascade_preview_vendor_logo_fallback',
      ownerNotified: true,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_vendor_logo',
    fallbackComponent: 'initial-letter',
  },

  vendor_gallery: {
    execute: async (db, ctx) => {
      if (!ctx.entityId) return;
      await rejectVendorGallery(db, ctx.entityId, ctx.reason);
    },
    promote: async (db, ctx) => {
      if (!ctx.entityId) return;
      await approveVendorGallery(db, ctx.entityId);
    },
    preview: () => ({
      entityRowsAffected: ['vendor_gallery_images.approval_status → REJECTED'],
      publicFallback: 'cascade_preview_gallery_omit',
      ownerNotified: false,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_vendor_gallery',
    fallbackComponent: 'omit',
  },

  user_gallery: {
    execute: async (db, ctx) => {
      if (!ctx.entityId) return;
      await rejectUserGallery(db, ctx.entityId, ctx.reason);
    },
    promote: async (db, ctx) => {
      if (!ctx.entityId) return;
      await approveUserGallery(db, ctx.entityId);
    },
    preview: () => ({
      entityRowsAffected: ['user_gallery_images.approval_status → REJECTED'],
      publicFallback: 'cascade_preview_gallery_omit',
      ownerNotified: false,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_user_gallery',
    fallbackComponent: 'omit',
  },

  avatar: {
    execute: async (db, ctx) => {
      if (!ctx.entityId) return;
      await rejectAvatar(db, ctx.entityId, ctx.reason);
    },
    promote: async (db, ctx) => {
      if (!ctx.entityId) return;
      // Promote pending avatar to live avatar value
      await approveAvatar(db, ctx.entityId);
    },
    preview: () => ({
      entityRowsAffected: ['users.avatar_approval_status → REJECTED'],
      publicFallback: 'cascade_preview_avatar_default',
      ownerNotified: true,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_avatar',
    fallbackComponent: 'default-icon',
  },

  support_attachment: {
    execute: async (db, ctx) => {
      // Flag the attachment as abusive by cfImageId lookup via entityId.
      // entityId is set to the support_attachment.id at upload-initiation time.
      if (!ctx.entityId) return;
      await flagSupportAttachment(db, ctx.entityId);
    },
    promote: noop,
    preview: () => ({
      entityRowsAffected: ['support_attachments.abuse_status → flagged'],
      publicFallback: 'cascade_preview_none',
      ownerNotified: false,
      emailSent: false,
    }),
    i18nKey: 'cascade_preview_support_attachment',
    fallbackComponent: 'hidden',
  },

  // Platform-controlled; bypassed before IMAGE_APPROVAL enqueue
  deal_image: {
    execute: noop,
    promote: noop,
    preview: noPreview,
    i18nKey: 'cascade_preview_deal_image_owned',
    fallbackComponent: 'none',
  },
  homepage_banner: {
    execute: noop,
    promote: noop,
    preview: noPreview,
    i18nKey: 'cascade_preview_homepage_banner_admin',
    fallbackComponent: 'none',
  },
};

// ─── Context loader ───────────────────────────────────────────────────────────

async function loadCtx(
  db: DrizzleClient,
  imageId: string,
  reasonOverride?: string | null,
): Promise<CascadeCtx> {
  const [row] = await db.select().from(imageUploads).where(eq(imageUploads.id, imageId)).limit(1);
  if (!row) throw new Error(`image_uploads row not found: ${imageId}`);
  return {
    imageId,
    purpose: row.purpose ?? '',
    entityType: row.entityType,
    entityId: row.entityId,
    reason: reasonOverride ?? row.aiReason ?? 'unspecified',
    uploaderUserId: row.uploaderUserId,
    uploaderVendorId: row.uploaderVendorId,
  };
}

// ─── Public API ───────────────────────────────────────────────────────────────

/**
 * AI PASS outcome: set approvalStatus=APPROVED, promote via registry, resolve notifications.
 * Writes an adminActions row with action=APPROVE, adminId=null (AI_AGENT).
 */
export async function markImageApproved(db: DrizzleClient, imageId: string): Promise<void> {
  const ctx = await loadCtx(db, imageId);

  await setImageApprovalStatus(db, imageId, 'APPROVED');

  const entry = CASCADE_REGISTRY[ctx.purpose];
  if (entry) {
    await entry.promote(db, ctx);
  }

  if (ctx.entityType && ctx.entityId) {
    await resolveImageNotifications(db, {
      entityType: ctx.entityType,
      entityId: ctx.entityId,
      purpose: ctx.purpose,
    });
  }

  await recordAdminAction(db, {
    adminId: null,
    action: 'APPROVE',
    targetType: 'IMAGE',
    targetId: imageId,
    note: 'AI PASS',
  });
}

/**
 * AI FLAG outcome: image stays PENDING approval, surfaces in admin inbox.
 * No cascade — human will decide.
 */
export async function markImageFlagged(
  db: DrizzleClient,
  imageId: string,
  reason?: string | null,
): Promise<void> {
  // Update aiDecision to FLAG so inbox query surfaces it
  // approvalStatus stays PENDING — human review required
  await markImageFlaggedForAdminReview(
    db,
    imageId,
    reason ?? 'Flagged by AI — requires human review',
  );
}

/**
 * AI REJECT outcome (or human reject): cascade side effects, write outbox for notification.
 * Writes an adminActions row with action=REJECT, adminId=null (AI_AGENT).
 * Outbox row triggers notification to owner via image.rejected handler.
 */
export async function rejectImageWithCascade(
  db: DrizzleClient,
  imageId: string,
  reasonOverride?: string | null,
): Promise<void> {
  const ctx = await loadCtx(db, imageId, reasonOverride);
  const entry = CASCADE_REGISTRY[ctx.purpose];

  if (!entry) {
    // Unknown purpose — just mark rejected, surface in admin log
    await markImageRejected(db, imageId, ctx.reason);
    return;
  }

  await markImageRejected(db, imageId, ctx.reason);

  // Apply per-purpose entity cascade
  await entry.execute(db, ctx);

  // Audit log
  await recordAdminAction(db, {
    adminId: null,
    action: 'REJECT',
    targetType: 'IMAGE',
    targetId: imageId,
    note: ctx.reason,
  });

  // Notification outbox (only if uploader is identifiable)
  const ownerType = ctx.uploaderVendorId ? 'vendor' : 'user';
  const ownerId = ctx.uploaderVendorId ?? ctx.uploaderUserId;
  if (ownerId && ctx.entityType && ctx.entityId) {
    await insertOutboxEvent(db, {
      aggregateType: ownerType,
      aggregateId: ownerId,
      eventType: 'image.rejected',
      payload: {
        imageId,
        ownerType,
        ownerId,
        entityType: ctx.entityType,
        entityId: ctx.entityId,
        purpose: ctx.purpose,
        reason: ctx.reason,
      },
    });
  }
}

/**
 * Compute a deep-link href for the image replacement flow based on purpose.
 * Used by the image.rejected outbox handler to populate notification payload.
 */
export function replaceHrefFor(purpose: string, entityId: string | null): string {
  switch (purpose) {
    case 'vendor_logo':
      return '/vendor/profile#logo';
    case 'vendor_hero':
      return '/vendor/profile#hero';
    case 'vendor_gallery':
      return `/vendor/gallery${entityId ? `?highlight=${entityId}` : ''}`;
    case 'user_gallery':
      return `/profile/gallery${entityId ? `?highlight=${entityId}` : ''}`;
    case 'avatar':
      return '/profile#avatar';
    case 'support_attachment':
      return '/support';
    default:
      return '/';
  }
}
