import {
  BaseResolver,
  bundle,
  makeDocumentFromString,
  type Config as RedoclyConfig,
  Source,
  type Document,
  lintDocument,
} from "@redocly/openapi-core";
import { Readable } from "node:stream";
import { fileURLToPath } from "node:url";
import { OpenAPI3 } from "../types.js";
import { debug, error, warn } from "./utils.js";

export interface ValidateAndBundleOptions {
  redoc: RedoclyConfig;
  silent: boolean;
  cwd?: URL;
}

interface ParseSchemaOptions {
  absoluteRef: string;
  resolver: BaseResolver;
}

export async function parseSchema(
  schema: unknown,
  { absoluteRef, resolver }: ParseSchemaOptions,
): Promise<Document> {
  if (!schema) {
    throw new Error(`Can’t parse empty schema`);
  }
  if (schema instanceof URL) {
    const result = await resolver.resolveDocument(null, absoluteRef, true);
    if ("parsed" in result) {
      return result;
    }
    throw result.originalError;
  }
  if (schema instanceof Readable) {
    const contents = await new Promise<string>((resolve) => {
      schema.resume();
      schema.setEncoding("utf8");
      let content = "";
      schema.on("data", (chunk: string) => {
        content += chunk;
      });
      schema.on("end", () => {
        resolve(content.trim());
      });
    });
    return parseSchema(contents, { absoluteRef, resolver });
  }
  if (schema instanceof Buffer) {
    return parseSchema(schema.toString("utf8"), { absoluteRef, resolver });
  }
  if (typeof schema === "string") {
    // URL
    if (
      schema.startsWith("http://") ||
      schema.startsWith("https://") ||
      schema.startsWith("file://")
    ) {
      const url = new URL(schema);
      return parseSchema(url, {
        absoluteRef: url.protocol === "file:" ? fileURLToPath(url) : url.href,
        resolver,
      });
    }
    // JSON
    if (schema[0] === "{") {
      return {
        source: new Source(absoluteRef, schema, "application/json"),
        parsed: JSON.parse(schema),
      };
    }
    // YAML
    return makeDocumentFromString(schema, absoluteRef);
  }
  if (typeof schema === "object" && !Array.isArray(schema)) {
    return {
      source: new Source(
        absoluteRef,
        JSON.stringify(schema),
        "application/json",
      ),
      parsed: schema,
    };
  }
  throw new Error(
    `Expected string, object, or Buffer. Got ${
      Array.isArray(schema) ? "Array" : typeof schema
    }`,
  );
}

/**
 * Validate an OpenAPI schema and flatten into a single schema using Redocly CLI
 */
export async function validateAndBundle(
  source: string | URL | OpenAPI3 | Readable | Buffer,
  options: ValidateAndBundleOptions,
) {
  const redocConfigT = performance.now();
  debug("Loaded Redoc config", "redoc", performance.now() - redocConfigT);
  const redocParseT = performance.now();
  let absoluteRef = fileURLToPath(
    new URL(options?.cwd ?? `file://${process.cwd()}/`),
  );
  if (source instanceof URL) {
    absoluteRef =
      source.protocol === "file:" ? fileURLToPath(source) : source.href;
  }
  const resolver = new BaseResolver(options.redoc.resolve);
  const document = await parseSchema(source, {
    absoluteRef,
    resolver,
  });
  debug("Parsed schema", "redoc", performance.now() - redocParseT);

  // 1. check for OpenAPI 3 or greater
  const openapiVersion = parseFloat(document.parsed.openapi);
  if (
    document.parsed.swagger ||
    !document.parsed.openapi ||
    Number.isNaN(openapiVersion) ||
    openapiVersion < 3 ||
    openapiVersion >= 4
  ) {
    if (document.parsed.swagger) {
      throw new Error(
        "Unsupported Swagger version: 2.x. Use OpenAPI 3.x instead.",
      );
    } else if (
      document.parsed.openapi ||
      openapiVersion < 3 ||
      openapiVersion >= 4
    ) {
      throw new Error(
        `Unsupported OpenAPI version: ${document.parsed.openapi}`,
      );
    }
    throw new Error("Unsupported schema format, expected `openapi: 3.x`");
  }

  // 2. lint
  const redocLintT = performance.now();
  const problems = await lintDocument({
    document,
    config: options.redoc.styleguide,
    externalRefResolver: resolver,
  });
  if (problems.length) {
    let errorMessage: string | undefined = undefined;
    for (const problem of problems) {
      if (problem.severity === "error") {
        errorMessage = problem.message;
        error(problem.message);
      } else {
        warn(problem.message, options.silent);
      }
    }
    if (errorMessage) {
      throw new Error(errorMessage);
    }
  }
  debug("Linted schema", "lint", performance.now() - redocLintT);

  // 3. bundle
  const redocBundleT = performance.now();
  const bundled = await bundle({
    config: options.redoc,
    dereference: false,
    doc: document,
  });
  if (bundled.problems.length) {
    let errorMessage: string | undefined = undefined;
    for (const problem of bundled.problems) {
      if (problem.severity === "error") {
        errorMessage = problem.message;
        error(problem.message);
        throw new Error(problem.message);
      } else {
        warn(problem.message, options.silent);
      }
    }
    if (errorMessage) {
      throw new Error(errorMessage);
    }
  }
  debug("Bundled schema", "bundle", performance.now() - redocBundleT);

  return bundled.bundle.parsed;
}
