/**
 * Media Feature — Server-side API
 *
 * Toutes les opérations CRUD sur le registre `zen_media` et `zen_media_references`.
 * Ne contient AUCUNE logique HTTP — utilisable depuis routes.server.js, server
 * actions, et modules externes (`@zen/module-*`).
 *
 * Stockage : délégué à `@zen/core/storage` (R2/Backblaze). La table est la
 * source de vérité ; S3 ne stocke que les bytes.
 */

import { randomBytes } from 'node:crypto';
import { query, queryOne } from '@zen/core/database';
import {
  uploadFile,
  deleteFile,
  generateUniqueFilename,
  validateUpload,
  FILE_TYPE_PRESETS,
  FILE_SIZE_LIMITS,
} from '@zen/core/storage';
import { generateId } from '../../core/users/password.js';
import { fail, info } from '@zen/core/shared/logger';

const SLUG_BYTES = 9; // 9 octets → 12 caractères base64url

/**
 * Génère un slug URL-safe court (12 caractères) pour identifier un média
 * publiquement sans exposer l'ID interne.
 */
function generateSlug() {
  return randomBytes(SLUG_BYTES).toString('base64url');
}

/**
 * Construit la clé S3 sous le préfixe `media/<yyyy>/<mm>/<filename-unique>`.
 * Le préfixe `media/` reste **privé** côté S3 — l'accès public passe par
 * notre route HTTP qui valide la visibilité en BD avant de proxyfier.
 */
function buildStorageKey(originalName) {
  const now = new Date();
  const yyyy = String(now.getUTCFullYear());
  const mm = String(now.getUTCMonth() + 1).padStart(2, '0');
  const filename = generateUniqueFilename(originalName, 'media');
  return `media/${yyyy}/${mm}/${filename}`;
}

/**
 * Devine la nature du fichier selon son MIME pour le filtrage UI et les
 * validations spécifiques (images vs documents).
 */
export function classifyMime(mimeType) {
  if (!mimeType) return 'other';
  if (mimeType.startsWith('image/')) return 'image';
  if (mimeType.startsWith('video/')) return 'video';
  if (mimeType.startsWith('audio/')) return 'audio';
  if (mimeType === 'application/pdf') return 'document';
  if (mimeType.startsWith('text/') || mimeType.includes('document') || mimeType.includes('sheet')) return 'document';
  return 'other';
}

const MIME_BY_EXT = {
  '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
  '.gif': 'image/gif',  '.webp': 'image/webp',
  '.pdf': 'application/pdf',
  '.txt': 'text/plain', '.csv': 'text/csv',
  '.mp4': 'video/mp4',  '.webm': 'video/webm',
  '.mp3': 'audio/mpeg', '.ogg': 'audio/ogg',
};

function deriveContentType(filename) {
  const lower = filename.toLowerCase();
  const dot = lower.lastIndexOf('.');
  if (dot === -1) return 'application/octet-stream';
  return MIME_BY_EXT[lower.slice(dot)] ?? 'application/octet-stream';
}

/**
 * Upload + insertion BD en une opération transactionnelle.
 *
 * Stratégie : on persiste en BD AVANT le commit final côté S3. En cas d'échec
 * BD on supprime l'objet S3 fraîchement uploadé pour ne pas laisser d'orphelin.
 *
 * @param {object} args
 * @param {File|Buffer} args.file - Fichier source (FormData File ou Buffer brut)
 * @param {string} [args.filename] - Nom original (requis si file est un Buffer)
 * @param {number} [args.size] - Taille (requise si file est un Buffer)
 * @param {string} args.uploadedBy - ID utilisateur qui upload
 * @param {'private'|'public'} [args.visibility='private']
 * @param {string} [args.altText]
 * @param {string} [args.caption]
 * @returns {Promise<{ success: true, media: object } | { success: false, error: string }>}
 */
export async function uploadMedia({ file, filename, size, uploadedBy, visibility = 'private', altText = null, caption = null }) {
  let buffer;
  let originalName;
  let fileSize;

  if (file && typeof file.arrayBuffer === 'function') {
    buffer = Buffer.from(await file.arrayBuffer());
    originalName = file.name ?? filename ?? 'unnamed';
    fileSize = file.size ?? buffer.length;
  } else if (Buffer.isBuffer(file)) {
    if (!filename) return { success: false, error: 'filename is required when uploading a Buffer' };
    buffer = file;
    originalName = filename;
    fileSize = size ?? buffer.length;
  } else {
    return { success: false, error: 'file must be a File or a Buffer' };
  }

  const isImage = deriveContentType(originalName).startsWith('image/');
  const validation = validateUpload({
    filename: originalName,
    size: fileSize,
    allowedTypes: isImage ? FILE_TYPE_PRESETS.IMAGES : FILE_TYPE_PRESETS.DOCUMENTS,
    maxSize: isImage ? FILE_SIZE_LIMITS.IMAGE : FILE_SIZE_LIMITS.DOCUMENT,
    buffer,
  });

  if (!validation.valid) {
    return { success: false, error: validation.errors.join(', ') };
  }

  const contentType = deriveContentType(originalName);
  const storageKey = buildStorageKey(originalName);

  const uploadResult = await uploadFile({
    key: storageKey,
    body: buffer,
    contentType,
    metadata: { uploadedBy, originalName },
    cacheControl: visibility === 'public' ? 'public, max-age=31536000, immutable' : 'private, max-age=0, no-store',
  });

  if (!uploadResult.success) {
    return { success: false, error: 'Storage upload failed' };
  }

  const id = generateId();
  const slug = generateSlug();

  try {
    const inserted = await queryOne(
      `INSERT INTO zen_media (id, slug, storage_key, original_name, mime_type, size_bytes, visibility, uploaded_by, alt_text, caption)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
       RETURNING *`,
      [id, slug, storageKey, originalName, contentType, fileSize, visibility, uploadedBy, altText, caption]
    );
    return { success: true, media: inserted };
  } catch (error) {
    try {
      await deleteFile(storageKey);
    } catch (rollbackError) {
      fail(`uploadMedia: rollback delete failed for orphan ${storageKey}: ${rollbackError.message}`);
    }
    fail(`uploadMedia: DB insert failed: ${error.message}`);
    return { success: false, error: 'Failed to record media' };
  }
}

export async function getMediaById(id) {
  return queryOne(`SELECT * FROM zen_media WHERE id = $1`, [id]);
}

export async function getMediaBySlug(slug) {
  return queryOne(`SELECT * FROM zen_media WHERE slug = $1`, [slug]);
}

/**
 * Liste paginée + filtres facultatifs.
 *
 * @param {object} [opts]
 * @param {number} [opts.page=1]
 * @param {number} [opts.limit=24]
 * @param {string} [opts.search] - Recherche LIKE sur original_name
 * @param {'image'|'video'|'audio'|'document'|'other'} [opts.kind]
 * @param {'private'|'public'} [opts.visibility]
 * @param {string} [opts.uploadedBy]
 */
export async function listMedia({ page = 1, limit = 24, search, kind, visibility, uploadedBy } = {}) {
  const safePage = Math.max(1, Math.floor(page));
  const safeLimit = Math.min(Math.max(1, Math.floor(limit)), 100);
  const offset = (safePage - 1) * safeLimit;

  const conditions = [];
  const params = [];

  if (search) {
    params.push(`%${search}%`);
    conditions.push(`original_name ILIKE $${params.length}`);
  }
  if (visibility) {
    params.push(visibility);
    conditions.push(`visibility = $${params.length}`);
  }
  if (uploadedBy) {
    params.push(uploadedBy);
    conditions.push(`uploaded_by = $${params.length}`);
  }
  if (kind) {
    if (kind === 'image')         conditions.push(`mime_type LIKE 'image/%'`);
    else if (kind === 'video')    conditions.push(`mime_type LIKE 'video/%'`);
    else if (kind === 'audio')    conditions.push(`mime_type LIKE 'audio/%'`);
    else if (kind === 'document') conditions.push(`(mime_type = 'application/pdf' OR mime_type LIKE 'text/%' OR mime_type LIKE 'application/vnd%')`);
  }

  const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';

  params.push(safeLimit, offset);
  const rows = (await query(
    `SELECT * FROM zen_media ${where} ORDER BY created_at DESC LIMIT $${params.length - 1} OFFSET $${params.length}`,
    params
  )).rows;

  const countParams = params.slice(0, -2);
  const totalRow = await queryOne(`SELECT COUNT(*)::int AS count FROM zen_media ${where}`, countParams);
  const total = totalRow?.count ?? 0;

  return {
    media: rows,
    pagination: { page: safePage, limit: safeLimit, total, totalPages: Math.ceil(total / safeLimit) },
  };
}

/**
 * Met à jour les metadata éditables d'un média (visibilité, alt, caption).
 * La clé S3 et le contenu binaire ne sont jamais modifiés ici.
 */
export async function updateMedia(id, { visibility, altText, caption } = {}) {
  const fields = [];
  const params = [];

  if (visibility !== undefined) {
    if (!['private', 'public'].includes(visibility)) {
      return { success: false, error: 'Invalid visibility value' };
    }
    params.push(visibility);
    fields.push(`visibility = $${params.length}`);
  }
  if (altText !== undefined) {
    params.push(altText || null);
    fields.push(`alt_text = $${params.length}`);
  }
  if (caption !== undefined) {
    params.push(caption || null);
    fields.push(`caption = $${params.length}`);
  }

  if (fields.length === 0) {
    return { success: false, error: 'No fields to update' };
  }

  fields.push(`updated_at = CURRENT_TIMESTAMP`);
  params.push(id);

  const updated = await queryOne(
    `UPDATE zen_media SET ${fields.join(', ')} WHERE id = $${params.length} RETURNING *`,
    params
  );

  if (!updated) return { success: false, error: 'Media not found' };
  return { success: true, media: updated };
}

/**
 * Compte les références. Utile pour l'UI ("utilisé par X contenus") et avant
 * de tenter une suppression.
 */
export async function countReferences(mediaId) {
  const row = await queryOne(
    `SELECT COUNT(*)::int AS count FROM zen_media_references WHERE media_id = $1`,
    [mediaId]
  );
  return row?.count ?? 0;
}

export async function listReferences(mediaId) {
  return (await query(
    `SELECT media_id, source_type, source_id, field, created_at
     FROM zen_media_references WHERE media_id = $1 ORDER BY created_at DESC`,
    [mediaId]
  )).rows;
}

/**
 * Supprime un média (BD puis S3). Refuse si des références existent — la FK
 * RESTRICT le bloque déjà au niveau BD, on retourne juste un message clair.
 */
export async function deleteMedia(id) {
  const media = await getMediaById(id);
  if (!media) return { success: false, error: 'Media not found' };

  const refCount = await countReferences(id);
  if (refCount > 0) {
    return { success: false, error: `Ce média est utilisé par ${refCount} contenu(s). Détachez-le avant de le supprimer.`, referenceCount: refCount };
  }

  try {
    await query(`DELETE FROM zen_media WHERE id = $1`, [id]);
  } catch (error) {
    fail(`deleteMedia: DB delete failed for ${id}: ${error.message}`);
    return { success: false, error: 'Failed to delete media record' };
  }

  try {
    await deleteFile(media.storage_key);
  } catch (error) {
    fail(`deleteMedia: S3 delete failed for ${media.storage_key} (orphan): ${error.message}`);
  }

  info(`deleteMedia: removed ${id} (${media.storage_key})`);
  return { success: true };
}

/**
 * Attache un média à une ressource consommatrice. Utilisé par les modules
 * externes (Posts, Pages, etc.) au moment de sauvegarder leur contenu.
 *
 * @param {object} args
 * @param {string} args.mediaId
 * @param {string} args.sourceType - Ex: 'post', 'page', '@zen/module-shop:product'
 * @param {string} args.sourceId
 * @param {string} [args.field=''] - Ex: 'featured_image', 'gallery'
 */
export async function attachMedia({ mediaId, sourceType, sourceId, field = '' }) {
  if (!mediaId || !sourceType || !sourceId) {
    return { success: false, error: 'mediaId, sourceType and sourceId are required' };
  }
  await query(
    `INSERT INTO zen_media_references (media_id, source_type, source_id, field)
     VALUES ($1, $2, $3, $4)
     ON CONFLICT DO NOTHING`,
    [mediaId, sourceType, sourceId, field]
  );
  return { success: true };
}

export async function detachMedia({ mediaId, sourceType, sourceId, field = '' }) {
  if (!mediaId || !sourceType || !sourceId) {
    return { success: false, error: 'mediaId, sourceType and sourceId are required' };
  }
  await query(
    `DELETE FROM zen_media_references WHERE media_id = $1 AND source_type = $2 AND source_id = $3 AND field = $4`,
    [mediaId, sourceType, sourceId, field]
  );
  return { success: true };
}

/**
 * Détache toutes les références d'une source donnée. À appeler lorsqu'un
 * consommateur supprime sa propre ressource (post, page) pour libérer les
 * médias attachés.
 */
export async function detachAllForSource({ sourceType, sourceId }) {
  if (!sourceType || !sourceId) {
    return { success: false, error: 'sourceType and sourceId are required' };
  }
  await query(
    `DELETE FROM zen_media_references WHERE source_type = $1 AND source_id = $2`,
    [sourceType, sourceId]
  );
  return { success: true };
}

/**
 * Construit l'URL servie par notre route HTTP. Le slug est utilisé en lieu et
 * place de l'ID pour éviter l'énumération séquentielle.
 *
 * Cette URL fonctionne pour les deux niveaux de visibilité :
 *   - public  → servi sans session
 *   - privé   → servi uniquement aux sessions avec `media.view`
 */
export function buildMediaUrl(media) {
  if (!media?.slug) return null;
  return `/zen/api/media/file/${media.slug}`;
}
