(function(global) {
  'use strict';

  // Claude's many-image request path rejects/pads near the 2000px hard limit.
  // Keep terminal-pasted screenshots at Claude's native resolution so long
  // sessions stay valid and avoid provider-side resize latency.
  const DEFAULT_MAX_LONG_EDGE = 1568;
  // Hard safety ceiling — an encoded image must never exceed this (well under the
  // 16MB server BODY_LIMIT_LARGE, leaving room for text + multiple attachments).
  const DEFAULT_MAX_BYTES = 8 * 1024 * 1024;
  // Soft compression target — re-encode anything larger to a compact format
  // (WebP preferred, JPEG fallback). A 1568px screenshot as PNG is ~1.5-3MB but
  // ~250-400KB as WebP/JPEG q85 with no visible loss, so keep pasted images well
  // under ~1MB to shrink the request body, transcript, brain storage, and the
  // vision tokens billed on every turn.
  const DEFAULT_TARGET_BYTES = 1024 * 1024;
  // Encoder ladder tried (in order) when an image is over the soft target. WebP
  // first (best ratio + preserves alpha); JPEG as the universal fallback for
  // browsers without WebP encode. Stop at the first candidate under target.
  const COMPRESSION_LADDER = [
    ['image/webp', 0.85],
    ['image/webp', 0.72],
    ['image/jpeg', 0.85],
    ['image/jpeg', 0.72],
  ];

  function isImageBlob(blob) {
    return !!blob && String(blob.type || '').toLowerCase().indexOf('image/') === 0;
  }

  function extensionForMime(mime) {
    const m = String(mime || '').toLowerCase();
    if (m.includes('jpeg') || m.includes('jpg')) return '.jpg';
    if (m.includes('webp')) return '.webp';
    if (m.includes('gif')) return '.gif';
    return '.png';
  }

  function filenameWithMime(filename, mime) {
    const name = String(filename || 'image.png');
    const ext = extensionForMime(mime);
    const stripped = name.replace(/\.(png|jpe?g|webp|gif)$/i, '');
    return stripped + ext;
  }

  function loadImageElement(blob) {
    return new Promise((resolve, reject) => {
      const url = URL.createObjectURL(blob);
      const img = new Image();
      img.onload = () => {
        URL.revokeObjectURL(url);
        resolve(img);
      };
      img.onerror = () => {
        URL.revokeObjectURL(url);
        reject(new Error('Image decode failed'));
      };
      img.src = url;
    });
  }

  async function decodeImage(blob) {
    if (typeof createImageBitmap === 'function') {
      try {
        const bitmap = await createImageBitmap(blob);
        return { source: bitmap, width: bitmap.width, height: bitmap.height, close: () => bitmap.close && bitmap.close() };
      } catch {
        // Fall through to HTMLImageElement for browsers/files createImageBitmap cannot decode.
      }
    }
    const img = await loadImageElement(blob);
    return { source: img, width: img.naturalWidth || img.width, height: img.naturalHeight || img.height, close: () => {} };
  }

  function canvasToBlob(canvas, type, quality) {
    return new Promise((resolve, reject) => {
      canvas.toBlob((blob) => {
        if (blob) resolve(blob);
        else reject(new Error('Image encode failed'));
      }, type, quality);
    });
  }

  function outputTypeFor(blob) {
    const type = String(blob && blob.type || '').toLowerCase();
    if (type === 'image/jpeg' || type === 'image/png' || type === 'image/webp') return type;
    return 'image/png';
  }

  async function normalizeForProvider(blob, opts) {
    opts = opts || {};
    if (!isImageBlob(blob)) {
      return { blob, filename: opts.filename || blob?.name || 'image.png', mimeType: blob?.type || 'application/octet-stream', resized: false };
    }

    const maxLongEdge = Number(opts.maxLongEdge || DEFAULT_MAX_LONG_EDGE);
    const maxBytes = Number(opts.maxBytes || DEFAULT_MAX_BYTES);
    const targetBytes = Math.min(Number(opts.targetBytes || DEFAULT_TARGET_BYTES), maxBytes);
    let decoded = null;
    try {
      decoded = await decodeImage(blob);
      const originalWidth = decoded.width || 0;
      const originalHeight = decoded.height || 0;
      if (!originalWidth || !originalHeight) throw new Error('Image has no dimensions');

      const keepOriginal = () => ({
        blob,
        filename: opts.filename || blob.name || filenameWithMime('image', blob.type),
        mimeType: blob.type || 'image/png',
        width: originalWidth,
        height: originalHeight,
        originalWidth,
        originalHeight,
        resized: false,
      });

      const scale = Math.min(1, maxLongEdge / Math.max(originalWidth, originalHeight));
      const needsDimensionResize = scale < 1;
      const overTarget = blob.size > targetBytes;
      // Already small and correctly sized — nothing to do.
      if (!needsDimensionResize && !overTarget) return keepOriginal();

      const width = Math.max(1, Math.round(originalWidth * scale));
      const height = Math.max(1, Math.round(originalHeight * scale));
      const canvas = document.createElement('canvas');
      canvas.width = width;
      canvas.height = height;
      const ctx = canvas.getContext('2d', { alpha: true });
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = 'high';
      ctx.drawImage(decoded.source, 0, 0, width, height);

      // Start by re-encoding in the source format (PNG stays lossless). If that
      // already fits the soft target we keep it — small or palette/line-art PNGs
      // stay PNG and avoid JPEG text artifacts.
      const sourceType = outputTypeFor(blob);
      let best = await canvasToBlob(canvas, sourceType, sourceType === 'image/png' ? undefined : 0.9);

      // Over target → walk the compression ladder, keeping the smallest blob and
      // stopping at the first one under target.
      if (best.size > targetBytes) {
        for (const [type, quality] of COMPRESSION_LADDER) {
          let candidate;
          try { candidate = await canvasToBlob(canvas, type, quality); }
          catch { continue; }
          // A browser without WebP encode silently returns PNG — ignore those.
          if (!candidate || candidate.type !== type) continue;
          if (candidate.size < best.size) best = candidate;
          if (best.size <= targetBytes) break;
        }
      }

      // Hard ceiling backstop: never ship something over maxBytes.
      if (best.size > maxBytes) {
        try {
          const forced = await canvasToBlob(canvas, 'image/jpeg', 0.6);
          if (forced && forced.size < best.size) best = forced;
        } catch { /* keep best */ }
      }

      // If we couldn't actually shrink it and no resize was needed, keep the
      // original rather than ship a larger (or equal) re-encode.
      if (!needsDimensionResize && best.size >= blob.size) return keepOriginal();

      return {
        blob: best,
        filename: filenameWithMime(opts.filename || blob.name || 'image.png', best.type || sourceType),
        mimeType: best.type || sourceType,
        width,
        height,
        originalWidth,
        originalHeight,
        resized: true,
        reason: needsDimensionResize ? 'dimensions' : 'bytes',
      };
    } catch (err) {
      console.warn('[image-normalize] using original image:', err && err.message ? err.message : err);
      return {
        blob,
        filename: opts.filename || blob.name || filenameWithMime('image', blob.type),
        mimeType: blob.type || 'image/png',
        resized: false,
        error: err && err.message ? err.message : String(err || ''),
      };
    } finally {
      if (decoded && typeof decoded.close === 'function') decoded.close();
    }
  }

  global.CTMImage = Object.assign({}, global.CTMImage || {}, {
    DEFAULT_MAX_LONG_EDGE,
    DEFAULT_MAX_BYTES,
    extensionForMime,
    normalizeForProvider,
  });
})(window);
