/**
 * VendorViolationKind — JobKind plugin for VENDOR_VIOLATION LLM jobs.
 *
 * Strangler-fig: registered in PR5. Behavior-faithful port of
 * processVendorAdmissionJob (llm.ts) + approveVendorAdmission/
 * flagVendorForHumanReview/rejectVendor (dispatch.ts).
 *
 * IMPORTANT: Despite the jobType name "VENDOR_VIOLATION", this is actually
 * the vendor ADMISSION review (new vendors in PENDING state). The legacy code
 * calls GeminiVendorViolationFlagAgent.review() — the same class handles both
 * violation flagging (flag()) and admission review (review()). This kind
 * wraps the admission path, guarded by account_state='PENDING'.
 *
 * Verdict mapping (faithful to legacy):
 *   APPROVE → account_state=PENDING_PROCESSOR, llm_decision=APPROVE, clear flag/reject reasons
 *   FLAG    → llm_decision=FLAG, flag_reason=rationale (no state change)
 *   REJECT  → account_state=REJECTED, llm_decision=REJECT, reject_reason=rationale
 *
 * applyVerdict: all writes in single tx with status-guarded select.
 * onRetryExhausted: insert outbox event vendor.review_required (vendor stays PENDING).
 */
import { z } from 'zod';
import { eq, and, sql } from 'drizzle-orm';
import type { DrizzleDb } from '@/server/db/client.js';
import { vendors, outbox } from '@/server/db/schema.js';
import { GeminiVendorViolationFlagAgent } from '@/server/ai/agents/vendor-violation-flag.js';
import type { JobKind, JobRunDeps, LlmJobRow, JobCompletionMeta } from './types.js';
import { insertOutboxRow } from '@/server/db/queries/outbox.js';
import * as vendorQ from '@/server/db/queries/ai/vendor-violation.js';

// ─── Input / Result / Verdict shapes ─────────────────────────────────────────

export interface VendorViolationInput {
  vendorId: string;
  businessName: string;
  ownerEmail: string;
  /** ISO 8601 phone country prefix ('+972' for Israeli platform default). */
  phoneCountryPrefix: string;
  createdAt: Date;
}

export interface VendorViolationResult {
  decision: 'APPROVE' | 'FLAG' | 'REJECT';
  rationale: string;
  modelName: string;
  promptTokens: number | null;
  completionTokens: number | null;
  totalTokens: number | null;
  costUsd: number | null;
}

export type VendorViolationVerdict = VendorViolationResult;

const VendorViolationResultSchema = z.object({
  decision: z.enum(['APPROVE', 'FLAG', 'REJECT']),
  rationale: z.string(),
  modelName: z.string(),
  promptTokens: z.number().nullable().optional(),
  completionTokens: z.number().nullable().optional(),
  totalTokens: z.number().nullable().optional(),
  costUsd: z.number().nullable().optional(),
});

// ─── Kind implementation ──────────────────────────────────────────────────────

export const vendorViolationKind: JobKind<
  VendorViolationInput,
  VendorViolationResult,
  VendorViolationVerdict
> = {
  jobType: 'VENDOR_VIOLATION',

  // ── loadInput ─────────────────────────────────────────────────────────────

  async loadInput(deps: JobRunDeps, jobRow: LlmJobRow): Promise<VendorViolationInput> {
    const vendorId = jobRow.targetId;
    if (!vendorId) throw new Error('VENDOR_VIOLATION job missing targetId');

    const [vendor] = await deps.db
      .select({
        id: vendors.id,
        businessName: vendors.businessName,
        email: vendors.email,
        phone: vendors.phone,
        accountState: vendors.accountState,
        createdAt: vendors.createdAt,
      })
      .from(vendors)
      .where(eq(vendors.id, vendorId))
      .limit(1);

    if (!vendor) throw new Error(`VENDOR_VIOLATION: vendor ${vendorId} not found`);
    if (vendor.accountState !== 'PENDING_FIRST_APPROVAL') {
      throw new Error(
        `VENDOR_VIOLATION: vendor ${vendorId} not in PENDING_FIRST_APPROVAL state (is ${vendor.accountState})`,
      );
    }

    // Phone is stored encrypted. Platform default is +972 (Israeli).
    // Phase 2G can read the raw prefix from inputPayload if stored at enqueue time.
    const payload = (jobRow.inputPayload ?? {}) as Record<string, unknown>;
    const phoneCountryPrefix = (payload.phoneCountryPrefix as string | undefined) ?? '+972';

    return {
      vendorId: vendor.id,
      businessName: vendor.businessName,
      ownerEmail: vendor.email,
      phoneCountryPrefix,
      createdAt: vendor.createdAt,
    };
  },

  // ── runProvider ───────────────────────────────────────────────────────────

  async runProvider(
    deps: JobRunDeps,
    _jobRow: LlmJobRow,
    input: VendorViolationInput,
  ): Promise<VendorViolationResult> {
    const agent = new GeminiVendorViolationFlagAgent(deps.provider, deps.creds, deps.model);

    const result = await agent.review({
      vendorId: input.vendorId,
      businessName: input.businessName,
      ownerEmail: input.ownerEmail,
      phoneCountryPrefix: input.phoneCountryPrefix,
      createdAt: input.createdAt,
    });

    const usage = agent.lastUsage;

    return {
      decision: result.decision,
      rationale: result.rationale,
      modelName: usage?.modelName ?? deps.model,
      promptTokens: usage?.promptTokens ?? null,
      completionTokens: usage?.completionTokens ?? null,
      totalTokens: usage?.totalTokens ?? null,
      costUsd: usage?.costUsd ?? null,
    };
  },

  // ── hydrateResult ─────────────────────────────────────────────────────────

  hydrateResult(jobRow: LlmJobRow): VendorViolationVerdict {
    const parsed = VendorViolationResultSchema.safeParse(jobRow.outputPayload);
    if (!parsed.success) {
      throw new Error(`VENDOR_VIOLATION: cannot hydrate output_payload: ${parsed.error.message}`);
    }
    const data = parsed.data;
    return {
      ...data,
      promptTokens: data.promptTokens ?? null,
      completionTokens: data.completionTokens ?? null,
      totalTokens: data.totalTokens ?? null,
      costUsd: data.costUsd ?? null,
    };
  },

  // ── applyVerdict ──────────────────────────────────────────────────────────

  async applyVerdict(
    deps: JobRunDeps,
    jobRow: LlmJobRow,
    result: VendorViolationVerdict,
  ): Promise<{ applied: boolean }> {
    const vendorId = jobRow.targetId;

    return deps.db.transaction(async (tx) => vendorQ.applyVerdict(tx, vendorId, result));
  },

  // ── onRetryExhausted ──────────────────────────────────────────────────────

  async onRetryExhausted(db: DrizzleDb, jobRow: LlmJobRow, reason: string): Promise<void> {
    const vendorId = jobRow.targetId;

    // Vendor stays PENDING — insert outbox event for admin to handle manually.
    // Job-scoped idempotency: same job retried (Step-2) shares jobRow.id, so we
    // emit exactly one notification per failure episode. A future re-flag is a
    // new job id → not suppressed. Outbox rows are retained, so a vendor-wide
    // key would wrongly suppress legit re-flags.
    await db.transaction(async (tx) => {
      const existing = await tx
        .select({ id: outbox.id })
        .from(outbox)
        .where(
          and(
            eq(outbox.eventType, 'vendor.review_required'),
            sql`${outbox.payload}->>'jobId' = ${jobRow.id}`,
          ),
        )
        .limit(1);

      if (existing.length > 0) return;

      await insertOutboxRow(tx, {
        aggregateType: 'vendor',
        aggregateId: vendorId,
        eventType: 'vendor.review_required',
        payload: { vendorId, reason, jobId: jobRow.id },
      });
    });
  },

  extractCompletionMeta(result: unknown): JobCompletionMeta {
    const r = result as VendorViolationResult;
    return {
      modelName: r.modelName,
      promptTokens: r.promptTokens ?? undefined,
      completionTokens: r.completionTokens ?? undefined,
      totalTokens: r.totalTokens ?? undefined,
      costUsd: r.costUsd != null ? r.costUsd.toFixed(6) : undefined,
    };
  },
};
