(function (root, factory) {
  const service = factory();
  if (typeof module === "object" && module.exports) {
    module.exports = service;
  } else {
    root.MealSnapshotService = service;
  }
}(typeof globalThis !== "undefined" ? globalThis : this, function () {
  "use strict";

  const SUPPORTED_IMAGE_TYPES = Object.freeze(["image/jpeg", "image/png", "image/webp"]);
  const DEFAULT_MAX_IMAGE_BYTES = 10 * 1024 * 1024;
  const DEFAULT_TIMEOUT_MS = 20_000;
  const SAFE_FALLBACK_ACTIONS = Object.freeze(["retry", "manual_entry", "save_note_only"]);

  function fallbackMetadata(actions = SAFE_FALLBACK_ACTIONS) {
    return Object.freeze({
      available: true,
      actions: Object.freeze(Array.from(actions)),
      message: "You can retry, enter the meal manually, or save a note without an estimate."
    });
  }

  function failure(status, options = {}) {
    return Object.freeze({
      ok: false,
      status,
      httpStatus: options.httpStatus || 0,
      providerStatus: options.providerStatus || null,
      retryAfterSeconds: options.retryAfterSeconds || null,
      manualFallback: options.manualFallback || fallbackMetadata(),
      message: options.message || "Meal Snapshot could not complete this request."
    });
  }

  function emitSafeEvent(logger, event, metadata = {}) {
    if (typeof logger !== "function") return;
    const safe = {
      event,
      status: String(metadata.status || "unknown").slice(0, 80),
      httpStatus: Number(metadata.httpStatus || 0),
      durationMs: Math.max(0, Number(metadata.durationMs || 0))
    };
    logger(Object.freeze(safe));
  }

  function validateFile(file, maxImageBytes) {
    if (!file || typeof file !== "object") {
      return failure("file_required", {
        message: "Choose a meal photo to continue.",
        manualFallback: fallbackMetadata(["manual_entry", "save_note_only"])
      });
    }
    if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {
      return failure("unsupported_media_type", {
        httpStatus: 415,
        message: "Use a JPEG, PNG, or WebP image.",
        manualFallback: fallbackMetadata(["choose_another_photo", "manual_entry", "save_note_only"])
      });
    }
    if (!Number.isFinite(file.size) || file.size <= 0) {
      return failure("invalid_file", {
        message: "The selected photo is empty or invalid.",
        manualFallback: fallbackMetadata(["choose_another_photo", "manual_entry", "save_note_only"])
      });
    }
    if (file.size > maxImageBytes) {
      return failure("payload_too_large", {
        httpStatus: 413,
        message: "The selected photo is too large.",
        manualFallback: fallbackMetadata(["choose_another_photo", "manual_entry", "save_note_only"])
      });
    }
    return null;
  }

  function hasValidManualFallback(value) {
    return Boolean(value)
      && value.available === true
      && Array.isArray(value.actions)
      && value.actions.every((action) => typeof action === "string");
  }

  function isNonNegativeNumber(value) {
    return Number.isFinite(value) && value >= 0;
  }

  function hasOnlyKeys(value, allowedKeys) {
    return Boolean(value)
      && typeof value === "object"
      && !Array.isArray(value)
      && Object.keys(value).every((key) => allowedKeys.includes(key));
  }

  function isValidIngredient(value) {
    return hasOnlyKeys(value, ["name", "amount", "confidence"])
      && typeof value.name === "string"
      && typeof value.amount === "string"
      && isNonNegativeNumber(value.confidence)
      && value.confidence <= 1;
  }

  function isValidReviewResponse(payload) {
    const estimate = payload && payload.estimate;
    return hasOnlyKeys(payload, [
      "schemaVersion",
      "status",
      "providerStatus",
      "requiresUserReview",
      "reviewReasons",
      "estimate",
      "manualFallback",
      "shouldSave"
    ])
      && payload.schemaVersion === "1.0"
      && payload.status === "review_needed"
      && payload.providerStatus === "mock_success"
      && payload.requiresUserReview === true
      && payload.shouldSave === false
      && Array.isArray(payload.reviewReasons)
      && payload.reviewReasons.every((reason) => typeof reason === "string")
      && hasValidManualFallback(payload.manualFallback)
      && hasOnlyKeys(estimate, [
        "title",
        "confidenceLabel",
        "calories",
        "protein",
        "carbs",
        "fat",
        "ingredients",
        "leanerNote",
        "disclaimer",
        "source"
      ])
      && typeof estimate.title === "string"
      && Array.isArray(estimate.ingredients)
      && estimate.ingredients.every(isValidIngredient)
      && isNonNegativeNumber(estimate.calories)
      && isNonNegativeNumber(estimate.protein)
      && isNonNegativeNumber(estimate.carbs)
      && isNonNegativeNumber(estimate.fat)
      && typeof estimate.confidenceLabel === "string"
      && typeof estimate.leanerNote === "string"
      && typeof estimate.disclaimer === "string"
      && typeof estimate.source === "string";
  }

  function isValidProviderUnavailableResponse(payload) {
    return hasOnlyKeys(payload, [
      "schemaVersion",
      "status",
      "providerStatus",
      "requiresUserReview",
      "estimate",
      "manualFallback",
      "message",
      "shouldSave"
    ])
      && payload.schemaVersion === "1.0"
      && payload.status === "fail_safe"
      && payload.providerStatus === "provider_unavailable"
      && payload.requiresUserReview === true
      && payload.estimate === null
      && payload.shouldSave === false
      && hasValidManualFallback(payload.manualFallback);
  }

  function normalizedEstimate(estimate) {
    return Object.freeze({
      title: String(estimate.title),
      confidenceLabel: String(estimate.confidenceLabel),
      calories: estimate.calories,
      protein: estimate.protein,
      carbs: estimate.carbs,
      fat: estimate.fat,
      ingredients: Object.freeze(estimate.ingredients.map((ingredient) => Object.freeze({
        name: String(ingredient.name),
        amount: String(ingredient.amount),
        confidence: ingredient.confidence
      }))),
      leanerNote: String(estimate.leanerNote),
      disclaimer: String(estimate.disclaimer),
      source: String(estimate.source)
    });
  }

  function privateUploadFilename(mimeType) {
    const extensions = {
      "image/jpeg": "jpg",
      "image/png": "png",
      "image/webp": "webp"
    };
    return `meal-photo.${extensions[mimeType]}`;
  }

  function retryAfterSeconds(response) {
    const raw = response.headers?.get?.("retry-after");
    const seconds = Number(raw);
    return Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds) : null;
  }

  function httpFailure(response, payload) {
    const status = response.status;
    if (status === 401) {
      return failure("unauthorized", { httpStatus: status, message: "Please sign in again to use Meal Snapshot." });
    }
    if (status === 413) {
      return failure("payload_too_large", {
        httpStatus: status,
        message: "The selected photo is too large.",
        manualFallback: fallbackMetadata(["choose_another_photo", "manual_entry", "save_note_only"])
      });
    }
    if (status === 415) {
      return failure("unsupported_media_type", {
        httpStatus: status,
        message: "Use a JPEG, PNG, or WebP image.",
        manualFallback: fallbackMetadata(["choose_another_photo", "manual_entry", "save_note_only"])
      });
    }
    if (status === 429) {
      return failure("rate_limited", {
        httpStatus: status,
        retryAfterSeconds: retryAfterSeconds(response),
        message: "You have reached the current snapshot limit. Try again later or enter the meal manually."
      });
    }
    if (status === 503 && isValidProviderUnavailableResponse(payload)) {
      return failure("provider_unavailable", {
        httpStatus: status,
        providerStatus: payload.providerStatus,
        manualFallback: Object.freeze({
          available: true,
          actions: Object.freeze(Array.from(payload.manualFallback.actions)),
          message: String(payload.manualFallback.message || "Meal Snapshot is temporarily unavailable.")
        }),
        message: String(payload.message || "Meal Snapshot is temporarily unavailable. Your photo was not saved.")
      });
    }
    return failure("request_failed", {
      httpStatus: status,
      message: "Meal Snapshot could not complete this request."
    });
  }

  async function readJsonSafely(response) {
    try {
      return await response.json();
    } catch {
      return null;
    }
  }

  function createMealSnapshotClient(options = {}) {
    const feature = options.featureConfig || {};
    const fetchImpl = options.fetchImpl || (typeof fetch === "function" ? fetch.bind(root) : null);
    const getAuthToken = options.getAuthToken;
    const logger = options.logger;
    const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
    const maxImageBytes = Number.isFinite(options.maxImageBytes)
      ? options.maxImageBytes
      : DEFAULT_MAX_IMAGE_BYTES;
    const createController = options.createAbortController || (() => new AbortController());
    const scheduleTimeout = options.setTimeoutImpl || setTimeout;
    const cancelTimeout = options.clearTimeoutImpl || clearTimeout;
    const now = options.now || Date.now;

    return Object.freeze({
      async analyzeMealPhoto({ file, signal } = {}) {
        const startedAt = now();
        if (feature.enabled !== true) {
          return failure("feature_disabled", {
            message: "Meal Snapshot Beta is not enabled."
          });
        }
        if (!feature.endpoint || typeof feature.endpoint !== "string") {
          return failure("configuration_error", {
            message: "Meal Snapshot Beta is not configured."
          });
        }

        const fileFailure = validateFile(file, maxImageBytes);
        if (fileFailure) return fileFailure;
        if (typeof fetchImpl !== "function") {
          return failure("network_unavailable", { message: "A network connection is not available." });
        }
        if (typeof getAuthToken !== "function") {
          return failure("auth_required", { message: "Authentication is required for Meal Snapshot." });
        }

        let token;
        try {
          token = await getAuthToken();
        } catch {
          return failure("auth_required", { message: "Authentication is required for Meal Snapshot." });
        }
        if (typeof token !== "string" || !token.trim()) {
          return failure("auth_required", { message: "Authentication is required for Meal Snapshot." });
        }

        const controller = createController();
        let timedOut = false;
        const forwardAbort = () => controller.abort();
        if (signal?.aborted) controller.abort();
        else signal?.addEventListener?.("abort", forwardAbort, { once: true });
        const timeoutId = scheduleTimeout(() => {
          timedOut = true;
          controller.abort();
        }, timeoutMs);

        const formData = new FormData();
        // Never forward a user filename, which may contain identifying details.
        formData.append("photo", file, privateUploadFilename(file.type));

        try {
          const response = await fetchImpl(feature.endpoint, {
            method: "POST",
            headers: { Authorization: `Bearer ${token}` },
            body: formData,
            signal: controller.signal
          });
          const payload = await readJsonSafely(response);
          let normalized;

          if (response.status === 200 && isValidReviewResponse(payload)) {
            normalized = Object.freeze({
              ok: true,
              status: "review_needed",
              httpStatus: 200,
              providerStatus: payload.providerStatus,
              requiresUserReview: true,
              reviewReasons: Object.freeze(Array.from(payload.reviewReasons)),
              estimate: normalizedEstimate(payload.estimate),
              manualFallback: Object.freeze({
                available: true,
                actions: Object.freeze(Array.from(payload.manualFallback.actions)),
                message: String(payload.manualFallback.message || "")
              }),
              shouldSave: false
            });
          } else if (response.ok || response.status === 503) {
            normalized = response.status === 503
              ? httpFailure(response, payload)
              : failure("invalid_response", {
                httpStatus: response.status,
                message: "Meal Snapshot returned an invalid response. Enter the meal manually or try again."
              });
          } else {
            normalized = httpFailure(response, payload);
          }

          emitSafeEvent(logger, "meal_snapshot_request_finished", {
            status: normalized.status,
            httpStatus: response.status,
            durationMs: now() - startedAt
          });
          return normalized;
        } catch (error) {
          const aborted = error?.name === "AbortError" || controller.signal.aborted;
          const normalized = aborted
            ? failure(timedOut ? "timeout" : "aborted", {
              message: timedOut ? "Meal Snapshot timed out. Try again or enter the meal manually." : "Meal Snapshot was canceled."
            })
            : failure("network_failure", {
              message: "Meal Snapshot could not connect. Try again or enter the meal manually."
            });
          emitSafeEvent(logger, "meal_snapshot_request_finished", {
            status: normalized.status,
            httpStatus: 0,
            durationMs: now() - startedAt
          });
          return normalized;
        } finally {
          cancelTimeout(timeoutId);
          signal?.removeEventListener?.("abort", forwardAbort);
        }
      }
    });
  }

  return Object.freeze({
    SUPPORTED_IMAGE_TYPES,
    DEFAULT_MAX_IMAGE_BYTES,
    createMealSnapshotClient,
    validateFile,
    isValidReviewResponse,
    isValidProviderUnavailableResponse
  });
}));
