import { createDbService } from '@/server/services/db.js';
/**
 * Support attachment helpers.
 *
 * - createAttachmentRecord: inserts a row in support_attachments once
 *   CF Images has ingested the R2 object (called from the upload completion
 *   flow for support_attachment purpose).
 * - loadAttachmentWithParent: loads an attachment row alongside the owner +
 *   vendor id of its parent ticket or case, used by the authz endpoint.
 * - signReadUrl: builds the CF Images delivery URL for a variant. CF Images
 *   signed-URLs require a signing token produced by the account API; for
 *   phase 2 we return the canonical delivery URL (hash-scoped, non-enumerable)
 *   — signing is layered on by phase 3 once private-delivery is turned on for
 *   the account.
 */

import { eq } from 'drizzle-orm';
import { bytesToHex } from '@/lib/encoding.js';
import {
  supportAttachments,
  supportTickets,
  transactionCases,
  returns,
  vendors,
} from '@/server/db/schema';
import { captureCaught } from '@/lib/observability';
import type { SupportAttachmentView } from '@/server/support/types';
import { asCaseId } from '@/server/platform-seams/ids.js';
import * as attachmentQ from '@/server/db/queries/support-attachments.js';

// ─── HMAC token helpers ───────────────────────────────────────────────────────

const TOKEN_TTL_SECONDS = 300; // 5 minutes

async function hmacSign(data: string, secret: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  );
  const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(data));
  return bytesToHex(sig);
}

async function hmacVerify(data: string, sig: string, secret: string): Promise<boolean> {
  const expected = await hmacSign(data, secret);
  if (expected.length !== sig.length) return false;
  // Constant-time compare
  let diff = 0;
  for (let i = 0; i < expected.length; i++) {
    diff |= expected.charCodeAt(i) ^ sig.charCodeAt(i);
  }
  return diff === 0;
}

export async function createAttachmentToken(
  attachmentId: string,
  variant: string,
  viewerId: string,
  secret: string,
): Promise<{ token: string; exp: number }> {
  const exp = Math.floor(Date.now() / 1000) + TOKEN_TTL_SECONDS;
  const data = `${attachmentId}:${variant}:${viewerId}:${exp}`;
  const sig = await hmacSign(data, secret);
  const token = Buffer.from(JSON.stringify({ attachmentId, variant, viewerId, exp, sig })).toString(
    'base64url',
  );
  return { token, exp };
}

export async function verifyAttachmentToken(
  raw: string,
  secret: string,
): Promise<{ attachmentId: string; variant: string; viewerId: string } | null> {
  try {
    const parsed = JSON.parse(Buffer.from(raw, 'base64url').toString('utf-8')) as {
      attachmentId: string;
      variant: string;
      viewerId: string;
      exp: number;
      sig: string;
    };
    if (Math.floor(Date.now() / 1000) > parsed.exp) return null;
    const data = `${parsed.attachmentId}:${parsed.variant}:${parsed.viewerId}:${parsed.exp}`;
    const valid = await hmacVerify(data, parsed.sig, secret);
    if (!valid) return null;
    return {
      attachmentId: parsed.attachmentId,
      variant: parsed.variant,
      viewerId: parsed.viewerId,
    };
  } catch (err) {
    captureCaught(err, {
      scope: 'server.support.attachments.verifyAttachmentToken',
      severity: 'info',
    });
    return null;
  }
}

export interface CreateAttachmentInput {
  parentType: 'ticket' | 'case' | 'return' | 'pending_return';
  parentId: string;
  messageId: string | null;
  cfImageId: string;
  variants: { thumb: string; card: string; full: string };
  mime: string;
  sizeBytes: number;
  uploaderId: string;
  uploaderRole: 'customer' | 'vendor';
}

export async function createAttachmentRecord(
  env: { DATABASE_URL: string },
  input: CreateAttachmentInput,
): Promise<string> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const row = await attachmentQ.insert(db, {
    parentType: input.parentType,
    parentId: input.parentId,
    messageId: input.messageId,
    cfImageId: input.cfImageId,
    variants: input.variants,
    mime: input.mime,
    sizeBytes: input.sizeBytes,
    uploaderId: input.uploaderId,
    uploaderRole: input.uploaderRole,
    abuseStatus: 'pending',
  });
  return row.id;
}

export interface LoadedAttachment {
  attachment: SupportAttachmentView;
  parentOwnerId: string;
  parentVendorId: string | null;
}

export async function loadAttachmentWithParent(
  attachmentId: string,
  env: { DATABASE_URL: string },
): Promise<LoadedAttachment | null> {
  const db = createDbService({ DATABASE_URL: env.DATABASE_URL });
  const [row] = await db
    .select()
    .from(supportAttachments)
    .where(eq(supportAttachments.id, attachmentId))
    .limit(1);
  if (!row) return null;

  let parentOwnerId: string;
  let parentVendorId: string | null = null;

  if (row.parentType === 'ticket') {
    const [t] = await db
      .select({ openerId: supportTickets.openerId })
      .from(supportTickets)
      .where(eq(supportTickets.id, row.parentId))
      .limit(1);
    if (!t) return null;
    parentOwnerId = t.openerId;
  } else if (row.parentType === 'case') {
    const [c] = await db
      .select({ customerId: transactionCases.customerId, vendorId: transactionCases.vendorId })
      .from(transactionCases)
      .where(eq(transactionCases.id, asCaseId(row.parentId)))
      .limit(1);
    if (!c) return null;
    parentOwnerId = c.customerId;
    parentVendorId = c.vendorId;
  } else if (row.parentType === 'return') {
    const [r] = await db
      .select({ buyerId: returns.buyerId, vendorId: returns.vendorId })
      .from(returns)
      .where(eq(returns.id, row.parentId))
      .limit(1);
    if (!r) return null;
    parentOwnerId = r.buyerId;
    // Resolve vendor's ownerUserId for vendor-side authz
    const [v] = await db
      .select({ ownerUserId: vendors.ownerUserId })
      .from(vendors)
      .where(eq(vendors.id, r.vendorId))
      .limit(1);
    parentVendorId = v?.ownerUserId ?? null;
  } else {
    // parentType === 'pending_return': no parent row exists yet.
    // Authz is uploaderId === viewerId only (enforced by caller).
    parentOwnerId = row.uploaderId;
  }

  const view: SupportAttachmentView = {
    id: row.id,
    parentType: row.parentType,
    parentId: row.parentId,
    messageId: row.messageId ?? null,
    cfImageId: row.cfImageId,
    variants: row.variants,
    mime: row.mime,
    sizeBytes: row.sizeBytes,
    uploaderId: row.uploaderId,
    uploaderRole: row.uploaderRole,
    abuseStatus: row.abuseStatus,
    createdAt: row.createdAt.toISOString(),
  };
  return { attachment: view, parentOwnerId, parentVendorId };
}

/**
 * Returns a short-lived HMAC-signed proxy URL for attachment delivery.
 *
 * The proxy endpoint (GET /api/support/attachments/[id]/file?token=...) validates
 * the token and redirects to the CF Images URL. This avoids exposing the permanent
 * CF Images hash-addressed URL directly and ensures access is time-bounded (5 min).
 *
 * Requires QR_SECRET and PUBLIC_SITE_URL — fails closed when signing is unavailable.
 */
export async function signReadUrl(
  attachmentId: string,
  variant: 'thumb' | 'card' | 'full',
  viewerId: string,
  env: { DATABASE_URL: string; PUBLIC_SITE_URL?: string; QR_SECRET?: string },
): Promise<string> {
  if (!env.QR_SECRET || !env.PUBLIC_SITE_URL) {
    throw new Error('Attachment signing unavailable');
  }

  const { token } = await createAttachmentToken(attachmentId, variant, viewerId, env.QR_SECRET);
  const proxyUrl = new URL(`/api/support/attachments/${attachmentId}/file`, env.PUBLIC_SITE_URL);
  proxyUrl.searchParams.set('token', token);
  return proxyUrl.toString();
}
