import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { createReadStream, existsSync, mkdirSync, writeFileSync } from 'fs';
import { join, resolve } from 'path';
import { randomUUID } from 'crypto';
import { createLogger } from '../../common/services/logger.service';

const SAFE_FILE_ID = /^[A-Za-z0-9._-]+$/;

const MIME_EXT: Record<string, string> = {
  'image/jpeg': 'jpg',
  'image/jpg': 'jpg',
  'image/png': 'png',
  'image/webp': 'webp',
  'image/gif': 'gif',
  'video/mp4': 'mp4',
  'video/3gpp': '3gp',
  'audio/ogg': 'ogg',
  'audio/mpeg': 'mp3',
  'audio/mp4': 'm4a',
  'audio/aac': 'aac',
  'audio/amr': 'amr',
  'application/pdf': 'pdf',
};

const EXT_MIME: Record<string, string> = {
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  png: 'image/png',
  webp: 'image/webp',
  gif: 'image/gif',
  mp4: 'video/mp4',
  '3gp': 'video/3gpp',
  ogg: 'audio/ogg',
  mp3: 'audio/mpeg',
  m4a: 'audio/mp4',
  aac: 'audio/aac',
  amr: 'audio/amr',
  pdf: 'application/pdf',
  bin: 'application/octet-stream',
};

export function extensionForMime(mimetype: string | undefined): string {
  if (!mimetype) return 'bin';
  const base = mimetype.split(';')[0].trim().toLowerCase();
  return MIME_EXT[base] ?? 'bin';
}

export function isSafeMediaFileId(fileId: string): boolean {
  return SAFE_FILE_ID.test(fileId) && !fileId.includes('..');
}

export function mimeForFileId(fileId: string): string {
  const dot = fileId.lastIndexOf('.');
  const ext = dot >= 0 ? fileId.slice(dot + 1).toLowerCase() : 'bin';
  return EXT_MIME[ext] ?? 'application/octet-stream';
}

@Injectable()
export class UltramsgMediaService {
  private readonly logger = createLogger('UltramsgMediaService');
  private readonly dir = resolve(process.cwd(), 'data', 'ultramsg-media');

  /** Public origin used in webhook `data.media` URLs (OpenAI OCR cannot fetch localhost). */
  mediaBaseUrl(): string {
    const configured = (process.env.ULTRAMSG_MEDIA_BASE_URL ?? '').trim().replace(/\/+$/, '');
    if (configured) return configured;
    const port = process.env.PORT || '2785';
    return `http://127.0.0.1:${port}`;
  }

  absoluteMediaUrl(fileId: string): string {
    return `${this.mediaBaseUrl()}/api/ultramsg/media/${fileId}`;
  }

  /**
   * Persist inbound base64 media and return a fetchable URL string for UltraMsg `data.media`.
   * Empty string when there is nothing to save.
   */
  saveInbound(media: { mimetype?: string; data?: string; omitted?: boolean } | undefined): string {
    if (!media || media.omitted || typeof media.data !== 'string' || media.data.length === 0) {
      return '';
    }
    if (/^https?:\/\//i.test(media.data)) {
      return media.data;
    }
    try {
      mkdirSync(this.dir, { recursive: true });
      const raw = media.data.includes(',') ? media.data.slice(media.data.indexOf(',') + 1) : media.data;
      const buf = Buffer.from(raw, 'base64');
      if (buf.length === 0) return '';
      const fileId = `${randomUUID()}.${extensionForMime(media.mimetype)}`;
      writeFileSync(join(this.dir, fileId), buf);
      return this.absoluteMediaUrl(fileId);
    } catch (error) {
      this.logger.warn(
        `Failed to persist inbound UltraMsg media: ${error instanceof Error ? error.message : 'unknown error'}`,
      );
      return '';
    }
  }

  resolvePath(fileId: string): string {
    if (!isSafeMediaFileId(fileId)) {
      throw new BadRequestException('Invalid media id');
    }
    const full = resolve(this.dir, fileId);
    if (!full.startsWith(this.dir)) {
      throw new BadRequestException('Invalid media id');
    }
    return full;
  }

  open(fileId: string): { stream: ReturnType<typeof createReadStream>; mime: string } {
    const full = this.resolvePath(fileId);
    if (!existsSync(full)) {
      throw new NotFoundException('Media not found');
    }
    return { stream: createReadStream(full), mime: mimeForFileId(fileId) };
  }
}
