'use strict'
// ─────────────────────────────────────────────────────────────────────────────
// Photos de profil (avatars) par utilisateur. Stockage serveur keyé par courriel
// Authentik → suit l'usager entre appareils. Même patron d'identité que
// user-prefs.js : l'identité = x-authentik-email (forward-auth SSO). Un usager ne
// peut modifier QUE son propre avatar ; un admin peut cibler un autre via ?as=.
//
//   GET    /avatars/{email}          → l'image (ou 404). Public-ish (chargée par <img>).
//   POST   /avatars        { dataUrl } → définit MON avatar (self)
//   POST   /avatars?as=<e> { dataUrl } → (ADMIN) définit l'avatar d'un autre
//   DELETE /avatars                   → supprime MON avatar (self)  · ?as= pour admin
//
// Repli visuel (initiales + couleur) = côté client (<UserAvatar>) sur 404/erreur.
// ─────────────────────────────────────────────────────────────────────────────
const fs = require('fs')
const path = require('path')
const crypto = require('crypto')
const { json, parseBody, readJsonFile, writeJsonFile } = require('./helpers')

const DIR = '/app/data/avatars'
const INDEX = path.join(DIR, '_index.json') // { emailLower: { ext, mime, ts } }
function ensureDir () { if (!fs.existsSync(DIR)) fs.mkdirSync(DIR, { recursive: true }) }

// SVG volontairement EXCLU (une image de profil SVG = vecteur XSS via <img>/inline).
const ALLOWED = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif' }
const MAX_BYTES = 1024 * 1024 // 1 Mo — les avatars sont petits ; le client redimensionne avant l'envoi.

const meOf = (req) => String(req.headers['x-authentik-email'] || '').toLowerCase()
function isAdmin (req) {
  const g = String(req.headers['x-authentik-groups'] || req.headers['x-authentik-group'] || '')
  return /(^|[,|;])\s*(ops-?admin|admin(istrateurs?)?|superviseurs?)\s*([,|;]|$)/i.test(g)
}

function decodeDataUrl (dataUrl) {
  if (typeof dataUrl !== 'string') return null
  const m = dataUrl.match(/^data:([^;,]+)(;base64)?,(.*)$/)
  if (!m) return null
  const mime = m[1].toLowerCase()
  if (!ALLOWED[mime]) return { error: 'type' }
  if (!m[2]) return { error: 'type' } // on exige du base64
  let buffer
  try { buffer = Buffer.from(m[3], 'base64') } catch { return null }
  if (!buffer.length) return null
  if (buffer.length > MAX_BYTES) return { error: 'too_large', size: buffer.length }
  return { mime, ext: ALLOWED[mime], buffer }
}

const fileFor = (email, ext) => path.join(DIR, crypto.createHash('sha1').update(email).digest('hex') + '.' + ext)

async function handle (req, res, method, path_) {
  // GET /avatars  → index { avatars: { emailLower: version } } : liste des courriels
  // AYANT une photo (+ version pour cache-bust). Le client s'en sert pour n'afficher
  // le <img> QUE pour ces courriels et éviter un 404 par avatar sans photo.
  if (method === 'GET' && (path_ === '/avatars' || path_ === '/avatars/')) {
    const idx = readJsonFile(INDEX, {})
    const avatars = {}
    for (const [email, meta] of Object.entries(idx)) avatars[email] = (meta && meta.ts) || 1
    res.setHeader('Cache-Control', 'no-cache')
    return json(res, 200, { avatars })
  }

  // GET /avatars/{email}
  if (method === 'GET') {
    const email = decodeURIComponent(path_.replace(/^\/avatars\/?/, '')).trim().toLowerCase()
    if (!email) return json(res, 400, { error: 'email requis' })
    const idx = readJsonFile(INDEX, {})
    const meta = idx[email]
    if (!meta) { res.statusCode = 404; return res.end() }
    const p = fileFor(email, meta.ext)
    if (!fs.existsSync(p)) { res.statusCode = 404; return res.end() }
    res.setHeader('Content-Type', meta.mime || 'application/octet-stream')
    // Change de contenu à la même URL → revalidation courte + ?v=<ts> côté client pour le cache-busting immédiat.
    res.setHeader('Cache-Control', 'public, max-age=300, must-revalidate')
    res.statusCode = 200
    return res.end(fs.readFileSync(p))
  }

  const me = meOf(req)
  const as = String(new URL(req.url, 'http://localhost').searchParams.get('as') || '').toLowerCase()
  const target = (as && as !== me && isAdmin(req)) ? as : me
  if (!target) return json(res, 400, { error: 'utilisateur inconnu (x-authentik-email absent)' })
  if (as && as !== me && !isAdmin(req)) return json(res, 403, { error: 'réservé aux administrateurs' })

  if (method === 'POST') {
    const b = await parseBody(req)
    const decoded = decodeDataUrl(b && b.dataUrl)
    if (!decoded) return json(res, 400, { error: 'dataUrl image invalide' })
    if (decoded.error === 'type') return json(res, 415, { error: 'format non supporté (png, jpeg, webp, gif)' })
    if (decoded.error === 'too_large') return json(res, 413, { error: `image trop lourde (${Math.round(decoded.size / 1024)} Ko > 1 Mo)` })
    ensureDir()
    const idx = readJsonFile(INDEX, {})
    // Retire un ancien fichier d'extension différente pour éviter les orphelins.
    if (idx[target] && idx[target].ext !== decoded.ext) { try { fs.unlinkSync(fileFor(target, idx[target].ext)) } catch { /* absent */ } }
    fs.writeFileSync(fileFor(target, decoded.ext), decoded.buffer)
    const ts = Date.now()
    idx[target] = { ext: decoded.ext, mime: decoded.mime, ts }
    writeJsonFile(INDEX, idx)
    return json(res, 200, { ok: true, email: target, version: ts })
  }

  if (method === 'DELETE') {
    const idx = readJsonFile(INDEX, {})
    if (idx[target]) { try { fs.unlinkSync(fileFor(target, idx[target].ext)) } catch { /* absent */ } ; delete idx[target]; writeJsonFile(INDEX, idx) }
    return json(res, 200, { ok: true, email: target })
  }

  return json(res, 405, { error: 'method not allowed' })
}

module.exports = { handle }
