/**
 * BlockEditor — liaison serveur avec la médiathèque pour les blocs image.
 *
 * Trois helpers, à appeler depuis les actions/routes serveur des modules
 * consommateurs qui hébergent du contenu BlockEditor :
 *
 *   - `normalizeImageBlocks(blocks)` : prépare les blocs pour la persistance.
 *     Lazy migration des blocs legacy (extraction du slug depuis `src`),
 *     back-fill de l'alt/caption côté média si manquants, suppression des
 *     champs `alt`/`caption` du bloc en mode strict (média = source unique).
 *
 *   - `enrichBlocksWithMedia(blocks)` : prépare les blocs pour le rendu.
 *     Une seule requête batch à `zen_media`, attache `_resolvedAlt` /
 *     `_resolvedCaption` aux blocs liés. Champs internes (préfixe `_`),
 *     non persistés.
 *
 *   - `syncBlockImageReferences({ sourceType, sourceId, blocks })` :
 *     synchronise `zen_media_references` pour la liste courante des médias
 *     référencés par les blocs image du document. À appeler dans la même
 *     transaction que le save.
 *
 * Module **serveur** : importe `@zen/core/database`. Les consommateurs
 * client n'ont pas besoin de ces helpers — l'éditeur client persiste les
 * blocs tels quels, c'est au moment du save côté serveur qu'on normalise.
 */

import { query } from '@zen/core/database';

const MEDIA_FILE_URL_RE = /^\/zen\/api\/media\/file\/([^/?#]+)$/;
const REFERENCE_FIELD = 'block_image';

function extractSlugFromSrc(src) {
  if (typeof src !== 'string') return null;
  const m = MEDIA_FILE_URL_RE.exec(src);
  return m ? m[1] : null;
}

function collectImageBlocks(blocks) {
  const out = [];
  if (!Array.isArray(blocks)) return out;
  for (const b of blocks) {
    if (b && b.type === 'image') out.push(b);
  }
  return out;
}

function collectSlugs(blocks) {
  const slugs = new Set();
  for (const b of collectImageBlocks(blocks)) {
    const slug = b.mediaSlug || extractSlugFromSrc(b.src);
    if (slug) slugs.add(slug);
  }
  return [...slugs];
}

/**
 * Charge `id`, `slug`, `alt_text`, `caption` pour un lot de slugs en une seule
 * requête. Retourne une `Map<slug, row>`.
 */
async function fetchMediaBySlugs(slugs) {
  if (slugs.length === 0) return new Map();
  const result = await query(
    `SELECT id, slug, alt_text, caption FROM zen_media WHERE slug = ANY($1::text[])`,
    [slugs]
  );
  const map = new Map();
  for (const row of result.rows) map.set(row.slug, row);
  return map;
}

/**
 * Normalise les blocs image avant persistance.
 *
 * - Si `mediaSlug` est absent mais que `src` matche `/zen/api/media/file/<slug>`,
 *   extrait et persiste le slug (lazy upgrade des blocs legacy).
 * - Si le bloc est lié à un média existant et que ce média n'a pas encore
 *   d'`alt_text` / `caption` mais que le bloc en porte (legacy ou saisie
 *   utilisateur), back-fill côté média (la valeur la plus récente vit
 *   côté bloc en cas de migration).
 * - Quand un `mediaSlug` est résolu (existant en BD), supprime `alt` et
 *   `caption` du bloc en mode strict.
 * - Quand `mediaSlug` est posé mais ne correspond à aucun média en BD
 *   (média supprimé ou slug invalide), on retire `mediaSlug` et on garde
 *   `alt`/`caption` côté bloc — l'image devient une URL externe.
 * - Les champs internes préfixés `_` (snapshots d'édition) sont toujours
 *   strippés.
 *
 * @param {object[]} blocks
 * @returns {Promise<object[]>} nouveau tableau de blocs (input non muté)
 */
export async function normalizeImageBlocks(blocks) {
  if (!Array.isArray(blocks) || blocks.length === 0) return blocks ?? [];

  const slugs = collectSlugs(blocks);
  const mediaBySlug = await fetchMediaBySlugs(slugs);

  // Back-fill : pour chaque média sans alt/caption où le bloc en porte, on
  // remonte la valeur. Une seule UPDATE par champ manquant ; on agrège les
  // candidats par slug pour ne back-filler qu'une fois (premier bloc gagne).
  const altBackfill = new Map(); // slug → alt_text
  const captionBackfill = new Map(); // slug → caption

  for (const b of collectImageBlocks(blocks)) {
    const slug = b.mediaSlug || extractSlugFromSrc(b.src);
    if (!slug) continue;
    const media = mediaBySlug.get(slug);
    if (!media) continue;
    if ((!media.alt_text || media.alt_text === '') && b.alt && !altBackfill.has(slug)) {
      altBackfill.set(slug, b.alt);
    }
    if ((!media.caption || media.caption === '') && b.caption && !captionBackfill.has(slug)) {
      captionBackfill.set(slug, b.caption);
    }
  }

  for (const [slug, altText] of altBackfill) {
    await query(
      `UPDATE zen_media SET alt_text = $1, updated_at = CURRENT_TIMESTAMP
       WHERE slug = $2 AND (alt_text IS NULL OR alt_text = '')`,
      [altText, slug]
    );
    const media = mediaBySlug.get(slug);
    if (media) media.alt_text = altText;
  }
  for (const [slug, caption] of captionBackfill) {
    await query(
      `UPDATE zen_media SET caption = $1, updated_at = CURRENT_TIMESTAMP
       WHERE slug = $2 AND (caption IS NULL OR caption = '')`,
      [caption, slug]
    );
    const media = mediaBySlug.get(slug);
    if (media) media.caption = caption;
  }

  return blocks.map(b => {
    if (!b || b.type !== 'image') return b;
    const slug = b.mediaSlug || extractSlugFromSrc(b.src);
    const media = slug ? mediaBySlug.get(slug) : null;
    // Champs internes toujours strippés.
    const {
      _mediaAlt: _ma, _mediaCaption: _mc,
      _resolvedAlt: _ra, _resolvedCaption: _rc,
      ...rest
    } = b;
    void _ma; void _mc; void _ra; void _rc;
    if (media) {
      // Mode strict : alt/caption vivent côté média.
      return { ...rest, mediaSlug: slug, alt: '', caption: '' };
    }
    // Pas de média correspondant → URL externe ou média supprimé. On
    // nettoie `mediaSlug` pour ne pas créer de référence orpheline.
    return { ...rest, mediaSlug: '' };
  });
}

/**
 * Attache `_resolvedAlt` et `_resolvedCaption` aux blocs image liés à un
 * média. Le composant de rendu lit ces champs en priorité (cf. helpers
 * `resolvedAlt` / `resolvedCaption` dans `Image.client.js`). Les blocs URL
 * libres ne sont pas modifiés.
 *
 * @param {object[]} blocks
 * @returns {Promise<object[]>}
 */
export async function enrichBlocksWithMedia(blocks) {
  if (!Array.isArray(blocks) || blocks.length === 0) return blocks ?? [];

  const slugs = collectSlugs(blocks);
  if (slugs.length === 0) return blocks;
  const mediaBySlug = await fetchMediaBySlugs(slugs);

  return blocks.map(b => {
    if (!b || b.type !== 'image') return b;
    const slug = b.mediaSlug || extractSlugFromSrc(b.src);
    if (!slug) return b;
    const media = mediaBySlug.get(slug);
    if (!media) return b;
    return {
      ...b,
      _resolvedAlt: media.alt_text ?? '',
      _resolvedCaption: media.caption ?? '',
    };
  });
}

/**
 * Synchronise `zen_media_references` pour les blocs image d'un document.
 *
 * Utilise le `field = 'block_image'` pour distinguer ces références des
 * autres usages (`featured_image`, `gallery`, …). À chaque save :
 * supprime les anciennes références `block_image` du document, puis
 * insère celles correspondant aux blocs courants.
 *
 * À appeler dans la même transaction que le save du contenu hôte. La
 * contrainte FK `ON DELETE RESTRICT` empêche ensuite la suppression d'un
 * média référencé via la médiathèque tant que le document existe.
 *
 * @param {object} args
 * @param {string} args.sourceType  Ex: 'post', 'page', '@zen/module-shop:product'
 * @param {string} args.sourceId    ID du document hôte
 * @param {object[]} args.blocks    Blocs (peuvent être déjà normalisés ou non)
 */
export async function syncBlockImageReferences({ sourceType, sourceId, blocks }) {
  if (!sourceType || !sourceId) {
    return { success: false, error: 'sourceType and sourceId are required' };
  }

  const slugs = collectSlugs(blocks);
  // Résoudre les slugs en `media_id` (la table de jointure indexe l'id, pas le slug).
  const mediaBySlug = await fetchMediaBySlugs(slugs);
  const mediaIds = [...new Set(slugs.map(s => mediaBySlug.get(s)?.id).filter(Boolean))];

  // Supprime toutes les anciennes références block_image du document. Cette
  // approche « replace » est plus simple qu'un diff et reste efficace : le
  // nombre d'images par document est petit, et la clé primaire compose
  // déjà (media_id, source_type, source_id, field).
  await query(
    `DELETE FROM zen_media_references
     WHERE source_type = $1 AND source_id = $2 AND field = $3`,
    [sourceType, sourceId, REFERENCE_FIELD]
  );

  if (mediaIds.length > 0) {
    // Insère en une seule requête multi-rows.
    const values = [];
    const params = [sourceType, sourceId, REFERENCE_FIELD];
    for (const mediaId of mediaIds) {
      params.push(mediaId);
      values.push(`($${params.length}, $1, $2, $3)`);
    }
    await query(
      `INSERT INTO zen_media_references (media_id, source_type, source_id, field)
       VALUES ${values.join(', ')}
       ON CONFLICT DO NOTHING`,
      params
    );
  }

  return { success: true, count: mediaIds.length };
}
