// Encrypted attachment upload / download.
//
// Per-message symmetric key + nonce encrypts the raw blob (XSalsa20-Poly1305
// via secretbox). The encrypted blob is uploaded to Supabase Storage under
// `{conversation_id}/{attachment_id}.bin`. The symmetric key + blob-nonce
// travel inside the per-device message envelope as JSON, so the server never
// sees the decryption material.

import { fromBase64, getCryptoBackend, toBase64 } from '../crypto/index.js';
import type { AppSupabaseClient } from '../supabase/client.js';

export const ATTACHMENT_BUCKET = 'chat-attachments';
export const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10 MB

// The per-attachment envelope material + public metadata. The base64 fields
// live in the encrypted message payload; the storage path + mime live on
// the public message_attachments row.
export interface AttachmentHandle {
  id: string; // matches message_attachments.id + storage sub-path
  storagePath: string;
  mimeType: string;
  sizeBytes: number;
  width?: number;
  height?: number;
  // base64-encoded — only readable via per-device envelope decrypt.
  keyB64: string;
  nonceB64: string;
}

export type CallEventStatus = 'ended' | 'missed' | 'declined';
export type CallEventKind = 'audio' | 'video';

// Plaintext payload wire format: either text+attachments or a compact
// call-event record. Pre-attachment messages without JSON auto-upgrade
// via the fallback branch in parseMessagePayload.

export interface TextMessagePayload {
  v: 1;
  type?: 'text';
  text: string;
  attachments: AttachmentHandle[];
  /** When set, the message self-destructs `expireMs` milliseconds after the
   *  server's createdAt. Sender initiates the delete; receivers hide locally
   *  as soon as the window elapses. Added in /tempmsg support. */
  expireMs?: number;
}

export interface CallEventPayload {
  v: 1;
  type: 'call_event';
  status: CallEventStatus;
  mediaKind: CallEventKind;
  durationSec: number;
}

export interface PollOption {
  id: string;
  emoji: string;
  text: string;
}

export interface PollPayload {
  v: 1;
  type: 'poll';
  question: string;
  options: PollOption[];
}

export type MessagePayload = TextMessagePayload | CallEventPayload | PollPayload;

export type ParsedMessagePayload =
  | {
      kind: 'text';
      text: string;
      attachments: AttachmentHandle[];
      expireMs?: number;
    }
  | {
      kind: 'call_event';
      status: CallEventStatus;
      mediaKind: CallEventKind;
      durationSec: number;
    }
  | {
      kind: 'poll';
      question: string;
      options: PollOption[];
    };

export function serializeMessagePayload(payload: MessagePayload): string {
  if (
    (!('type' in payload) || payload.type === 'text' || payload.type === undefined) &&
    'attachments' in payload &&
    payload.attachments.length === 0
  ) {
    return payload.text;
  }
  return JSON.stringify(payload);
}

export function parseMessagePayload(raw: string | null): ParsedMessagePayload {
  if (!raw) return { kind: 'text', text: '', attachments: [] };
  if (!raw.startsWith('{')) return { kind: 'text', text: raw, attachments: [] };
  try {
    const obj = JSON.parse(raw) as Partial<MessagePayload> & { type?: string };
    if (obj && obj.v === 1) {
      if (obj.type === 'call_event') {
        const p = obj as CallEventPayload;
        return {
          kind: 'call_event',
          status: p.status,
          mediaKind: p.mediaKind,
          durationSec: typeof p.durationSec === 'number' ? p.durationSec : 0,
        };
      }
      if (obj.type === 'poll') {
        const p = obj as Partial<PollPayload>;
        const options = Array.isArray(p.options)
          ? p.options
              .map((option, idx) => {
                const candidate = option as Partial<PollOption>;
                const text = typeof candidate.text === 'string' ? candidate.text.trim() : '';
                if (!text) return null;
                return {
                  id:
                    typeof candidate.id === 'string' && candidate.id.trim()
                      ? candidate.id.trim()
                      : 'option-' + (idx + 1),
                  emoji:
                    typeof candidate.emoji === 'string' && candidate.emoji.trim()
                      ? candidate.emoji.trim()
                      : String(idx + 1),
                  text,
                };
              })
              .filter((option): option is PollOption => option !== null)
          : [];
        return {
          kind: 'poll',
          question: typeof p.question === 'string' ? p.question.trim() : '',
          options,
        };
      }
      const t = obj as TextMessagePayload;
      return {
        kind: 'text',
        text: typeof t.text === 'string' ? t.text : '',
        attachments: Array.isArray(t.attachments) ? t.attachments : [],
        ...(typeof t.expireMs === 'number' && t.expireMs > 0
          ? { expireMs: t.expireMs }
          : {}),
      };
    }
  } catch {
    /* fall through to plain text */
  }
  return { kind: 'text', text: raw, attachments: [] };
}

function ensureUuid(): string {
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
    return crypto.randomUUID();
  }
  throw new Error('crypto.randomUUID unavailable');
}

export interface EncryptedAttachmentResult {
  handle: AttachmentHandle;
  key: Uint8Array; // raw bytes — caller is responsible for wiping
  nonce: Uint8Array;
}

// Encrypt + upload a blob. Does NOT insert the message_attachments row —
// the caller combines this with a message insert so everything commits
// atomically at the application layer.
export async function encryptAndUploadAttachment(params: {
  client: AppSupabaseClient;
  conversationId: string;
  file: Blob;
  mimeType: string;
  sizeBytes: number;
  width?: number;
  height?: number;
}): Promise<EncryptedAttachmentResult> {
  const backend = getCryptoBackend();

  const bytes = new Uint8Array(await params.file.arrayBuffer());
  const key = backend.randomBytes(backend.secretboxKeyLength);
  const nonce = backend.randomBytes(backend.secretboxNonceLength);
  const ciphertext = backend.secretbox(bytes, nonce, key);

  const id = ensureUuid();
  const storagePath = params.conversationId + '/' + id + '.bin';

  const { error } = await params.client.storage
    .from(ATTACHMENT_BUCKET)
    .upload(storagePath, ciphertext, {
      contentType: 'application/octet-stream',
      upsert: false,
    });
  if (error) throw error;

  const handle: AttachmentHandle = {
    id,
    storagePath,
    mimeType: params.mimeType,
    sizeBytes: params.sizeBytes,
    ...(params.width !== undefined ? { width: params.width } : {}),
    ...(params.height !== undefined ? { height: params.height } : {}),
    keyB64: await toBase64(key),
    nonceB64: await toBase64(nonce),
  };

  return { handle, key, nonce };
}

// Download + decrypt an attachment blob and return a Blob the caller can use
// with URL.createObjectURL. Throws on network or authentication failures.
export async function downloadAndDecryptAttachment(params: {
  client: AppSupabaseClient;
  handle: AttachmentHandle;
}): Promise<Blob> {
  const backend = getCryptoBackend();

  const { data, error } = await params.client.storage
    .from(ATTACHMENT_BUCKET)
    .download(params.handle.storagePath);
  if (error) throw error;
  if (!data) throw new Error('empty download');

  const ciphertext = new Uint8Array(await data.arrayBuffer());
  const key = await fromBase64(params.handle.keyB64);
  const nonce = await fromBase64(params.handle.nonceB64);
  const plainBytes = backend.secretboxOpen(ciphertext, nonce, key);
  // Zero key/nonce buffers on the way out.
  for (let i = 0; i < key.length; i++) key[i] = 0;
  for (let i = 0; i < nonce.length; i++) nonce[i] = 0;
  // Copy into a fresh ArrayBuffer so Blob accepts it across TS lib variants.
  const copy = new Uint8Array(plainBytes.byteLength);
  copy.set(plainBytes);
  return new Blob([copy.buffer], { type: params.handle.mimeType });
}

// Insert the public metadata row for an attachment. The ciphertext itself has
// already been uploaded to storage under `handle.storagePath`.
export async function insertAttachmentRow(
  client: AppSupabaseClient,
  messageId: string,
  handle: AttachmentHandle,
  blobNonceHex: string,
): Promise<void> {
  const row: Record<string, unknown> = {
    id: handle.id,
    message_id: messageId,
    storage_path: handle.storagePath,
    nonce: blobNonceHex,
    mime_type: handle.mimeType,
    size_bytes: handle.sizeBytes,
  };
  if (handle.width !== undefined) row.width = handle.width;
  if (handle.height !== undefined) row.height = handle.height;
  const { error } = await client.from('message_attachments').insert(row as never);
  if (error) throw error;
}
