import { defu } from 'defu';
import { withLeadingSlash, hasProtocol, joinURL, decode } from 'ufo';
import { createError, defineEventHandler, setResponseStatus, createApp, toWebHandler, toNodeListener, toPlainHandler, getRequestHeader, appendResponseHeader, send, getResponseHeader, setResponseHeader } from 'h3';
import { imageMeta } from 'image-meta';
import destr from 'destr';
import { negotiate } from '@fastify/accept-negotiator';
import getEtag from 'etag';
import { ofetch } from 'ofetch';
import { resolve, join, parse } from 'pathe';

const Handlers = {
  __proto__: null,
  get b () { return b; },
  get background () { return background; },
  get blur () { return blur; },
  get crop () { return crop; },
  get enlarge () { return enlarge; },
  get extend () { return extend; },
  get extract () { return extract; },
  get fit () { return fit; },
  get flatten () { return flatten; },
  get flip () { return flip; },
  get flop () { return flop; },
  get gamma () { return gamma; },
  get grayscale () { return grayscale; },
  get h () { return h; },
  get height () { return height; },
  get kernel () { return kernel; },
  get median () { return median; },
  get modulate () { return modulate; },
  get negate () { return negate; },
  get normalize () { return normalize; },
  get pos () { return pos; },
  get position () { return position; },
  get q () { return q; },
  get quality () { return quality; },
  get resize () { return resize; },
  get rotate () { return rotate; },
  get s () { return s; },
  get sharpen () { return sharpen; },
  get threshold () { return threshold; },
  get tint () { return tint; },
  get trim () { return trim; },
  get w () { return w; },
  get width () { return width; }
};

function VArg(argument) {
  return destr(argument);
}
function parseArgs(arguments_, mappers) {
  const vargs = arguments_.split("_");
  return mappers.map((v, index) => v(vargs[index]));
}
function getHandler(key) {
  return Handlers[key];
}
function applyHandler(context, pipe, handler, argumentsString) {
  const arguments_ = handler.args ? parseArgs(argumentsString, handler.args) : [];
  return handler.apply(context, pipe, ...arguments_);
}
function clampDimensionsPreservingAspectRatio(sourceDimensions, desiredDimensions) {
  const desiredAspectRatio = desiredDimensions.width / desiredDimensions.height;
  let { width, height } = desiredDimensions;
  if (sourceDimensions.width && width > sourceDimensions.width) {
    width = sourceDimensions.width;
    height = Math.round(sourceDimensions.width / desiredAspectRatio);
  }
  if (sourceDimensions.height && height > sourceDimensions.height) {
    height = sourceDimensions.height;
    width = Math.round(sourceDimensions.height * desiredAspectRatio);
  }
  return { width, height };
}

const quality = {
  args: [VArg],
  order: -1,
  apply: (context, _pipe, quality2) => {
    context.quality = quality2;
  }
};
const fit = {
  args: [VArg],
  order: -1,
  apply: (context, _pipe, fit2) => {
    context.fit = fit2;
  }
};
const position = {
  args: [VArg],
  order: -1,
  apply: (context, _pipe, position2) => {
    context.position = position2;
  }
};
const HEX_RE = /^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i;
const SHORTHEX_RE = /^([\da-f])([\da-f])([\da-f])$/i;
const background = {
  args: [VArg],
  order: -1,
  apply: (context, _pipe, background2) => {
    background2 = String(background2);
    if (!background2.startsWith("#") && (HEX_RE.test(background2) || SHORTHEX_RE.test(background2))) {
      background2 = "#" + background2;
    }
    context.background = background2;
  }
};
const enlarge = {
  args: [],
  apply: (context) => {
    context.enlarge = true;
  }
};
const kernel = {
  args: [VArg],
  apply: (context, _pipe, kernel2) => {
    context.kernel = kernel2;
  }
};
const width = {
  args: [VArg],
  apply: (context, pipe, width2) => {
    return pipe.resize(width2, void 0, {
      withoutEnlargement: !context.enlarge
    });
  }
};
const height = {
  args: [VArg],
  apply: (context, pipe, height2) => {
    return pipe.resize(void 0, height2, {
      withoutEnlargement: !context.enlarge
    });
  }
};
const resize = {
  args: [VArg, VArg, VArg],
  apply: (context, pipe, size) => {
    let [width2, height2] = String(size).split("x").map(Number);
    if (!width2) {
      return;
    }
    if (!height2) {
      height2 = width2;
    }
    if (!context.enlarge) {
      const clamped = clampDimensionsPreservingAspectRatio(context.meta, {
        width: width2,
        height: height2
      });
      width2 = clamped.width;
      height2 = clamped.height;
    }
    return pipe.resize(width2, height2, {
      fit: context.fit,
      position: context.position,
      background: context.background,
      kernel: context.kernel
    });
  }
};
const trim = {
  args: [VArg],
  apply: (_context, pipe, threshold2) => {
    return pipe.trim(threshold2);
  }
};
const extend = {
  args: [VArg, VArg, VArg, VArg],
  apply: (context, pipe, top, right, bottom, left) => {
    return pipe.extend({
      top,
      left,
      bottom,
      right,
      background: context.background
    });
  }
};
const extract = {
  args: [VArg, VArg, VArg, VArg],
  apply: (_context, pipe, left, top, width2, height2) => {
    return pipe.extract({
      left,
      top,
      width: width2,
      height: height2
    });
  }
};
const rotate = {
  args: [VArg],
  apply: (context, pipe, angel) => {
    return pipe.rotate(angel, {
      background: context.background
    });
  }
};
const flip = {
  args: [],
  apply: (_context, pipe) => {
    return pipe.flip();
  }
};
const flop = {
  args: [],
  apply: (_context, pipe) => {
    return pipe.flop();
  }
};
const sharpen = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe, sigma, flat, jagged) => {
    return pipe.sharpen(sigma, flat, jagged);
  }
};
const median = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe, size) => {
    return pipe.median(size);
  }
};
const blur = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe, sigma) => {
    return pipe.blur(sigma);
  }
};
const flatten = {
  args: [VArg, VArg, VArg],
  apply: (context, pipe) => {
    return pipe.flatten({
      background: context.background
    });
  }
};
const gamma = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe, gamma2, gammaOut) => {
    return pipe.gamma(gamma2, gammaOut);
  }
};
const negate = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe) => {
    return pipe.negate();
  }
};
const normalize = {
  args: [VArg, VArg, VArg],
  apply: (_context, pipe) => {
    return pipe.normalize();
  }
};
const threshold = {
  args: [VArg],
  apply: (_context, pipe, threshold2) => {
    return pipe.threshold(threshold2);
  }
};
const modulate = {
  args: [VArg],
  apply: (_context, pipe, brightness, saturation, hue) => {
    return pipe.modulate({
      brightness,
      saturation,
      hue
    });
  }
};
const tint = {
  args: [VArg],
  apply: (_context, pipe, rgb) => {
    return pipe.tint(rgb);
  }
};
const grayscale = {
  args: [VArg],
  apply: (_context, pipe) => {
    return pipe.grayscale();
  }
};
const crop = extract;
const q = quality;
const b = background;
const w = width;
const h = height;
const s = resize;
const pos = position;

function getEnv(name) {
  return name in process.env ? destr(process.env[name]) : void 0;
}
function cachedPromise(function_) {
  let p;
  return (...arguments_) => {
    if (p) {
      return p;
    }
    p = Promise.resolve(function_(...arguments_));
    return p;
  };
}

const SUPPORTED_FORMATS = /* @__PURE__ */ new Set([
  "jpeg",
  "png",
  "webp",
  "avif",
  "tiff",
  "heif",
  "gif",
  "heic"
]);
function createIPX(userOptions) {
  const options = defu(userOptions, {
    alias: getEnv("IPX_ALIAS") || {},
    maxAge: getEnv("IPX_MAX_AGE") ?? 60,
    sharpOptions: {
      jpegProgressive: true
    }
  });
  options.alias = Object.fromEntries(
    Object.entries(options.alias || {}).map((e) => [
      withLeadingSlash(e[0]),
      e[1]
    ])
  );
  const getSharp = cachedPromise(async () => {
    return await import('sharp').then(
      (r) => r.default || r
    );
  });
  const getSVGO = cachedPromise(async () => {
    const { optimize } = await import('svgo');
    return { optimize };
  });
  return function ipx(id, modifiers = {}, opts = {}) {
    if (!id) {
      throw createError({
        statusCode: 400,
        statusText: `IPX_MISSING_ID`,
        message: `Resource id is missing`
      });
    }
    id = hasProtocol(id) ? id : withLeadingSlash(id);
    for (const base in options.alias) {
      if (id.startsWith(base)) {
        id = joinURL(options.alias[base], id.slice(base.length));
      }
    }
    const storage = hasProtocol(id) ? options.httpStorage || options.storage : options.storage || options.httpStorage;
    if (!storage) {
      throw createError({
        statusCode: 500,
        statusText: `IPX_NO_STORAGE`,
        message: "No storage configured!"
      });
    }
    const getSourceMeta = cachedPromise(async () => {
      const sourceMeta = await storage.getMeta(id, opts);
      if (!sourceMeta) {
        throw createError({
          statusCode: 404,
          statusText: `IPX_RESOURCE_NOT_FOUND`,
          message: `Resource not found: ${id}`
        });
      }
      const _maxAge = sourceMeta.maxAge ?? options.maxAge;
      return {
        maxAge: typeof _maxAge === "string" ? Number.parseInt(_maxAge) : _maxAge,
        mtime: sourceMeta.mtime ? new Date(sourceMeta.mtime) : void 0
      };
    });
    const getSourceData = cachedPromise(async () => {
      const sourceData = await storage.getData(id, opts);
      if (!sourceData) {
        throw createError({
          statusCode: 404,
          statusText: `IPX_RESOURCE_NOT_FOUND`,
          message: `Resource not found: ${id}`
        });
      }
      return Buffer.from(sourceData);
    });
    const process = cachedPromise(async () => {
      const sourceData = await getSourceData();
      let imageMeta$1;
      try {
        imageMeta$1 = imageMeta(sourceData);
      } catch {
        throw createError({
          statusCode: 400,
          statusText: `IPX_INVALID_IMAGE`,
          message: `Cannot parse image metadata: ${id}`
        });
      }
      let mFormat = modifiers.f || modifiers.format;
      if (mFormat === "jpg") {
        mFormat = "jpeg";
      }
      const format = mFormat && SUPPORTED_FORMATS.has(mFormat) ? mFormat : SUPPORTED_FORMATS.has(imageMeta$1.type || "") ? imageMeta$1.type : "jpeg";
      if (imageMeta$1.type === "svg" && !mFormat) {
        if (options.svgo === false) {
          return {
            data: sourceData,
            format: "svg+xml",
            meta: imageMeta$1
          };
        } else {
          const { optimize } = await getSVGO();
          const svg = optimize(sourceData.toString("utf8"), {
            ...options.svgo,
            plugins: ["removeScriptElement", ...options.svgo?.plugins || []]
          }).data;
          return {
            data: svg,
            format: "svg+xml",
            meta: imageMeta$1
          };
        }
      }
      const animated = modifiers.animated !== void 0 || modifiers.a !== void 0 || format === "gif";
      const Sharp = await getSharp();
      let sharp = Sharp(sourceData, { animated, ...options.sharpOptions });
      Object.assign(
        sharp.options,
        options.sharpOptions
      );
      const handlers = Object.entries(modifiers).map(([name, arguments_]) => ({
        handler: getHandler(name),
        name,
        args: arguments_
      })).filter((h) => h.handler).sort((a, b) => {
        const aKey = (a.handler.order || a.name || "").toString();
        const bKey = (b.handler.order || b.name || "").toString();
        return aKey.localeCompare(bKey);
      });
      const handlerContext = { meta: imageMeta$1 };
      for (const h of handlers) {
        sharp = applyHandler(handlerContext, sharp, h.handler, h.args) || sharp;
      }
      if (SUPPORTED_FORMATS.has(format || "")) {
        sharp = sharp.toFormat(format, {
          quality: handlerContext.quality
        });
      }
      const processedImage = await sharp.toBuffer();
      return {
        data: processedImage,
        format,
        meta: imageMeta$1
      };
    });
    return {
      getSourceMeta,
      process
    };
  };
}

const MODIFIER_SEP = /[&,]/g;
const MODIFIER_VAL_SEP = /[:=_]/;
function createIPXH3Handler(ipx) {
  const _handler = async (event) => {
    const [modifiersString = "", ...idSegments] = event.path.slice(
      1
      /* leading slash */
    ).split("/");
    const id = safeString(decode(idSegments.join("/")));
    if (!modifiersString) {
      throw createError({
        statusCode: 400,
        statusText: `IPX_MISSING_MODIFIERS`,
        message: `Modifiers are missing: ${id}`
      });
    }
    if (!id || id === "/") {
      throw createError({
        statusCode: 400,
        statusText: `IPX_MISSING_ID`,
        message: `Resource id is missing: ${event.path}`
      });
    }
    const modifiers = /* @__PURE__ */ Object.create(null);
    if (modifiersString !== "_") {
      for (const p of modifiersString.split(MODIFIER_SEP)) {
        const [key, ...values] = p.split(MODIFIER_VAL_SEP);
        modifiers[safeString(key)] = values.map((v) => safeString(decode(v))).join("_");
      }
    }
    const mFormat = modifiers.f || modifiers.format;
    if (mFormat === "auto") {
      const acceptHeader = getRequestHeader(event, "accept") || "";
      const autoFormat = autoDetectFormat(
        acceptHeader,
        !!(modifiers.a || modifiers.animated)
      );
      delete modifiers.f;
      delete modifiers.format;
      if (autoFormat) {
        modifiers.format = autoFormat;
        appendResponseHeader(event, "vary", "Accept");
      }
    }
    const img = ipx(id, modifiers);
    const sourceMeta = await img.getSourceMeta();
    sendResponseHeaderIfNotSet(
      event,
      "content-security-policy",
      "default-src 'none'"
    );
    if (sourceMeta.mtime) {
      sendResponseHeaderIfNotSet(
        event,
        "last-modified",
        sourceMeta.mtime.toUTCString()
      );
      const _ifModifiedSince = getRequestHeader(event, "if-modified-since");
      if (_ifModifiedSince && new Date(_ifModifiedSince) >= sourceMeta.mtime) {
        setResponseStatus(event, 304);
        return send(event);
      }
    }
    const { data, format } = await img.process();
    if (typeof sourceMeta.maxAge === "number") {
      sendResponseHeaderIfNotSet(
        event,
        "cache-control",
        `max-age=${+sourceMeta.maxAge}, public, s-maxage=${+sourceMeta.maxAge}`
      );
    }
    const etag = getEtag(data);
    sendResponseHeaderIfNotSet(event, "etag", etag);
    if (etag && getRequestHeader(event, "if-none-match") === etag) {
      setResponseStatus(event, 304);
      return send(event);
    }
    if (format) {
      sendResponseHeaderIfNotSet(event, "content-type", `image/${format}`);
    }
    return data;
  };
  return defineEventHandler(async (event) => {
    try {
      return await _handler(event);
    } catch (_error) {
      const error = createError(_error);
      setResponseStatus(event, error.statusCode, error.statusMessage);
      return {
        error: {
          message: `[${error.statusCode}] [${error.statusMessage || "IPX_ERROR"}] ${error.message}`
        }
      };
    }
  });
}
function createIPXH3App(ipx) {
  const app = createApp({ debug: true });
  app.use(createIPXH3Handler(ipx));
  return app;
}
function createIPXWebServer(ipx) {
  return toWebHandler(createIPXH3App(ipx));
}
function createIPXNodeServer(ipx) {
  return toNodeListener(createIPXH3App(ipx));
}
function createIPXPlainServer(ipx) {
  return toPlainHandler(createIPXH3App(ipx));
}
function sendResponseHeaderIfNotSet(event, name, value) {
  if (!getResponseHeader(event, name)) {
    setResponseHeader(event, name, value);
  }
}
function autoDetectFormat(acceptHeader, animated) {
  if (animated) {
    const acceptMime2 = negotiate(acceptHeader, ["image/webp", "image/gif"]);
    return acceptMime2?.split("/")[1] || "gif";
  }
  const acceptMime = negotiate(acceptHeader, [
    "image/avif",
    "image/webp",
    "image/jpeg",
    "image/png",
    "image/tiff",
    "image/heif",
    "image/gif"
  ]);
  return acceptMime?.split("/")[1] || "jpeg";
}
function safeString(input) {
  return JSON.stringify(input).replace(/^"|"$/g, "").replace(/\\+/g, "\\").replace(/\\"/g, '"');
}

const HTTP_RE = /^https?:\/\//;
function ipxHttpStorage(_options = {}) {
  const allowAllDomains = _options.allowAllDomains ?? getEnv("IPX_HTTP_ALLOW_ALL_DOMAINS") ?? false;
  let _domains = _options.domains || getEnv("IPX_HTTP_DOMAINS") || [];
  const defaultMaxAge = _options.maxAge || getEnv("IPX_HTTP_MAX_AGE") || 300;
  const fetchOptions = _options.fetchOptions || getEnv("IPX_HTTP_FETCH_OPTIONS") || {};
  if (typeof _domains === "string") {
    _domains = _domains.split(",").map((s) => s.trim());
  }
  const domains = new Set(
    _domains.map((d) => {
      if (!HTTP_RE.test(d)) {
        d = "http://" + d;
      }
      return new URL(d).hostname;
    }).filter(Boolean)
  );
  function validateId(id) {
    const url = new URL(decodeURIComponent(id));
    if (!url.hostname) {
      throw createError({
        statusCode: 403,
        statusText: `IPX_MISSING_HOSTNAME`,
        message: `Hostname is missing: ${id}`
      });
    }
    if (!allowAllDomains && !domains.has(url.hostname)) {
      throw createError({
        statusCode: 403,
        statusText: `IPX_FORBIDDEN_HOST`,
        message: `Forbidden host: ${url.hostname}`
      });
    }
    return url.toString();
  }
  function parseResponse(response) {
    let maxAge = defaultMaxAge;
    if (_options.ignoreCacheControl) {
      const _cacheControl = response.headers.get("cache-control");
      if (_cacheControl) {
        const m = _cacheControl.match(/max-age=(\d+)/);
        if (m && m[1]) {
          maxAge = Number.parseInt(m[1]);
        }
      }
    }
    let mtime;
    const _lastModified = response.headers.get("last-modified");
    if (_lastModified) {
      mtime = new Date(_lastModified);
    }
    return { maxAge, mtime };
  }
  return {
    name: "ipx:http",
    async getMeta(id) {
      const url = validateId(id);
      try {
        const response = await ofetch.raw(url, {
          ...fetchOptions,
          method: "HEAD"
        });
        const { maxAge, mtime } = parseResponse(response);
        return { mtime, maxAge };
      } catch {
        return {};
      }
    },
    async getData(id) {
      const url = validateId(id);
      const response = await ofetch(url, {
        ...fetchOptions,
        method: "GET",
        responseType: "arrayBuffer"
      });
      return response;
    }
  };
}

function ipxFSStorage(_options = {}) {
  const dirs = resolveDirs(_options.dir);
  const maxAge = _options.maxAge || getEnv("IPX_FS_MAX_AGE");
  const _getFS = cachedPromise(
    () => import('node:fs/promises').catch(() => {
      throw createError({
        statusCode: 500,
        statusText: `IPX_FILESYSTEM_ERROR`,
        message: `Failed to resolve filesystem module`
      });
    })
  );
  const resolveFile = async (id) => {
    const fs = await _getFS();
    for (const dir of dirs) {
      const filePath = join(dir, id);
      if (!isValidPath(filePath) || !filePath.startsWith(dir + "/")) {
        throw createError({
          statusCode: 403,
          statusText: `IPX_FORBIDDEN_PATH`,
          message: `Forbidden path: ${id}`
        });
      }
      try {
        const stats = await fs.stat(filePath);
        if (!stats.isFile()) {
          continue;
        }
        return {
          stats,
          read: () => fs.readFile(filePath)
        };
      } catch (error) {
        if (error.code === "ENOENT") {
          continue;
        }
        throw createError({
          statusCode: 403,
          statusText: `IPX_FORBIDDEN_FILE`,
          message: `Cannot access file: ${id}`
        });
      }
    }
    throw createError({
      statusCode: 404,
      statusText: `IPX_FILE_NOT_FOUND`,
      message: `File not found: ${id}`
    });
  };
  return {
    name: "ipx:node-fs",
    async getMeta(id) {
      const { stats } = await resolveFile(id);
      return {
        mtime: stats.mtime,
        maxAge
      };
    },
    async getData(id) {
      const { read } = await resolveFile(id);
      return read();
    }
  };
}
const isWindows = process.platform === "win32";
function isValidPath(fp) {
  if (isWindows) {
    fp = fp.slice(parse(fp).root.length);
  }
  if (/["*:<>?|]/.test(fp)) {
    return false;
  }
  return true;
}
function resolveDirs(dirs) {
  if (!dirs || !Array.isArray(dirs)) {
    const dir = resolve(dirs || getEnv("IPX_FS_DIR") || ".");
    return [dir];
  }
  return dirs.map((dirs2) => resolve(dirs2));
}

export { createIPXH3Handler as a, createIPXH3App as b, createIPX as c, createIPXWebServer as d, createIPXNodeServer as e, createIPXPlainServer as f, ipxFSStorage as g, ipxHttpStorage as i };
