import { OpenApi } from "../OpenApi";
import { OpenApiV3_1 } from "../OpenApiV3_1";

export namespace OpenApiV3_1Emender {
  export const convert = (input: OpenApiV3_1.IDocument): OpenApi.IDocument => {
    if ((input as OpenApi.IDocument)["x-samchon-emended"] === true)
      return input as OpenApi.IDocument;
    return {
      ...input,
      components: convertComponents(input.components ?? {}),
      paths: input.paths
        ? Object.fromEntries(
            Object.entries(input.paths)
              .filter(([_, v]) => v !== undefined)
              .map(
                ([key, value]) => [key, convertPathItem(input)(value)] as const,
              ),
          )
        : undefined,
      webhooks: input.webhooks
        ? Object.fromEntries(
            Object.entries(input.webhooks)
              .filter(([_, v]) => v !== undefined)
              .map(
                ([key, value]) =>
                  [key, convertWebhooks(input)(value)!] as const,
              )
              .filter(([_, value]) => value !== undefined),
          )
        : undefined,
      "x-samchon-emended": true,
    };
  };

  /* -----------------------------------------------------------
    OPERATORS
  ----------------------------------------------------------- */
  const convertWebhooks =
    (doc: OpenApiV3_1.IDocument) =>
    (
      webhook:
        | OpenApiV3_1.IPath
        | OpenApiV3_1.IJsonSchema.IReference<`#/components/pathItems/${string}`>,
    ): OpenApi.IPath | undefined => {
      if (!TypeChecker.isReference(webhook))
        return convertPathItem(doc)(webhook);
      const found: OpenApiV3_1.IPath | undefined =
        doc.components?.pathItems?.[webhook.$ref.split("/").pop() ?? ""];
      return found ? convertPathItem(doc)(found) : undefined;
    };

  const convertPathItem =
    (doc: OpenApiV3_1.IDocument) =>
    (pathItem: OpenApiV3_1.IPath): OpenApi.IPath => ({
      ...(pathItem as any),
      ...(pathItem.get
        ? { get: convertOperation(doc)(pathItem)(pathItem.get) }
        : undefined),
      ...(pathItem.put
        ? { put: convertOperation(doc)(pathItem)(pathItem.put) }
        : undefined),
      ...(pathItem.post
        ? { post: convertOperation(doc)(pathItem)(pathItem.post) }
        : undefined),
      ...(pathItem.delete
        ? { delete: convertOperation(doc)(pathItem)(pathItem.delete) }
        : undefined),
      ...(pathItem.options
        ? { options: convertOperation(doc)(pathItem)(pathItem.options) }
        : undefined),
      ...(pathItem.head
        ? { head: convertOperation(doc)(pathItem)(pathItem.head) }
        : undefined),
      ...(pathItem.patch
        ? { patch: convertOperation(doc)(pathItem)(pathItem.patch) }
        : undefined),
      ...(pathItem.trace
        ? { trace: convertOperation(doc)(pathItem)(pathItem.trace) }
        : undefined),
    });

  const convertOperation =
    (doc: OpenApiV3_1.IDocument) =>
    (pathItem: OpenApiV3_1.IPath) =>
    (input: OpenApiV3_1.IOperation): OpenApi.IOperation => ({
      ...input,
      parameters:
        pathItem.parameters !== undefined || input.parameters !== undefined
          ? [...(pathItem.parameters ?? []), ...(input.parameters ?? [])]
              .map((p) => {
                if (!TypeChecker.isReference(p))
                  return convertParameter(doc.components ?? {})(p);
                const found:
                  | Omit<OpenApiV3_1.IOperation.IParameter, "in">
                  | undefined = p.$ref.startsWith("#/components/headers/")
                  ? doc.components?.headers?.[p.$ref.split("/").pop() ?? ""]
                  : doc.components?.parameters?.[p.$ref.split("/").pop() ?? ""];
                return found !== undefined
                  ? convertParameter(doc.components ?? {})({
                      ...found,
                      in: "header",
                    })
                  : undefined!;
              })
              .filter((_, v) => v !== undefined)
          : undefined,
      requestBody: input.requestBody
        ? convertRequestBody(doc)(input.requestBody)
        : undefined,
      responses: input.responses
        ? Object.fromEntries(
            Object.entries(input.responses)
              .filter(([_, v]) => v !== undefined)
              .map(
                ([key, value]) => [key, convertResponse(doc)(value)!] as const,
              )
              .filter(([_, value]) => value !== undefined),
          )
        : undefined,
    });

  const convertParameter =
    (components: OpenApiV3_1.IComponents) =>
    (
      input: OpenApiV3_1.IOperation.IParameter,
    ): OpenApi.IOperation.IParameter => ({
      ...input,
      schema: convertSchema(components)(input.schema),
      examples: input.examples
        ? Object.fromEntries(
            Object.entries(input.examples)
              .map(([key, value]) => [
                key,
                TypeChecker.isReference(value)
                  ? components.examples?.[value.$ref.split("/").pop() ?? ""]
                  : value,
              ])
              .filter(([_, v]) => v !== undefined),
          )
        : undefined,
    });

  const convertRequestBody =
    (doc: OpenApiV3_1.IDocument) =>
    (
      input:
        | OpenApiV3_1.IOperation.IRequestBody
        | OpenApiV3_1.IJsonSchema.IReference<`#/components/requestBodies/${string}`>,
    ): OpenApi.IOperation.IRequestBody | undefined => {
      if (TypeChecker.isReference(input)) {
        const found: OpenApiV3_1.IOperation.IRequestBody | undefined =
          doc.components?.requestBodies?.[input.$ref.split("/").pop() ?? ""];
        if (found === undefined) return undefined;
        input = found;
      }
      return {
        ...input,
        content: input.content
          ? convertContent(doc.components ?? {})(input.content)
          : undefined,
      };
    };

  const convertResponse =
    (doc: OpenApiV3_1.IDocument) =>
    (
      input:
        | OpenApiV3_1.IOperation.IResponse
        | OpenApiV3_1.IJsonSchema.IReference<`#/components/responses/${string}`>,
    ): OpenApi.IOperation.IResponse | undefined => {
      if (TypeChecker.isReference(input)) {
        const found: OpenApiV3_1.IOperation.IResponse | undefined =
          doc.components?.responses?.[input.$ref.split("/").pop() ?? ""];
        if (found === undefined) return undefined;
        input = found;
      }
      return {
        ...input,
        content: input.content
          ? convertContent(doc.components ?? {})(input.content)
          : undefined,
        headers: input.headers
          ? Object.fromEntries(
              Object.entries(input.headers)
                .filter(([_, v]) => v !== undefined)
                .map(
                  ([key, value]) =>
                    [
                      key,
                      (() => {
                        if (TypeChecker.isReference(value) === false)
                          return convertParameter(doc.components ?? {})({
                            ...value,
                            in: "header",
                          });
                        const found:
                          | Omit<OpenApiV3_1.IOperation.IParameter, "in">
                          | undefined = value.$ref.startsWith(
                          "#/components/headers/",
                        )
                          ? doc.components?.headers?.[
                              value.$ref.split("/").pop() ?? ""
                            ]
                          : undefined;
                        return found !== undefined
                          ? convertParameter(doc.components ?? {})({
                              ...found,
                              in: "header",
                            })
                          : undefined!;
                      })(),
                    ] as const,
                )
                .filter(([_, v]) => v !== undefined),
            )
          : undefined,
      };
    };

  const convertContent =
    (components: OpenApiV3_1.IComponents) =>
    (
      record: Record<string, OpenApiV3_1.IOperation.IMediaType>,
    ): Record<string, OpenApi.IOperation.IMediaType> =>
      Object.fromEntries(
        Object.entries(record)
          .filter(([_, v]) => v !== undefined)
          .map(
            ([key, value]) =>
              [
                key,
                {
                  ...value,
                  schema: value.schema
                    ? convertSchema(components)(value.schema)
                    : undefined,
                  examples: value.examples
                    ? Object.fromEntries(
                        Object.entries(value.examples)
                          .map(([key, value]) => [
                            key,
                            TypeChecker.isReference(value)
                              ? components.examples?.[
                                  value.$ref.split("/").pop() ?? ""
                                ]
                              : value,
                          ])
                          .filter(([_, v]) => v !== undefined),
                      )
                    : undefined,
                },
              ] as const,
          ),
      );

  /* -----------------------------------------------------------
    DEFINITIONS
  ----------------------------------------------------------- */
  const convertComponents = (
    input: OpenApiV3_1.IComponents,
  ): OpenApi.IComponents => ({
    schemas: Object.fromEntries(
      Object.entries(input.schemas ?? {})
        .filter(([_, v]) => v !== undefined)
        .map(([key, value]) => [key, convertSchema(input)(value)] as const),
    ),
    securitySchemes: input.securitySchemes,
  });

  const convertSchema =
    (components: OpenApiV3_1.IComponents) =>
    (input: OpenApiV3_1.IJsonSchema): OpenApi.IJsonSchema => {
      const union: OpenApi.IJsonSchema[] = [];
      const attribute: OpenApi.IJsonSchema.__IAttribute = {
        title: input.title,
        description: input.description,
        ...Object.fromEntries(
          Object.entries(input).filter(
            ([key, value]) => key.startsWith("x-") && value !== undefined,
          ),
        ),
      };
      const nullable: { value: boolean; default?: null } = {
        value: false,
        default: undefined,
      };

      const visit = (schema: OpenApiV3_1.IJsonSchema): void => {
        // NULLABLE PROPERTY
        if (
          (schema as OpenApiV3_1.IJsonSchema.__ISignificant<any>).nullable ===
          true
        ) {
          nullable.value ||= true;
          if ((schema as OpenApiV3_1.IJsonSchema.INumber).default === null)
            nullable.default = null;
        }
        if (
          Array.isArray((schema as OpenApiV3_1.IJsonSchema.INumber).enum) &&
          (schema as OpenApiV3_1.IJsonSchema.INumber).enum?.length &&
          (schema as OpenApiV3_1.IJsonSchema.INumber).enum?.some(
            (e) => e === null,
          )
        )
          nullable.value ||= true;

        // MIXED TYPE CASE
        if (TypeChecker.isMixed(schema)) {
          if (schema.const !== undefined)
            visit({
              ...schema,
              ...{
                type: undefined,
                oneOf: undefined,
                anyOf: undefined,
                allOf: undefined,
                $ref: undefined,
              },
            });
          if (schema.oneOf !== undefined)
            visit({
              ...schema,
              ...{
                type: undefined,
                anyOf: undefined,
                allOf: undefined,
                $ref: undefined,
              },
            });
          if (schema.anyOf !== undefined)
            visit({
              ...schema,
              ...{
                type: undefined,
                oneOf: undefined,
                allOf: undefined,
                $ref: undefined,
              },
            });
          if (schema.allOf !== undefined)
            visit({
              ...schema,
              ...{
                type: undefined,
                oneOf: undefined,
                anyOf: undefined,
                $ref: undefined,
              },
            });
          for (const type of schema.type)
            if (type === "boolean" || type === "number" || type === "string")
              visit({
                ...schema,
                ...{
                  enum:
                    schema.enum?.length && schema.enum.filter((e) => e !== null)
                      ? schema.enum.filter((x) => typeof x === type)
                      : undefined,
                },
                type: type as any,
              });
            else if (type === "integer")
              visit({
                ...schema,
                ...{
                  enum:
                    schema.enum?.length && schema.enum.filter((e) => e !== null)
                      ? schema.enum.filter(
                          (x) =>
                            x !== null &&
                            typeof x === "number" &&
                            Number.isInteger(x),
                        )
                      : undefined,
                },
                type: type as any,
              });
            else visit({ ...schema, type: type as any });
        }
        // UNION TYPE CASE
        else if (TypeChecker.isOneOf(schema)) schema.oneOf.forEach(visit);
        else if (TypeChecker.isAnyOf(schema)) schema.anyOf.forEach(visit);
        else if (TypeChecker.isAllOf(schema))
          if (schema.allOf.length === 1) visit(schema.allOf[0]);
          else union.push(convertAllOfSchema(components)(schema));
        // ATOMIC TYPE CASE (CONSIDER ENUM VALUES)
        else if (TypeChecker.isBoolean(schema))
          if (
            schema.enum?.length &&
            schema.enum.filter((e) => e !== null).length
          )
            for (const value of schema.enum.filter((e) => e !== null))
              union.push({
                const: value,
                ...({
                  ...schema,
                  type: undefined as any,
                  enum: undefined,
                  default: undefined,
                } satisfies OpenApiV3_1.IJsonSchema.IBoolean as any),
              } satisfies OpenApi.IJsonSchema.IConstant);
          else
            union.push({
              ...schema,
              default: schema.default ?? undefined,
              ...{
                enum: undefined,
              },
            });
        else if (TypeChecker.isInteger(schema) || TypeChecker.isNumber(schema))
          if (schema.enum?.length && schema.enum.filter((e) => e !== null))
            for (const value of schema.enum.filter((e) => e !== null))
              union.push({
                const: value,
                ...({
                  ...schema,
                  type: undefined as any,
                  enum: undefined,
                  default: undefined,
                  minimum: undefined,
                  maximum: undefined,
                  exclusiveMinimum: undefined,
                  exclusiveMaximum: undefined,
                  multipleOf: undefined,
                } satisfies OpenApiV3_1.IJsonSchema.IInteger as any),
              } satisfies OpenApi.IJsonSchema.IConstant);
          else
            union.push({
              ...schema,
              default: schema.default ?? undefined,
              ...{
                enum: undefined,
              },
              ...(typeof schema.exclusiveMinimum === "number"
                ? {
                    minimum: schema.exclusiveMinimum,
                    exclusiveMinimum: true,
                  }
                : {
                    exclusiveMinimum: schema.exclusiveMinimum,
                  }),
              ...(typeof schema.exclusiveMaximum === "number"
                ? {
                    maximum: schema.exclusiveMaximum,
                    exclusiveMaximum: true,
                  }
                : {
                    exclusiveMaximum: schema.exclusiveMaximum,
                  }),
            });
        else if (TypeChecker.isString(schema))
          if (
            schema.enum?.length &&
            schema.enum.filter((e) => e !== null).length
          )
            for (const value of schema.enum.filter((e) => e !== null))
              union.push({
                const: value,
                ...({
                  ...schema,
                  type: undefined as any,
                  enum: undefined,
                  default: undefined,
                } satisfies OpenApiV3_1.IJsonSchema.IString as any),
              } satisfies OpenApi.IJsonSchema.IConstant);
          else
            union.push({
              ...schema,
              default: schema.default ?? undefined,
              ...{
                enum: undefined,
              },
            });
        // ARRAY TYPE CASE (CONSIDER TUPLE)
        else if (TypeChecker.isArray(schema)) {
          if (Array.isArray(schema.items))
            union.push({
              ...schema,
              ...{
                items: undefined!,
                prefixItems: schema.items.map(convertSchema(components)),
                additionalItems:
                  typeof schema.additionalItems === "object" &&
                  schema.additionalItems !== null
                    ? convertSchema(components)(schema.additionalItems)
                    : schema.additionalItems,
              },
            } satisfies OpenApi.IJsonSchema.ITuple);
          else if (Array.isArray(schema.prefixItems))
            union.push({
              ...schema,
              ...{
                items: undefined!,
                prefixItems: schema.prefixItems.map(convertSchema(components)),
                additionalItems:
                  typeof schema.additionalItems === "object" &&
                  schema.additionalItems !== null
                    ? convertSchema(components)(schema.additionalItems)
                    : schema.additionalItems,
              },
            });
          else if (schema.items === undefined)
            union.push({
              ...schema,
              ...{
                items: undefined!,
                prefixItems: [],
              },
            });
          else
            union.push({
              ...schema,
              ...{
                items: convertSchema(components)(schema.items),
                prefixItems: undefined,
                additionalItems: undefined,
              },
            });
        }
        // OBJECT TYPE CASE
        else if (TypeChecker.isObject(schema))
          union.push({
            ...schema,
            ...{
              properties: schema.properties
                ? Object.fromEntries(
                    Object.entries(schema.properties)
                      .filter(([_, v]) => v !== undefined)
                      .map(
                        ([key, value]) =>
                          [key, convertSchema(components)(value)] as const,
                      ),
                  )
                : {},
              additionalProperties: schema.additionalProperties
                ? typeof schema.additionalProperties === "object" &&
                  schema.additionalProperties !== null
                  ? convertSchema(components)(schema.additionalProperties)
                  : schema.additionalProperties
                : undefined,
              required: schema.required ?? [],
            },
          });
        else if (TypeChecker.isRecursiveReference(schema))
          union.push({
            ...schema,
            ...{
              $ref: schema.$recursiveRef,
              $recursiveRef: undefined,
            },
          });
        // THE OTHERS
        else union.push(schema);
      };

      visit(input);
      if (
        nullable.value === true &&
        !union.some((e) => (e as OpenApi.IJsonSchema.INull).type === "null")
      )
        union.push({
          type: "null",
          default: nullable.default,
        });
      return {
        ...(union.length === 0
          ? { type: undefined }
          : union.length === 1
            ? { ...union[0] }
            : { oneOf: union.map((u) => ({ ...u, nullable: undefined })) }),
        ...attribute,
        ...{ nullable: undefined },
      };
    };

  const convertAllOfSchema =
    (components: OpenApiV3_1.IComponents) =>
    (input: OpenApiV3_1.IJsonSchema.IAllOf): OpenApi.IJsonSchema => {
      const objects: Array<OpenApiV3_1.IJsonSchema.IObject | null> =
        input.allOf.map((schema) => retrieveObject(components)(schema));
      if (objects.some((obj) => obj === null))
        return {
          type: undefined,
          ...{
            allOf: undefined,
          },
        };
      return {
        ...input,
        type: "object",
        properties: Object.fromEntries(
          objects
            .map((o) => Object.entries(o?.properties ?? {}))
            .flat()
            .map(
              ([key, value]) =>
                [key, convertSchema(components)(value)] as const,
            ),
        ),
        ...{
          allOf: undefined,
          required: [...new Set(objects.map((o) => o?.required ?? []).flat())],
        },
      };
    };

  const retrieveObject =
    (components: OpenApiV3_1.IComponents) =>
    (
      input: OpenApiV3_1.IJsonSchema,
      visited: Set<OpenApiV3_1.IJsonSchema> = new Set(),
    ): OpenApiV3_1.IJsonSchema.IObject | null => {
      if (TypeChecker.isObject(input))
        return input.properties !== undefined && !input.additionalProperties
          ? input
          : null;
      else if (visited.has(input)) return null;
      else visited.add(input);

      if (TypeChecker.isReference(input))
        return retrieveObject(components)(
          components.schemas?.[input.$ref.split("/").pop() ?? ""] ?? {},
          visited,
        );
      else if (TypeChecker.isRecursiveReference(input))
        return retrieveObject(components)(
          components.schemas?.[input.$recursiveRef.split("/").pop() ?? ""] ??
            {},
          visited,
        );
      return null;
    };

  namespace TypeChecker {
    export const isConstant = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IConstant =>
      (schema as OpenApiV3_1.IJsonSchema.IConstant).const !== undefined;
    export const isBoolean = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IBoolean =>
      (schema as OpenApiV3_1.IJsonSchema.IBoolean).type === "boolean";
    export const isInteger = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IInteger =>
      (schema as OpenApiV3_1.IJsonSchema.IInteger).type === "integer";
    export const isNumber = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.INumber =>
      (schema as OpenApiV3_1.IJsonSchema.INumber).type === "number";
    export const isString = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IString =>
      (schema as OpenApiV3_1.IJsonSchema.IString).type === "string";
    export const isArray = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IArray =>
      (schema as OpenApiV3_1.IJsonSchema.IArray).type === "array";
    export const isObject = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IObject =>
      (schema as OpenApiV3_1.IJsonSchema.IObject).type === "object";
    export const isReference = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IReference =>
      (schema as OpenApiV3_1.IJsonSchema.IReference).$ref !== undefined;
    export const isRecursiveReference = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IRecursiveReference =>
      (schema as OpenApiV3_1.IJsonSchema.IRecursiveReference).$recursiveRef !==
      undefined;
    export const isAllOf = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IAllOf =>
      (schema as OpenApiV3_1.IJsonSchema.IAllOf).allOf !== undefined;
    export const isAnyOf = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IAnyOf =>
      (schema as OpenApiV3_1.IJsonSchema.IAnyOf).anyOf !== undefined;
    export const isOneOf = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IOneOf =>
      (schema as OpenApiV3_1.IJsonSchema.IOneOf).oneOf !== undefined;
    export const isNullOnly = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.INull =>
      (schema as OpenApiV3_1.IJsonSchema.INull).type === "null";
    export const isMixed = (
      schema: OpenApiV3_1.IJsonSchema,
    ): schema is OpenApiV3_1.IJsonSchema.IMixed =>
      Array.isArray((schema as OpenApiV3_1.IJsonSchema.IMixed).type);
  }
}
