import { TestPipeline } from "./pipeline.js";
import { Logger } from "../core/logger/core.js";
import { nodeLogDestination } from "../core/logger/node.js";
import { toRoutingStrategy } from "../i18n/utils.js";
import { validateConfig } from "../core/config/config.js";
import { ASTRO_CONFIG_DEFAULTS } from "../core/config/schema.js";
import { RenderContext } from "../core/render-context.js";
import { posix } from "node:path";
import { getParts, getPattern, validateSegment } from "../core/routing/manifest/create.js";
import { removeLeadingForwardSlash } from "../core/path.js";
import { loadRenderer } from "../core/render/index.js";
function createContainerManifest(renderers, config, manifest, middleware) {
  let i18nManifest = void 0;
  if (config?.i18n) {
    i18nManifest = {
      fallback: config?.i18n.fallback,
      strategy: toRoutingStrategy(config?.i18n.routing, config?.i18n.domains),
      defaultLocale: config?.i18n.defaultLocale,
      locales: config?.i18n.locales,
      domainLookupTable: {}
    };
  }
  const defaultMiddleware = (_, next) => {
    return next();
  };
  return {
    rewritingEnabled: false,
    trailingSlash: manifest?.trailingSlash ?? config?.trailingSlash,
    buildFormat: manifest?.buildFormat ?? config?.build.format,
    compressHTML: manifest?.compressHTML ?? config?.compressHTML,
    assets: manifest?.assets ?? /* @__PURE__ */ new Set(),
    assetsPrefix: manifest?.assetsPrefix ?? void 0,
    entryModules: manifest?.entryModules ?? {},
    routes: manifest?.routes ?? [],
    adapterName: "",
    clientDirectives: manifest?.clientDirectives ?? /* @__PURE__ */ new Map(),
    renderers: manifest?.renderers ?? renderers,
    base: manifest?.base ?? config?.base,
    componentMetadata: manifest?.componentMetadata ?? /* @__PURE__ */ new Map(),
    inlinedScripts: manifest?.inlinedScripts ?? /* @__PURE__ */ new Map(),
    i18n: manifest?.i18n ?? i18nManifest,
    checkOrigin: false,
    middleware: manifest?.middleware ?? middleware ?? defaultMiddleware
  };
}
class unstable_AstroContainer {
  #pipeline;
  #config;
  /**
   * Internally used to check if the container was created with a manifest.
   * @private
   */
  #withManifest = false;
  constructor({
    streaming = false,
    renderers = [],
    config,
    manifest,
    resolve
  }) {
    this.#config = config;
    this.#pipeline = TestPipeline.create({
      logger: new Logger({
        level: "info",
        dest: nodeLogDestination
      }),
      manifest: createContainerManifest(renderers, config, manifest),
      streaming,
      serverLike: true,
      renderers,
      resolve: async (specifier) => {
        if (this.#withManifest) {
          return this.#containerResolve(specifier);
        } else if (resolve) {
          return resolve(specifier);
        }
        return specifier;
      }
    });
  }
  async #containerResolve(specifier) {
    const found = this.#pipeline.manifest.entryModules[specifier];
    if (found) {
      return new URL(found, this.#config.build.client).toString();
    }
    return found;
  }
  /**
   * Creates a new instance of a container.
   * 
   * @param {AstroContainerOptions=} containerOptions
   */
  static async create(containerOptions = {}) {
    const {
      astroConfig = ASTRO_CONFIG_DEFAULTS,
      streaming = false,
      renderers = []
    } = containerOptions;
    const config = await validateConfig(astroConfig, process.cwd(), "container");
    const loadedRenderers = await Promise.all(
      renderers.map(async (renderer) => {
        const mod = await import(renderer.serverEntrypoint);
        if (typeof mod.default !== "undefined") {
          return {
            ...renderer,
            ssr: mod.default
          };
        }
        return void 0;
      })
    );
    const finalRenderers = loadedRenderers.filter((r) => Boolean(r));
    return new unstable_AstroContainer({ streaming, renderers: finalRenderers, config });
  }
  // NOTE: we keep this private via TS instead via `#` so it's still available on the surface, so we can play with it.
  /**
   * 
   * @param manifest
   * @private
   */
  static async createFromManifest(manifest) {
    const config = await validateConfig(ASTRO_CONFIG_DEFAULTS, process.cwd(), "container");
    const container = new unstable_AstroContainer({
      manifest,
      config
    });
    container.#withManifest = true;
    return container;
  }
  /**
   * Use this method to manually insert a route inside the container.
   *
   * This method is useful if you're rending a component that attempt to render redirect or a rewrite.
   *
   * @param {object} options
   * @param {string} options.path The path of the route. It has to match what you see in a URL browser, e.g. `/blog/12334/first-post`
   * @param {ComponentInstance} options.componentInstance The a compile instance of the Astro component.
   * @param {string[]} options.params The params of the route. Use these when your route is dynamic. For a route `/blog/[id]/[...dynamic]`, the params are `["id", "...dynamic"]`
   * @param {RouteType} options.type The type of route.
   */
  insertRoute({
    path,
    route,
    componentInstance,
    params = {},
    type = "page"
  }) {
    const pathUrl = new URL(path, "https://example.com");
    const routeData = this.#createRoute(pathUrl, route, params, type);
    this.#pipeline.manifest.routes.push({
      routeData,
      file: "",
      links: [],
      styles: [],
      scripts: []
    });
    this.#pipeline.insertRoute(routeData, componentInstance);
    return routeData;
  }
  /**
   * @description
   * It renders a component and returns the result as a string.
   * 
   * ## Example
   * 
   * ```js
   * import Card from "../src/components/Card.astro";
   * 
   * const container = await AstroContainer.create();
   * const result = await container.renderToString(Card);
   * 
   * console.log(result); // it's a string
   * ```
   * 
   * 
   * @param {AstroComponentFactory} component The instance of the component.
   * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
   */
  async renderToString(component, options = {}) {
    const response = await this.renderToResponse(component, options);
    return await response.text();
  }
  /**
   * @description
   * It renders a component and returns the `Response` as result of the rendering phase.
   *
   * ## Example
   *
   * ```js
   * import Card from "../src/components/Card.astro";
   *
   * const container = await AstroContainer.create();
   * const response = await container.renderToResponse(Card);
   *
   * console.log(response.status); // it's a number
   * ```
   *
   *
   * @param {AstroComponentFactory} component The instance of the component.
   * @param {ContainerRenderOptions=} options Possible options to pass when rendering the component.
   */
  async renderToResponse(component, options = {}) {
    const { routeType = "page", slots, route = "" } = options;
    const request = options?.request ?? new Request("https://example.com/");
    const url = new URL(request.url);
    const componentInstance = routeType === "endpoint" ? component : this.#wrapComponent(component, options.params);
    const routeData = this.insertRoute({
      route,
      path: request.url,
      componentInstance,
      params: options.params,
      type: routeType
    });
    const renderContext = RenderContext.create({
      pipeline: this.#pipeline,
      routeData,
      status: options?.status ?? 200,
      middleware: this.#pipeline.middleware,
      request,
      pathname: url.pathname,
      locals: options?.locals ?? {}
    });
    return renderContext.render(componentInstance, slots);
  }
  #createRoute(url, route = url.pathname, params, type) {
    const segments = removeLeadingForwardSlash(route).split(posix.sep).filter(Boolean).map((s) => {
      validateSegment(s);
      return getParts(s, route);
    });
    return {
      component: "",
      generate(_data) {
        return "";
      },
      params: Object.keys(params),
      pattern: getPattern(segments, this.#config, this.#config.trailingSlash),
      prerender: false,
      segments,
      type,
      route,
      fallbackRoutes: [],
      isIndex: false
    };
  }
  /**
   * If the provided component isn't a default export, the function wraps it in an object `{default: Component }` to mimic the default export.
   * @param componentFactory
   * @param params
   * @private
   */
  #wrapComponent(componentFactory, params) {
    if (params) {
      return {
        default: componentFactory,
        getStaticPaths() {
          return [{ params }];
        }
      };
    }
    return { default: componentFactory };
  }
}
export {
  unstable_AstroContainer
};
