/**
 * Real ImageApprovalAgent - Gemini 2.0 Flash vision implementation.
 *
 * Two flows:
 * - reviewSearchEngineImage: checks that the image is relevant to the deal
 *   title/category and passes content policy. Returns approve/reject + confidence.
 * - reviewUploadedImage: screens vendor/user uploads for policy violations.
 *   Returns auto_approve if clearly safe, needs_review otherwise.
 *
 * On any Gemini error both methods fail safe:
 *   - reviewSearchEngineImage → reject (confidence 0) - don't auto-approve on error
 *   - reviewUploadedImage     → needs_review (confidence 0) - defer to human
 *
 * UploadImageModerationAgent:
 * - Queue-driven agent for LLM_JOBS IMAGE_APPROVAL flow.
 * - Instantiate via static create(provider, creds, model) — model resolved by caller.
 * - evaluate() takes raw ArrayBuffer + mime, returns PASS | FLAG | REJECT.
 */

import { requireConfiguredLlmModel } from '../model-config.js';
import { withSentry } from '@/server/observability/with-sentry';
import { captureCaught } from '@/server/observability/capture.server';
import { safeFetchImage } from '@/server/security/safe-fetch-image.js';
import { env } from '@/server/env.js';
import type { LlmProvider } from '../providers/types.js';
import type { AiCredentials } from '../credentials.js';
import type {
  ImageApprovalAgent,
  SearchEngineImageReviewInput,
  SearchEngineImageReviewResult,
  UploadedImageReviewInput,
  UploadedImageReviewResult,
} from '../types.js';

/** Max image body size we will fetch for Gemini (10 MB). */
const MAX_IMAGE_BYTES = 10 * 1024 * 1024;

/**
 * Fetch an image from a URL and return raw bytes + mimeType.
 * Throws if the fetch fails or the body is too large.
 */
async function fetchImageBytes(
  imageUrl: string,
): Promise<{ imageBytes: ArrayBuffer; mimeType: string }> {
  return safeFetchImage(imageUrl, {
    siteUrl: env.PUBLIC_SITE_URL,
    maxBytes: MAX_IMAGE_BYTES,
  });
}

/** Parse a raw Gemini text response for a numeric confidence value (0-1). */
function parseConfidence(raw: string): number {
  const match = raw.match(/confidence[:\s]+([0-9.]+)/i);
  if (match?.[1]) {
    const val = parseFloat(match[1]);
    if (!isNaN(val)) return Math.min(1, Math.max(0, val));
  }
  return 0.5; // default mid-confidence when not provided
}

export class GeminiImageApprovalAgent implements ImageApprovalAgent {
  private readonly provider: LlmProvider;
  private readonly creds: AiCredentials;
  private readonly model: string;

  constructor(provider: LlmProvider, creds: AiCredentials, model: string) {
    this.provider = provider;
    this.creds = creds;
    this.model = requireConfiguredLlmModel(model, 'image approval');
    this.reviewSearchEngineImage = withSentry(this.reviewSearchEngineImage.bind(this), {
      name: 'ai.image-approval.search-engine',
      kind: 'ai-agent',
    });
    this.reviewUploadedImage = withSentry(this.reviewUploadedImage.bind(this), {
      name: 'ai.image-approval.uploaded',
      kind: 'ai-agent',
    });
    this.reviewVendorHeroImage = withSentry(this.reviewVendorHeroImage.bind(this), {
      name: 'ai.image-approval.vendor-hero',
      kind: 'ai-agent',
    });
  }

  reviewSearchEngineImage: (
    input: SearchEngineImageReviewInput,
  ) => Promise<SearchEngineImageReviewResult> = async (input) => {
    const { imageUrl, dealTitle, category } = input;

    const prompt = `You are a content moderator for Multideal - a Hebrew marketplace that offers discounted deals from local Israeli businesses.

Deal title: "${dealTitle}"
Category: "${category}"

Evaluate whether this image is appropriate and relevant for the deal listing above.

APPROVE if:
- The image clearly depicts a product, food item, service, experience, or business premises relevant to the deal title and category.
- Professional or amateur quality photos are both acceptable.
- Minor branding, watermarks, or Hebrew text overlays are fine.

REJECT if:
- The image contains nudity, explicit sexual content, graphic violence, or hate symbols.
- The image is clearly unrelated to the deal (e.g., a random stock photo of something completely different).
- The image is predominantly spam text, QR codes, or promotional overlays unrelated to the product.
- The image shows prohibited items (weapons, drugs, alcohol without appropriate context).

Respond with exactly this format (two lines):
DECISION: APPROVE or REJECT
CONFIDENCE: a number between 0.0 and 1.0
REASON: one short sentence explaining your decision`;

    try {
      const { imageBytes, mimeType } = await fetchImageBytes(imageUrl);
      const { text: raw } = await this.provider.generateTextWithImage(this.creds, {
        model: this.model,
        prompt,
        imageBytes,
        mime: mimeType,
      });

      const upper = raw.toUpperCase();
      const decision: SearchEngineImageReviewResult['decision'] = upper.includes(
        'DECISION: APPROVE',
      )
        ? 'approve'
        : 'reject';

      const confidence = parseConfidence(raw);

      // Extract reason after "REASON:" if present
      const reasonMatch = raw.match(/REASON:\s*(.+)/i);
      const reason = reasonMatch?.[1]?.trim();

      return { decision, confidence, reason };
    } catch (err) {
      captureCaught(err, { scope: 'server.ai.agents.image-approval', severity: 'warning' });
      // Fail safe: reject on error so no bad images slip through automatically
      return {
        decision: 'reject',
        confidence: 0,
        reason: 'Image review unavailable - deferred to human moderation',
      };
    }
  };

  reviewUploadedImage: (input: UploadedImageReviewInput) => Promise<UploadedImageReviewResult> =
    async (input) => {
      const { imageUrl, dealTitle, uploaderType } = input;

      const prompt = `You are a strict content moderator for Multideal - a Hebrew marketplace for local Israeli businesses.

This image was uploaded by a ${uploaderType} for the deal: "${dealTitle}".

Screen the image for policy violations. Respond with exactly this format:
DECISION: AUTO_APPROVE or NEEDS_REVIEW
CONFIDENCE: a number between 0.0 and 1.0
FLAGS: comma-separated list of concerns (or "none")

AUTO_APPROVE only if ALL of the following are true:
- No nudity, sexual content, graphic violence, or hate symbols.
- No personal identifying information (faces shown only if expected for the deal type).
- No embedded QR codes unrelated to the product.
- Clearly depicts a product, service, food, or business location.
- No prohibited items (weapons, drugs, illicit substances).

NEEDS_REVIEW if there is any doubt, ambiguity, or if any of the above conditions may be violated.`;

      try {
        const { imageBytes, mimeType } = await fetchImageBytes(imageUrl);
        const { text: raw } = await this.provider.generateTextWithImage(this.creds, {
          model: this.model,
          prompt,
          imageBytes,
          mime: mimeType,
        });

        const upper = raw.toUpperCase();
        const decision: UploadedImageReviewResult['decision'] = upper.includes(
          'DECISION: AUTO_APPROVE',
        )
          ? 'auto_approve'
          : 'needs_review';

        const confidence = parseConfidence(raw);

        // Parse flags
        const flagsMatch = raw.match(/FLAGS:\s*(.+)/i);
        const flagsRaw = flagsMatch?.[1]?.trim() ?? '';
        const flags =
          flagsRaw.toLowerCase() === 'none' || !flagsRaw
            ? []
            : flagsRaw
                .split(',')
                .map((f) => f.trim())
                .filter(Boolean);

        return { decision, confidence, flags };
      } catch (err) {
        captureCaught(err, { scope: 'server.ai.agents.image-approval', severity: 'warning' });
        // Fail safe: always require human review on error
        return {
          decision: 'needs_review',
          confidence: 0,
          flags: ['Image review unavailable - deferred to human moderation'],
        };
      }
    };

  reviewVendorHeroImage: (imageUrl: string) => Promise<{
    decision: 'APPROVED' | 'REJECTED' | 'NEEDS_REVIEW';
    rejectReasonCode?:
      | 'PHONE_NUMBER'
      | 'EMAIL'
      | 'QR_CODE'
      | 'COMPETITOR_BRAND'
      | 'TEXT_HEAVY'
      | 'TOO_DARK'
      | 'POLICY_VIOLATION';
    confidence: number;
  }> = async (imageUrl) => {
    const prompt = `You are a strict content moderator for Multideal, a Hebrew marketplace for local Israeli businesses.

Screen this vendor hero banner image. Respond with EXACTLY this format (no extra text):
DECISION: APPROVED or REJECTED or NEEDS_REVIEW
REASON_CODE: one of PHONE_NUMBER | EMAIL | QR_CODE | COMPETITOR_BRAND | TEXT_HEAVY | TOO_DARK | POLICY_VIOLATION | NONE
CONFIDENCE: 0.0 to 1.0

APPROVED: clearly a relevant business/product/service photo with no violations.
NEEDS_REVIEW: ambiguous — not clearly safe, not clearly a violation.
REJECTED if ANY of the following:
- Visible phone number → REASON_CODE: PHONE_NUMBER
- Visible email address → REASON_CODE: EMAIL
- QR code present → REASON_CODE: QR_CODE
- Competitor brand logo/watermark → REASON_CODE: COMPETITOR_BRAND
- More than 70% of image is text → REASON_CODE: TEXT_HEAVY
- Image predominantly dark (under 15% luminance) → REASON_CODE: TOO_DARK
- Nudity / explicit / graphic content → REASON_CODE: POLICY_VIOLATION

On APPROVED or NEEDS_REVIEW, set REASON_CODE: NONE`;

    try {
      const { imageBytes, mimeType } = await fetchImageBytes(imageUrl);
      const { text: raw } = await this.provider.generateTextWithImage(this.creds, {
        model: this.model,
        prompt,
        imageBytes,
        mime: mimeType,
      });
      const upper = raw.toUpperCase();

      let decision: 'APPROVED' | 'REJECTED' | 'NEEDS_REVIEW' = 'NEEDS_REVIEW';
      if (upper.includes('DECISION: APPROVED')) decision = 'APPROVED';
      else if (upper.includes('DECISION: REJECTED')) decision = 'REJECTED';

      const validCodes = [
        'PHONE_NUMBER',
        'EMAIL',
        'QR_CODE',
        'COMPETITOR_BRAND',
        'TEXT_HEAVY',
        'TOO_DARK',
        'POLICY_VIOLATION',
      ] as const;
      type RejectCode = (typeof validCodes)[number];
      const codeMatch = raw.match(/REASON_CODE:\s*(\w+)/i);
      const codeRaw = codeMatch?.[1]?.toUpperCase() ?? '';
      const rejectReasonCode = validCodes.includes(codeRaw as RejectCode)
        ? (codeRaw as RejectCode)
        : undefined;

      return { decision, rejectReasonCode, confidence: parseConfidence(raw) };
    } catch (err) {
      captureCaught(err, { scope: 'server.ai.agents.image-approval', severity: 'warning' });
      return { decision: 'NEEDS_REVIEW', confidence: 0 };
    }
  };
}

// ─── UploadImageModerationAgent ───────────────────────────────────────────────

export type ImageModerationDecision = 'PASS' | 'FLAG' | 'REJECT';

export interface ImageModerationResult {
  decision: ImageModerationDecision;
  reason: string | null;
  score: number | null;
  rawText: string;
  modelName: string;
}

/**
 * Queue-driven image moderation agent for the IMAGE_APPROVAL LLM job type.
 *
 * Unlike GeminiImageApprovalAgent (URL-fetch + inline prompt), this agent:
 * - Takes raw image bytes (already fetched from R2 by the caller).
 * - Returns a PASS | FLAG | REJECT decision for persistence to image_uploads.
 *
 * Instantiate via the static create(provider, creds, model) factory.
 * Model is resolved by the caller (ImageApprovalKind via resolveQueueForJobType).
 * Fails safe: evaluate() returns FLAG on any error (defer to human).
 */
export class UploadImageModerationAgent {
  private readonly provider: LlmProvider;
  private readonly creds: AiCredentials;
  private readonly model: string;

  /** Populated after a successful LLM call; null after a fallback/error. */
  public lastUsage: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    costUsd: number | null;
  } | null = null;

  private constructor(provider: LlmProvider, creds: AiCredentials, model: string) {
    this.provider = provider;
    this.creds = creds;
    this.model = model;
  }

  /**
   * Factory: construct from already-resolved provider/creds/model.
   * Used by ImageApprovalKind which gets provider/model from resolveQueueForJobType.
   */
  static create(
    provider: LlmProvider,
    creds: AiCredentials,
    model: string,
  ): UploadImageModerationAgent {
    return new UploadImageModerationAgent(provider, creds, model);
  }

  /**
   * Evaluate raw image bytes against Multideal content policy.
   *
   * @param imageBytes - Raw image ArrayBuffer from R2.
   * @param mime       - MIME type (e.g. 'image/jpeg').
   */
  async evaluate(imageBytes: ArrayBuffer, mime: string): Promise<ImageModerationResult> {
    const prompt = `אתה מנחה תוכן אוטומטי של מולטידיל - מרקטפלייס ישראלי לעסקאות מקומיות.

בדוק את התמונה שהועלתה על פי מדיניות התוכן.

ענה בדיוק בפורמט הבא (ללא טקסט נוסף):
DECISION: PASS או FLAG או REJECT
SCORE: מספר בין 0.0 ל-1.0 (ביטחון בהחלטה)
REASON: משפט קצר המסביר את ההחלטה (באנגלית, עד 200 תווים)

PASS: תמונה תקינה - מוצר, שירות, מזון, עסק או פנים אם רלוונטי לסוג העסקה.
FLAG: ספק - אין הפרה ברורה אך נדרש בדיקת אדם.
REJECT אם אחד מהבאים:
- תוכן מיני מפורש או עירום
- אלימות גרפית, נשק, דם
- סמלי שנאה, גזענות
- מספר טלפון, כתובת אימייל, קוד QR שלא קשורים למוצר
- פריטים אסורים (סמים, נשק)
- ספאם טקסטואלי - יותר מ-70% מהתמונה הוא טקסט שיווקי`;

    try {
      const {
        text: raw,
        usage,
        costUsd,
        model,
      } = await this.provider.generateTextWithImage(this.creds, {
        model: this.model,
        prompt,
        imageBytes,
        mime,
        kind: 'IMAGE_APPROVAL',
      });
      this.lastUsage = {
        promptTokens: usage.promptTokens,
        completionTokens: usage.completionTokens,
        totalTokens: usage.totalTokens,
        costUsd,
      };

      const resolvedModel = model ?? this.model;
      return {
        decision: parseImageModerationDecision(raw),
        reason: parseImageModerationReason(raw),
        score: parseConfidence(raw),
        rawText: raw,
        modelName: resolvedModel,
      };
    } catch (err) {
      this.lastUsage = null;
      captureCaught(err, {
        scope: 'server.ai.agents.image-approval.queue',
        severity: 'warning',
      });
      // Fail safe: FLAG on error — defer to human, don't auto-reject
      return {
        decision: 'FLAG',
        reason: 'Image review unavailable - deferred to human moderation',
        score: 0,
        rawText: '',
        modelName: this.model,
      };
    }
  }
}

/** Parse DECISION from raw Gemini text: PASS | FLAG | REJECT. Defaults to FLAG. */
function parseImageModerationDecision(raw: string): ImageModerationDecision {
  const upper = raw.toUpperCase();
  if (upper.includes('DECISION: PASS')) return 'PASS';
  if (upper.includes('DECISION: REJECT')) return 'REJECT';
  return 'FLAG'; // default: human review
}

/** Parse REASON line from raw Gemini text. */
function parseImageModerationReason(raw: string): string | null {
  const match = raw.match(/REASON:\s*(.+)/i);
  return match?.[1]?.trim().slice(0, 200) ?? null;
}
