import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { isAbsolute } from "node:path";
import colors from "piccolore";
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../../core/constants.js";
import { getAlgorithm, shouldTrackCspHashes } from "../../core/csp/common.js";
import { generateCspDigest } from "../../core/encryption.js";
import { AstroError, AstroErrorData } from "../../core/errors/index.js";
import { appendForwardSlash, joinPaths, prependForwardSlash } from "../../core/path.js";
import { getClientOutputDirectory } from "../../prerender/utils.js";
import {
  ASSETS_DIR,
  CACHE_DIR,
  DEFAULTS,
  RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID,
  RESOLVED_RUNTIME_VIRTUAL_MODULE_ID,
  RESOLVED_VIRTUAL_MODULE_ID,
  RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID,
  RUNTIME_VIRTUAL_MODULE_ID,
  VIRTUAL_MODULE_ID
} from "./constants.js";
import { collectComponentData } from "./core/collect-component-data.js";
import { collectFontAssetsFromFaces } from "./core/collect-font-assets-from-faces.js";
import { collectFontData } from "./core/collect-font-data.js";
import { computeFontFamiliesAssets } from "./core/compute-font-families-assets.js";
import { filterAndTransformFontFaces } from "./core/filter-and-transform-font-faces.js";
import { getOrCreateFontFamilyAssets } from "./core/get-or-create-font-family-assets.js";
import { optimizeFallbacks } from "./core/optimize-fallbacks.js";
import { resolveFamily } from "./core/resolve-family.js";
import { BuildFontFileIdGenerator } from "./infra/build-font-file-id-generator.js";
import { BuildUrlResolver } from "./infra/build-url-resolver.js";
import { CachedFontFetcher } from "./infra/cached-font-fetcher.js";
import { CapsizeFontMetricsResolver } from "./infra/capsize-font-metrics-resolver.js";
import { DevFontFileIdGenerator } from "./infra/dev-font-file-id-generator.js";
import { DevUrlResolver } from "./infra/dev-url-resolver.js";
import { FsFontFileContentResolver } from "./infra/fs-font-file-content-resolver.js";
import { LevenshteinStringMatcher } from "./infra/levenshtein-string-matcher.js";
import { MinifiableCssRenderer } from "./infra/minifiable-css-renderer.js";
import { NodeFontTypeExtractor } from "./infra/node-font-type-extractor.js";
import { RealSystemFallbacksProvider } from "./infra/system-fallbacks-provider.js";
import { UnifontFontResolver } from "./infra/unifont-font-resolver.js";
import { UnstorageFsStorage } from "./infra/unstorage-fs-storage.js";
import { XxhashHasher } from "./infra/xxhash-hasher.js";
import { fontFileMiddleware, resToMinimalResponse } from "./core/font-file-middleware.js";
function fontsPlugin({ settings, sync, logger }) {
  const assetsDir = prependForwardSlash(
    appendForwardSlash(joinPaths(settings.config.build.assets, ASSETS_DIR))
  );
  const baseUrl = joinPaths(settings.config.base, assetsDir);
  let fontFileById = null;
  let componentDataByCssVariable = null;
  let fontDataByCssVariable = null;
  let fontFetcher = null;
  let fontTypeExtractor = null;
  let built = false;
  let serverAddress = null;
  let urls = null;
  function cleanup() {
    componentDataByCssVariable = null;
    fontDataByCssVariable = null;
    fontFileById = null;
    fontFetcher = null;
    serverAddress = null;
    urls = null;
  }
  return {
    name: "astro:fonts",
    async buildStart() {
      if (sync) {
        return;
      }
      const { root } = settings.config;
      const isBuild = this.environment.config.command === "build";
      const hasher = await XxhashHasher.create();
      const storage = new UnstorageFsStorage({
        // In dev, we cache fonts data in .astro so it can be easily inspected and cleared
        base: new URL(CACHE_DIR, isBuild ? settings.config.cacheDir : settings.dotAstroDir)
      });
      const systemFallbacksProvider = new RealSystemFallbacksProvider();
      fontFetcher = new CachedFontFetcher({ storage, fetch, readFile });
      const cssRenderer = new MinifiableCssRenderer({ minify: isBuild });
      const fontMetricsResolver = new CapsizeFontMetricsResolver({ fontFetcher, cssRenderer });
      fontTypeExtractor = new NodeFontTypeExtractor();
      const stringMatcher = new LevenshteinStringMatcher();
      const urlResolver = isBuild ? new BuildUrlResolver({
        base: baseUrl,
        assetsPrefix: settings.config.build.assetsPrefix,
        searchParams: settings.adapter?.client?.assetQueryParams ?? new URLSearchParams()
      }) : new DevUrlResolver({
        base: baseUrl,
        searchParams: settings.adapter?.client?.assetQueryParams ?? new URLSearchParams()
      });
      const contentResolver = new FsFontFileContentResolver({
        readFileSync: (path) => readFileSync(path, "utf-8")
      });
      const fontFileIdGenerator = isBuild ? new BuildFontFileIdGenerator({
        hasher,
        contentResolver
      }) : new DevFontFileIdGenerator({
        hasher,
        contentResolver
      });
      const { bold } = colors;
      const defaults = DEFAULTS;
      const resolvedFamilies = settings.config.fonts?.map(
        (family) => resolveFamily({ family, hasher })
      ) ?? [];
      const { fontFamilyAssets, fontFileById: _fontFileById } = await computeFontFamiliesAssets({
        resolvedFamilies,
        defaults,
        bold,
        logger,
        stringMatcher,
        fontResolver: await UnifontFontResolver.create({
          families: resolvedFamilies,
          hasher,
          storage,
          root
        }),
        getOrCreateFontFamilyAssets: ({ family, fontFamilyAssetsByUniqueKey }) => getOrCreateFontFamilyAssets({
          family,
          fontFamilyAssetsByUniqueKey,
          bold,
          logger
        }),
        filterAndTransformFontFaces: ({ family, fonts }) => filterAndTransformFontFaces({
          family,
          fonts,
          fontFileIdGenerator,
          fontTypeExtractor,
          urlResolver
        }),
        collectFontAssetsFromFaces: ({ collectedFontsIds, family, fontFilesIds, fonts }) => collectFontAssetsFromFaces({
          collectedFontsIds,
          family,
          fontFilesIds,
          fonts,
          fontFileIdGenerator,
          hasher,
          defaults
        })
      });
      fontDataByCssVariable = collectFontData(fontFamilyAssets);
      componentDataByCssVariable = await collectComponentData({
        cssRenderer,
        defaults,
        fontFamilyAssets,
        optimizeFallbacks: ({ collectedFonts, fallbacks, family }) => optimizeFallbacks({
          collectedFonts,
          fallbacks,
          family,
          fontMetricsResolver,
          systemFallbacksProvider
        })
      });
      fontFileById = _fontFileById;
      if (shouldTrackCspHashes(settings.config.security.csp)) {
        const algorithm = getAlgorithm(settings.config.security.csp);
        for (const { css } of componentDataByCssVariable.values()) {
          settings.injectedCsp.styleHashes.push(await generateCspDigest(css, algorithm));
        }
        for (const resource of urlResolver.cspResources) {
          settings.injectedCsp.fontResources.add(resource);
        }
      }
      urls = urlResolver.urls;
      if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender && fontFileById.size > 0) {
        settings.fontsHttpServer = await new Promise((r) => {
          const server = createServer((req, res) => {
            const next = () => {
              if (!res.writableEnded) {
                res.writeHead(404);
                res.end();
              }
            };
            if (req.url?.startsWith(baseUrl)) {
              return fontFileMiddleware({
                url: req.url.slice(baseUrl.length - 1),
                response: resToMinimalResponse(res),
                next,
                fontFetcher,
                fontFileById,
                fontTypeExtractor,
                logger
              });
            }
            return next();
          }).listen(() => {
            r(server);
          });
        });
        serverAddress = settings.fontsHttpServer.address();
      }
    },
    async configureServer(server) {
      server.httpServer?.on("listening", () => {
        serverAddress = server.httpServer?.address();
      });
      server.watcher.on("change", (path) => {
        if (!fontFileById) {
          return;
        }
        const localPaths = [...fontFileById.values()].filter(({ url }) => isAbsolute(url)).map((v) => v.url);
        if (localPaths.includes(path)) {
          logger.info("assets", "Font file updated");
          server.restart();
        }
      });
      server.watcher.on("unlink", (path) => {
        if (!fontFileById) {
          return;
        }
        const localPaths = [...fontFileById.values()].filter(({ url }) => isAbsolute(url)).map((v) => v.url);
        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(
        assetsDir,
        (req, res, next) => fontFileMiddleware({
          url: req.url,
          response: resToMinimalResponse(res),
          next,
          fontFetcher,
          fontFileById,
          fontTypeExtractor,
          logger
        })
      );
    },
    resolveId: {
      filter: {
        id: new RegExp(
          `^(${VIRTUAL_MODULE_ID}|${RUNTIME_VIRTUAL_MODULE_ID}|${RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID})$`
        )
      },
      handler(id) {
        if (id === VIRTUAL_MODULE_ID) {
          return RESOLVED_VIRTUAL_MODULE_ID;
        }
        if (id === RUNTIME_VIRTUAL_MODULE_ID) {
          return RESOLVED_RUNTIME_VIRTUAL_MODULE_ID;
        }
        if (id === RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID) {
          return RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID;
        }
      }
    },
    load: {
      filter: {
        id: new RegExp(
          `^(${RESOLVED_VIRTUAL_MODULE_ID}|${RESOLVED_RUNTIME_VIRTUAL_MODULE_ID}|${RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID})$`
        )
      },
      async handler(id) {
        if (id === RESOLVED_VIRTUAL_MODULE_ID) {
          return {
            code: `
						export const componentDataByCssVariable = new Map(${JSON.stringify(Array.from(componentDataByCssVariable?.entries() ?? []))});
						export const fontDataByCssVariable = ${JSON.stringify(fontDataByCssVariable ?? {})}
					`
          };
        }
        if (id === RESOLVED_RUNTIME_VIRTUAL_MODULE_ID) {
          return {
            code: `export * from 'astro/assets/fonts/runtime.js';`
          };
        }
        if (id === RESOLVED_RUNTIME_FONT_FILE_URL_RESOLVER_VIRTUAL_MODULE_ID) {
          const isPrerender = this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender;
          if (this.environment.config.command === "build" && !isPrerender) {
            return {
              code: `
								import { SsrRuntimeFontFileUrlResolver } from ${JSON.stringify(new URL("./infra/ssr-runtime-font-file-url-resolver.js", import.meta.url))};
								export const runtimeFontFileUrlResolver = new SsrRuntimeFontFileUrlResolver({
									urls: new Set(${JSON.stringify(urls)}),
								});
							`
            };
          }
          return {
            code: `
							import { RemoteRuntimeFontFileUrlResolver } from ${JSON.stringify(new URL("./infra/remote-runtime-font-file-url-resolver.js", import.meta.url))};
							export const runtimeFontFileUrlResolver = new RemoteRuntimeFontFileUrlResolver({
								urls: new Set(${JSON.stringify(urls)}),
								address: ${JSON.stringify(serverAddress)},
							});
						`
          };
        }
      }
    },
    async buildEnd() {
      if (built) {
        return;
      }
      if (sync || !settings.config.fonts?.length || this.environment.config.command === "serve") {
        cleanup();
        return;
      }
      try {
        const dir = getClientOutputDirectory(settings);
        const fontsDir = new URL(`.${assetsDir}`, dir);
        try {
          mkdirSync(fontsDir, { recursive: true });
        } catch (cause) {
          throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
        }
        if (fontFileById) {
          logger.info(
            "assets",
            `Copying fonts (${fontFileById.size} file${fontFileById.size === 1 ? "" : "s"})...`
          );
          await Promise.all(
            Array.from(fontFileById.entries()).map(async ([id, associatedData]) => {
              const data = await fontFetcher.fetch({ id, ...associatedData });
              try {
                writeFileSync(new URL(id, fontsDir), data);
              } catch (cause) {
                throw new AstroError(AstroErrorData.UnknownFilesystemError, { cause });
              }
            })
          );
        }
      } finally {
        cleanup();
        built = true;
      }
    }
  };
}
export {
  fontsPlugin
};
