/**
 * Image format helpers for the imageTool.
 *
 * The image providers (Flux, GPT-Image-1.5, etc.) typically return PNG
 * bytes, but our default output is now WebP — meaning we need real
 * format conversion before saving to disk. This module owns:
 *
 *   - Format resolution (what target format does the user want?)
 *   - Magic-byte detection (what format ARE the provider's bytes?)
 *   - Transcoding via sharp, with a graceful fallback if sharp fails
 *     to load (e.g. weird platform without prebuilt binaries — we'd
 *     rather save the original bytes than fail the whole render)
 *   - Filename ↔ format mapping
 *
 * Pure-ish: `transcodeIfNeeded` is the only function with side effects
 * (calls sharp, which spins up native code). Everything else is sync
 * + side-effect-free → easy to unit test.
 *
 * Why a separate file: keeps imageTool.js focused on orchestration and
 * makes the format logic testable WITHOUT mocking the entire tool +
 * its provider clients.
 */

import path from 'path';

/** Canonical formats we read/write. */
export const SUPPORTED_FORMATS = Object.freeze(['png', 'jpeg', 'webp']);

/** Default output when the agent doesn't specify. */
export const DEFAULT_FORMAT = 'webp';

/**
 * Map an extension or alias to a canonical format name. Returns null
 * if the input doesn't map cleanly.
 *
 * - 'jpg' / 'jpeg' / '.jpg' / '.jpeg' → 'jpeg'
 * - 'png' / '.png' → 'png'
 * - 'webp' / '.webp' → 'webp'
 */
export function normalizeFormat(input) {
  if (typeof input !== 'string') return null;
  const lower = input.trim().toLowerCase().replace(/^\./, '');
  if (lower === 'jpg' || lower === 'jpeg') return 'jpeg';
  if (lower === 'png')  return 'png';
  if (lower === 'webp') return 'webp';
  return null;
}

/**
 * The on-disk extension for a canonical format. JPEG → "jpg" (the more
 * common file extension); PNG → "png"; WebP → "webp".
 */
export function extensionFor(format) {
  if (format === 'jpeg') return 'jpg';
  if (format === 'png')  return 'png';
  if (format === 'webp') return 'webp';
  return 'webp';   // fallback — should never hit if callers respect SUPPORTED_FORMATS
}

/**
 * Decide what target format to write, given the agent's request.
 *
 * Priority:
 *   1. outputPath extension (e.g. "foo.png" forces PNG, "foo.jpg" → JPEG)
 *   2. outputType field (explicit format request)
 *   3. DEFAULT_FORMAT ('webp')
 *
 * @param {object} args
 * @param {string} [args.outputPath]  May or may not have an extension.
 * @param {string} [args.outputType]  e.g. 'png' | 'webp' | 'jpg' — agent-friendly.
 * @returns {{ format: 'png'|'jpeg'|'webp', source: 'extension'|'outputType'|'default' }}
 */
export function resolveTargetFormat({ outputPath, outputType } = {}) {
  if (typeof outputPath === 'string' && outputPath.length > 0) {
    const ext = path.extname(outputPath);
    const fmt = normalizeFormat(ext);
    if (fmt) return { format: fmt, source: 'extension' };
  }
  if (outputType !== undefined && outputType !== null) {
    const fmt = normalizeFormat(outputType);
    if (fmt) return { format: fmt, source: 'outputType' };
  }
  return { format: DEFAULT_FORMAT, source: 'default' };
}

/**
 * Sniff the encoded format of a buffer by inspecting the first few bytes.
 * Returns null when nothing matches — caller then assumes "unknown" and
 * MAY skip transcoding (safer than transcoding bytes we don't recognize).
 *
 * Magic-byte references:
 *   PNG  : 89 50 4E 47 0D 0A 1A 0A
 *   JPEG : FF D8 FF
 *   WebP : "RIFF" .... "WEBP"
 *   GIF  : "GIF87a" or "GIF89a"  (we don't transcode but recognize)
 *   BMP  : 42 4D
 */
export function detectFormatFromBytes(buffer) {
  if (!buffer || typeof buffer.length !== 'number' || buffer.length < 12) return null;
  if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4E && buffer[3] === 0x47) return 'png';
  if (buffer[0] === 0xFF && buffer[1] === 0xD8 && buffer[2] === 0xFF) return 'jpeg';
  if (
    buffer[0] === 0x52 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x46 &&
    buffer[8] === 0x57 && buffer[9] === 0x45 && buffer[10] === 0x42 && buffer[11] === 0x50
  ) return 'webp';
  if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) return 'gif';
  if (buffer[0] === 0x42 && buffer[1] === 0x4D) return 'bmp';
  return null;
}

/**
 * Lazy sharp loader. Caches the module after first import; re-attempts
 * on null cache so a transient failure (e.g. concurrent install) doesn't
 * permanently disable transcoding.
 *
 * Wrapped in try/catch — if the native module fails to load on the
 * host (rare but possible on uncommon platforms), we report null and
 * the caller falls back to writing raw provider bytes.
 */
let _sharp = null;
let _sharpLoadAttempted = false;
let _sharpLoadError = null;
async function _getSharp() {
  if (_sharp) return _sharp;
  if (_sharpLoadAttempted && _sharpLoadError) {
    // Retry once after a small grace period rather than caching the
    // failure forever — node module resolution can race during install.
  }
  try {
    const mod = await import('sharp');
    _sharp = mod.default || mod;
    _sharpLoadError = null;
    return _sharp;
  } catch (err) {
    _sharpLoadAttempted = true;
    _sharpLoadError = err;
    return null;
  }
}

/** Test-only — clear sharp cache so a subsequent call re-imports. */
export function _resetSharpCacheForTests() {
  _sharp = null;
  _sharpLoadAttempted = false;
  _sharpLoadError = null;
}

/**
 * Transcode `buffer` to `targetFormat`. Returns a result describing
 * what happened so the caller can log + report:
 *
 *   { buffer: <out>, transcoded: true,  inputFormat: 'png', outputFormat: 'webp' }
 *     — sharp converted the bytes
 *   { buffer: <in>,  transcoded: false, inputFormat: 'webp', outputFormat: 'webp', reason: 'already-target' }
 *     — input format already matches target; no work
 *   { buffer: <in>,  transcoded: false, inputFormat: null,   outputFormat: 'webp', reason: 'unknown-input' }
 *     — couldn't sniff input; safer to write bytes as-is
 *   { buffer: <in>,  transcoded: false, inputFormat: 'png',  outputFormat: 'webp', reason: 'sharp-unavailable', error }
 *     — sharp couldn't load; fall back to raw bytes
 *   { buffer: <in>,  transcoded: false, inputFormat: 'png',  outputFormat: 'webp', reason: 'transcode-failed', error }
 *     — sharp threw mid-transcode
 *
 * @param {Buffer} buffer
 * @param {'png'|'jpeg'|'webp'} targetFormat
 * @param {object} [opts]
 * @param {number} [opts.quality=85]   For JPEG/WebP. PNG ignores it.
 * @param {object} [opts.logger]
 */
export async function transcodeIfNeeded(buffer, targetFormat, opts = {}) {
  const quality = Number.isFinite(opts.quality) ? opts.quality : 85;
  const logger  = opts.logger || null;
  const inputFormat = detectFormatFromBytes(buffer);

  // Already in the desired format — no work, no quality loss.
  if (inputFormat && inputFormat === targetFormat) {
    return { buffer, transcoded: false, inputFormat, outputFormat: targetFormat, reason: 'already-target' };
  }
  if (!inputFormat) {
    logger?.warn?.('[imageFormat] could not sniff input format; writing bytes as-is', { targetFormat });
    return { buffer, transcoded: false, inputFormat: null, outputFormat: targetFormat, reason: 'unknown-input' };
  }
  // GIF / BMP — sharp can read these but transcoding GIFs to webp
  // animation requires more thought; for now write bytes as-is.
  if (inputFormat === 'gif' || inputFormat === 'bmp') {
    return { buffer, transcoded: false, inputFormat, outputFormat: targetFormat, reason: 'unsupported-input' };
  }

  const sharpFn = await _getSharp();
  if (!sharpFn) {
    logger?.warn?.('[imageFormat] sharp not available; writing bytes as-is', {
      targetFormat,
      error: _sharpLoadError?.message,
    });
    return {
      buffer,
      transcoded: false,
      inputFormat,
      outputFormat: targetFormat,
      reason: 'sharp-unavailable',
      error: _sharpLoadError,
    };
  }

  try {
    let pipeline = sharpFn(buffer);
    if (targetFormat === 'webp') pipeline = pipeline.webp({ quality });
    else if (targetFormat === 'jpeg') pipeline = pipeline.jpeg({ quality });
    else if (targetFormat === 'png')  pipeline = pipeline.png();
    const out = await pipeline.toBuffer();
    return { buffer: out, transcoded: true, inputFormat, outputFormat: targetFormat };
  } catch (err) {
    logger?.warn?.('[imageFormat] transcode failed; writing original bytes as-is', {
      inputFormat, targetFormat, error: err?.message,
    });
    return {
      buffer,
      transcoded: false,
      inputFormat,
      outputFormat: targetFormat,
      reason: 'transcode-failed',
      error: err,
    };
  }
}

/**
 * Convenience: if the user-supplied outputPath has no extension,
 * append the target format's extension. Otherwise return as-is.
 * (The agent might pass `images/foo` without an extension — we'd
 * normally reject, but for the auto-extension case we just append.)
 */
export function ensureExtension(outputPath, targetFormat) {
  if (!outputPath || typeof outputPath !== 'string') return outputPath;
  const ext = path.extname(outputPath);
  if (ext && normalizeFormat(ext)) return outputPath;
  return `${outputPath}.${extensionFor(targetFormat)}`;
}

export default {
  SUPPORTED_FORMATS,
  DEFAULT_FORMAT,
  normalizeFormat,
  extensionFor,
  resolveTargetFormat,
  detectFormatFromBytes,
  transcodeIfNeeded,
  ensureExtension,
  _resetSharpCacheForTests,
};
