import { mkdirSync, writeFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { isAbsolute } from "node:path";
import { fileURLToPath } from "node:url";
import { removeTrailingForwardSlash } from "@astrojs/internal-helpers/path";
import { createStorage } from "unstorage";
import fsLiteDriver from "unstorage/drivers/fs-lite";
import xxhash from "xxhash-wasm";
import { collectErrorMetadata } from "../../core/errors/dev/utils.js";
import { AstroError, AstroErrorData, isAstroError } from "../../core/errors/index.js";
import { formatErrorMessage } from "../../core/messages.js";
import { getClientOutputDirectory } from "../../prerender/utils.js";
import {
  CACHE_DIR,
  RESOLVED_VIRTUAL_MODULE_ID,
  URL_PREFIX,
  VIRTUAL_MODULE_ID
} from "./constants.js";
import { loadFonts } from "./load.js";
import { generateFallbackFontFace, readMetrics } from "./metrics.js";
import {
  cache,
  extractFontType,
  resolveFontFamily,
  sortObjectByKey,
  withoutQuotes
} from "./utils.js";
async function fetchFont(url) {
  try {
    if (isAbsolute(url)) {
      return await readFile(url);
    }
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response was not successful, received status code ${response.status}`);
    }
    return Buffer.from(await response.arrayBuffer());
  } catch (cause) {
    throw new AstroError(
      {
        ...AstroErrorData.CannotFetchFontFile,
        message: AstroErrorData.CannotFetchFontFile.message(url)
      },
      { cause }
    );
  }
}
function fontsPlugin({ settings, sync, logger }) {
  if (!settings.config.experimental.fonts) {
    return {
      name: "astro:fonts:fallback",
      resolveId(id) {
        if (id === VIRTUAL_MODULE_ID) {
          return RESOLVED_VIRTUAL_MODULE_ID;
        }
      },
      load(id) {
        if (id === RESOLVED_VIRTUAL_MODULE_ID) {
          return {
            code: ""
          };
        }
      }
    };
  }
  const baseUrl = removeTrailingForwardSlash(settings.config.base) + URL_PREFIX;
  let resolvedMap = null;
  let hashToUrlMap = null;
  let isBuild;
  let storage = null;
  const cleanup = () => {
    resolvedMap = null;
    hashToUrlMap = null;
    storage = null;
  };
  async function initialize({ resolveMod, base }) {
    const { h64ToString } = await xxhash();
    storage = createStorage({
      // Types are weirly exported
      driver: fsLiteDriver({
        base: fileURLToPath(base)
      })
    });
    hashToUrlMap = /* @__PURE__ */ new Map();
    resolvedMap = /* @__PURE__ */ new Map();
    const families = [];
    for (const family of settings.config.experimental.fonts) {
      families.push(
        await resolveFontFamily({
          family,
          root: settings.config.root,
          resolveMod,
          generateNameWithHash: (_family) => `${withoutQuotes(_family.name)}-${h64ToString(JSON.stringify(sortObjectByKey(_family)))}`
        })
      );
    }
    await loadFonts({
      base: baseUrl,
      families,
      storage,
      hashToUrlMap,
      resolvedMap,
      hashString: h64ToString,
      generateFallbackFontFace,
      getMetricsForFamily: async (name, font) => {
        return await readMetrics(name, await cache(storage, font.hash, () => fetchFont(font.url)));
      },
      log: (message) => logger.info("assets", message)
    });
  }
  return {
    name: "astro:fonts",
    config(_, { command }) {
      isBuild = command === "build";
    },
    async buildStart() {
      if (isBuild) {
        await initialize({
          resolveMod: (id) => import(id),
          base: new URL(CACHE_DIR, settings.config.cacheDir)
        });
      }
    },
    async configureServer(server) {
      await initialize({
        resolveMod: (id) => server.ssrLoadModule(id),
        // In dev, we cache fonts data in .astro so it can be easily inspected and cleared
        base: new URL(CACHE_DIR, settings.dotAstroDir)
      });
      const localPaths = [...hashToUrlMap.values()].filter((url) => isAbsolute(url));
      server.watcher.on("change", (path) => {
        if (localPaths.includes(path)) {
          logger.info("assets", "Font file updated");
          server.restart();
        }
      });
      server.watcher.on("unlink", (path) => {
        if (localPaths.includes(path)) {
          logger.warn(
            "assets",
            `The font file ${JSON.stringify(path)} referenced in your config has been deleted. Restore the file or remove this font from your configuration if it is no longer needed.`
          );
        }
      });
      server.middlewares.use(URL_PREFIX, async (req, res, next) => {
        if (!req.url) {
          return next();
        }
        const hash = req.url.slice(1);
        const url = hashToUrlMap?.get(hash);
        if (!url) {
          return next();
        }
        res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0");
        res.setHeader("Pragma", "no-cache");
        res.setHeader("Expires", 0);
        try {
          const data = await cache(storage, hash, () => fetchFont(url));
          res.setHeader("Content-Length", data.length);
          res.setHeader("Content-Type", `font/${extractFontType(hash)}`);
          res.end(data);
        } catch (err) {
          logger.error("assets", "Cannot download font file");
          if (isAstroError(err)) {
            logger.error(
              "SKIP_FORMAT",
              formatErrorMessage(collectErrorMetadata(err), logger.level() === "debug")
            );
          }
          res.statusCode = 500;
          res.end();
        }
      });
    },
    resolveId(id) {
      if (id === VIRTUAL_MODULE_ID) {
        return RESOLVED_VIRTUAL_MODULE_ID;
      }
    },
    load(id) {
      if (id === RESOLVED_VIRTUAL_MODULE_ID) {
        return {
          code: `export const fontsData = new Map(${JSON.stringify(Array.from(resolvedMap?.entries() ?? []))})`
        };
      }
    },
    async buildEnd() {
      if (sync || settings.config.experimental.fonts.length === 0) {
        cleanup();
        return;
      }
      try {
        const dir = getClientOutputDirectory(settings);
        const fontsDir = new URL("." + baseUrl, dir);
        try {
          mkdirSync(fontsDir, { recursive: true });
        } catch (cause) {
          throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
        }
        if (hashToUrlMap) {
          logger.info("assets", "Copying fonts...");
          await Promise.all(
            Array.from(hashToUrlMap.entries()).map(async ([hash, url]) => {
              const data = await cache(storage, hash, () => fetchFont(url));
              try {
                writeFileSync(new URL(hash, fontsDir), data);
              } catch (cause) {
                throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
              }
            })
          );
        }
      } finally {
        cleanup();
      }
    }
  };
}
export {
  fontsPlugin
};
