/**
 * Supabase Storage for ticket message attachments (images, voice notes, documents).
 * Max size aligns with storage-api FILE_SIZE_LIMIT (default 52428800 in docker-compose).
 */
import { createClient, type SupabaseClient } from '@supabase/supabase-js';
import { randomUUID } from 'crypto';
import type { Message, MessageContentType } from '@wa-ticketing/shared';
import { transcribeAudio, WHISPER_MAX_BYTES } from './openaiService.js';

const BUCKET = process.env.MEDIA_BUCKET ?? 'ticket-media';
const SIGNED_TTL_SEC = Number(process.env.MEDIA_SIGNED_URL_TTL_SEC ?? '3600');
/** Same default as storage service FILE_SIZE_LIMIT in docker-compose */
export const MEDIA_MAX_BYTES = Number(process.env.MEDIA_MAX_BYTES ?? '52428800');

const IMAGE_MIMES = new Set([
  'image/jpeg',
  'image/png',
  'image/webp',
  'image/gif',
]);
const VOICE_MIMES = new Set([
  'audio/ogg',
  'audio/opus',
  'audio/mp4',
  'audio/mpeg',
  'audio/webm',
  'application/ogg',
]);
const DOCUMENT_MIMES = new Set([
  'application/pdf',
  'application/msword',
  'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  'application/vnd.ms-excel',
  'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  'application/vnd.ms-powerpoint',
  'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  'text/plain',
  'text/csv',
  'application/zip',
  'application/x-zip-compressed',
  'application/octet-stream',
]);

export type TicketMediaKind = Extract<MessageContentType, 'image' | 'voice_note' | 'document'>;

let supabase: SupabaseClient | null = null;
let bucketEnsured = false;

function trimStorageBase(url: string): string {
  return url.trim().replace(/\/+$/, '');
}

/**
 * Signed URLs from the Supabase client use SUPABASE_URL (e.g. http://kong:8000 inside Docker).
 * Browsers must load media via SUPABASE_PUBLIC_URL (host/port or https URL they can reach).
 */
export function toBrowserSignedUrl(signedUrl: string): string {
  const internalBase = process.env.SUPABASE_URL?.trim()
    ? trimStorageBase(process.env.SUPABASE_URL)
    : '';
  const publicBase = trimStorageBase(
    process.env.SUPABASE_PUBLIC_URL?.trim() || process.env.SUPABASE_URL?.trim() || '',
  );
  if (!internalBase || !publicBase || internalBase === publicBase) return signedUrl;
  if (signedUrl.startsWith(internalBase)) {
    return publicBase + signedUrl.slice(internalBase.length);
  }
  return signedUrl;
}

function getSupabase(): SupabaseClient {
  const url = process.env.SUPABASE_URL?.trim();
  const key = process.env.SERVICE_ROLE_KEY?.trim();
  if (!url || !key) {
    throw Object.assign(new Error('SUPABASE_URL and SERVICE_ROLE_KEY are required for ticket media'), {
      statusCode: 503,
    });
  }
  if (!supabase) {
    supabase = createClient(url, key, { auth: { persistSession: false, autoRefreshToken: false } });
  }
  return supabase;
}

/** One-time bucket create (idempotent) for self-hosted stacks without SQL bucket seed. */
async function ensureBucket(): Promise<void> {
  if (bucketEnsured) return;
  const sb = getSupabase();
  const { data: list, error: listErr } = await sb.storage.listBuckets();
  if (listErr) {
    throw Object.assign(new Error(`Storage listBuckets: ${listErr.message}`), { statusCode: 503 });
  }
  if (list?.some(b => b.name === BUCKET)) {
    bucketEnsured = true;
    return;
  }
  const { error: createErr } = await sb.storage.createBucket(BUCKET, {
    public: false,
    fileSizeLimit: MEDIA_MAX_BYTES,
  });
  if (createErr && !String(createErr.message).includes('already exists')) {
    throw Object.assign(new Error(`Storage createBucket: ${createErr.message}`), { statusCode: 503 });
  }
  bucketEnsured = true;
}

/** Max files per multipart message (portal + staff + WhatsApp batch). */
export const MAX_MEDIA_FILES_PER_REQUEST = 10;

export function normalizeMime(mime: string): string {
  return mime.split(';')[0].trim().toLowerCase();
}

/** Infer image / voice / document from MIME (browser / portal uploads). */
export function inferMediaKindFromMime(mimeRaw: string): TicketMediaKind | null {
  const m = normalizeMime(mimeRaw);
  if (IMAGE_MIMES.has(m)) return 'image';
  if (VOICE_MIMES.has(m)) return 'voice_note';
  if (DOCUMENT_MIMES.has(m)) return 'document';
  return null;
}

/** Infer kind or 400 — use for multi-attach flows */
export function requireMediaKindFromMime(mimeRaw: string): TicketMediaKind {
  const k = inferMediaKindFromMime(mimeRaw);
  if (!k) {
    throw Object.assign(new Error(`Unsupported media type: ${normalizeMime(mimeRaw)}`), { statusCode: 400 });
  }
  return k;
}

/** Safe display / WhatsApp fileName (no path segments). */
export function sanitizeFileName(name: string): string {
  const base = name.replace(/[/\\]/g, '_').trim() || 'document';
  return base.slice(0, 200);
}

export function documentDisplayText(fileName: string, caption?: string | null): string {
  const cap = caption?.trim();
  if (cap) return cap;
  const fn = sanitizeFileName(fileName);
  return `[Document: ${fn}]`;
}

export function extFromMime(mime: string): string {
  const m = normalizeMime(mime);
  const map: Record<string, string> = {
    'image/jpeg': 'jpg',
    'image/png': 'png',
    'image/webp': 'webp',
    'image/gif': 'gif',
    'audio/ogg': 'ogg',
    'application/ogg': 'ogg',
    'audio/opus': 'ogg',
    'audio/mp4': 'm4a',
    'audio/mpeg': 'mp3',
    'audio/webm': 'webm',
    'application/pdf': 'pdf',
    'application/msword': 'doc',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
    'application/vnd.ms-excel': 'xls',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
    'application/vnd.ms-powerpoint': 'ppt',
    'application/vnd.openxmlformats-officedocument.presentationml.presentation': 'pptx',
    'text/plain': 'txt',
    'text/csv': 'csv',
    'application/zip': 'zip',
    'application/x-zip-compressed': 'zip',
  };
  return map[m] ?? 'bin';
}

export function assertWithinMaxBytes(size: number): void {
  if (size > MEDIA_MAX_BYTES) {
    throw Object.assign(new Error(`Media exceeds limit (${MEDIA_MAX_BYTES} bytes)`), { statusCode: 400 });
  }
}

/** Staff / API validation for outbound uploads */
export function validateStaffMime(mimeRaw: string, messageType: TicketMediaKind): void {
  const m = normalizeMime(mimeRaw);
  if (messageType === 'image' && !IMAGE_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported image type: ${m}`), { statusCode: 400 });
  }
  if (messageType === 'voice_note' && !VOICE_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported audio type: ${m}`), { statusCode: 400 });
  }
  if (messageType === 'document' && !DOCUMENT_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported document type: ${m}`), { statusCode: 400 });
  }
}

/** Inbound from WhatsApp: accept common types; still store what WA sent if in allow lists */
export function validateInboundMime(mimeRaw: string, messageType: TicketMediaKind): void {
  const m = normalizeMime(mimeRaw);
  if (messageType === 'image' && !IMAGE_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported inbound image type: ${m}`), { statusCode: 400 });
  }
  if (messageType === 'voice_note' && !VOICE_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported inbound audio type: ${m}`), { statusCode: 400 });
  }
  if (messageType === 'document' && !DOCUMENT_MIMES.has(m)) {
    throw Object.assign(new Error(`Unsupported inbound document type: ${m}`), { statusCode: 400 });
  }
}

export function decodeBase64ToBuffer(b64: string): Buffer {
  const buf = Buffer.from(b64, 'base64');
  if (!buf.length) throw Object.assign(new Error('Empty media payload'), { statusCode: 400 });
  assertWithinMaxBytes(buf.length);
  return buf;
}

export async function uploadTicketObject(params: {
  ticketId: string;
  buffer: Buffer;
  mime: string;
  messageType: TicketMediaKind;
}): Promise<{ path: string; sizeBytes: number }> {
  assertWithinMaxBytes(params.buffer.length);
  await ensureBucket();
  const ext = extFromMime(params.mime);
  const path = `tickets/${params.ticketId}/${randomUUID()}.${ext}`;
  const sb = getSupabase();
  const { error } = await sb.storage.from(BUCKET).upload(path, params.buffer, {
    contentType: normalizeMime(params.mime),
    upsert: false,
  });
  if (error) {
    throw Object.assign(new Error(`Storage upload failed: ${error.message}`), { statusCode: 503 });
  }
  return { path, sizeBytes: params.buffer.length };
}

export async function createSignedUrlForPath(storagePath: string | null): Promise<string | null> {
  if (!storagePath?.trim()) return null;
  await ensureBucket();
  const sb = getSupabase();
  const { data, error } = await sb.storage.from(BUCKET).createSignedUrl(storagePath, SIGNED_TTL_SEC);
  if (error || !data?.signedUrl) return null;
  return toBrowserSignedUrl(data.signedUrl);
}

/** Max bytes to inline as data-URL for vision models (keeps LLM payloads bounded). */
export const VISION_INLINE_MAX_BYTES = Number(process.env.VISION_INLINE_MAX_BYTES ?? '4194304');

/** Download a stored object for server-side use (e.g. multimodal talkback). */
export async function downloadTicketObject(
  storagePath: string,
): Promise<{ buffer: Buffer; mime: string } | null> {
  if (!storagePath?.trim()) return null;
  await ensureBucket();
  const sb = getSupabase();
  const { data, error } = await sb.storage.from(BUCKET).download(storagePath);
  if (error || !data) return null;
  const ab = await data.arrayBuffer();
  const buffer = Buffer.from(ab);
  const mime = (data.type && data.type !== 'application/octet-stream')
    ? normalizeMime(data.type)
    : normalizeMime(extMimeGuess(storagePath));
  return { buffer, mime };
}

function extMimeGuess(path: string): string {
  const ext = path.split('.').pop()?.toLowerCase() ?? '';
  if (ext === 'jpg' || ext === 'jpeg') return 'image/jpeg';
  if (ext === 'png') return 'image/png';
  if (ext === 'webp') return 'image/webp';
  if (ext === 'gif') return 'image/gif';
  if (ext === 'ogg') return 'audio/ogg';
  if (ext === 'opus') return 'audio/opus';
  if (ext === 'm4a') return 'audio/mp4';
  if (ext === 'mp3') return 'audio/mpeg';
  if (ext === 'webm') return 'audio/webm';
  return 'application/octet-stream';
}

/** data: URL for vision APIs, or null when missing / too large. */
export async function ticketImageAsDataUrl(storagePath: string | null): Promise<string | null> {
  if (!storagePath) return null;
  const obj = await downloadTicketObject(storagePath);
  if (!obj || !obj.mime.startsWith('image/')) return null;
  if (obj.buffer.length > VISION_INLINE_MAX_BYTES) return null;
  return `data:${obj.mime};base64,${obj.buffer.toString('base64')}`;
}

/** Whisper transcript for an inbound voice note in Storage (null if unavailable). */
export async function transcribeTicketVoiceNote(storagePath: string | null): Promise<string | null> {
  if (!storagePath) return null;
  const obj = await downloadTicketObject(storagePath);
  if (!obj) return null;
  const mime = obj.mime.startsWith('audio/') || obj.mime === 'application/ogg'
    ? obj.mime
    : 'audio/ogg';
  if (obj.buffer.length > WHISPER_MAX_BYTES) return null;
  return transcribeAudio(obj.buffer, mime);
}

export async function enrichMessageWithMediaUrl(m: Message): Promise<Message> {
  if (m.message_type === 'text' || !m.media_path) {
    return { ...m, media_signed_url: null };
  }
  try {
    const url = await createSignedUrlForPath(m.media_path);
    return { ...m, media_signed_url: url };
  } catch {
    return { ...m, media_signed_url: null };
  }
}

export async function enrichMessagesWithMediaUrls(messages: Message[]): Promise<Message[]> {
  return Promise.all(messages.map(m => enrichMessageWithMediaUrl(m)));
}
