import { OpenApi } from "../../OpenApi";
import { OpenApiV3Downgrader } from "../../converters/OpenApiV3Downgrader";
import { OpenApiV3Upgrader } from "../../converters/OpenApiV3Upgrader";
import { ILlmFunction } from "../../structures/ILlmFunction";
import { ILlmSchemaV3 } from "../../structures/ILlmSchemaV3";
import { IOpenApiSchemaError } from "../../structures/IOpenApiSchemaError";
import { IResult } from "../../structures/IResult";
import { LlmTypeCheckerV3 } from "../../utils/LlmTypeCheckerV3";
import { OpenApiConstraintShifter } from "../../utils/OpenApiConstraintShifter";
import { OpenApiTypeChecker } from "../../utils/OpenApiTypeChecker";
import { OpenApiValidator } from "../../utils/OpenApiValidator";
import { LlmDescriptionInverter } from "./LlmDescriptionInverter";
import { LlmParametersFinder } from "./LlmParametersComposer";

export namespace LlmSchemaV3Composer {
  /**
   * @internal
   */
  export const IS_DEFS = false;

  /* -----------------------------------------------------------
    CONVERTERS
  ----------------------------------------------------------- */
  export const parameters = (props: {
    config: ILlmSchemaV3.IConfig;
    components: OpenApi.IComponents;
    schema: OpenApi.IJsonSchema.IObject | OpenApi.IJsonSchema.IReference;
    /** @internal */
    validate?: (
      schema: OpenApi.IJsonSchema,
      accessor: string,
    ) => IOpenApiSchemaError.IReason[];
    accessor?: string;
    refAccessor?: string;
  }): IResult<ILlmSchemaV3.IParameters, IOpenApiSchemaError> => {
    const entity: IResult<OpenApi.IJsonSchema.IObject, IOpenApiSchemaError> =
      LlmParametersFinder.parameters({
        ...props,
        method: "LlmSchemaV3Composer.parameters",
      });
    if (entity.success === false) return entity;

    const result: IResult<ILlmSchemaV3, IOpenApiSchemaError> = schema(props);
    if (result.success === false) return result;
    return {
      success: true,
      value: {
        ...(result.value as ILlmSchemaV3.IObject),
        additionalProperties: false,
      } satisfies ILlmSchemaV3.IParameters,
    };
  };

  export const schema = (props: {
    config: ILlmSchemaV3.IConfig;
    components: OpenApi.IComponents;
    schema: OpenApi.IJsonSchema;
    /** @internal */
    validate?: (
      schema: OpenApi.IJsonSchema,
      accessor: string,
    ) => IOpenApiSchemaError.IReason[];
    accessor?: string;
    refAccessor?: string;
  }): IResult<ILlmSchemaV3, IOpenApiSchemaError> => {
    // CHECK TUPLE TYPE
    const reasons: IOpenApiSchemaError.IReason[] = [];
    OpenApiTypeChecker.visit({
      closure: (next, accessor) => {
        if (props.validate) reasons.push(...props.validate(next, accessor));
        if (OpenApiTypeChecker.isTuple(next))
          reasons.push({
            accessor: accessor,
            schema: next,
            message: "LLM does not allow tuple type.",
          });
        else if (OpenApiTypeChecker.isReference(next)) {
          // UNABLE TO FIND MATCHED REFERENCE
          const key = next.$ref.split("#/components/schemas/")[1];
          if (props.components.schemas?.[key] === undefined) {
            reasons.push({
              schema: next,
              message: `${accessor}: unable to find reference type ${JSON.stringify(key)}.`,
              accessor: accessor,
            });
          }
        }
      },
      components: props.components,
      schema: props.schema,
      accessor: props.accessor,
      refAccessor: props.refAccessor,
    });
    // if ((valid as boolean) === false) return null;
    if (reasons.length > 0)
      return {
        success: false,
        error: {
          method: "LlmSchemaV3Composer.schema",
          message: "Failed to compose LLM schema of v3",
          reasons,
        },
      };

    // CHECK MISMATCHES
    const escaped: IResult<OpenApi.IJsonSchema, IOpenApiSchemaError> =
      OpenApiTypeChecker.escape({
        ...props,
        recursive: props.config.recursive,
      });
    if (escaped.success === false)
      // UNREACHABLE
      return {
        success: false,
        error: {
          method: "LlmSchemaV3Composer.schema",
          message: "Failed to compose LLM schema of v3",
          reasons: escaped.error.reasons,
        },
      };

    // SPECIALIZATIONS
    const downgraded: ILlmSchemaV3 = OpenApiV3Downgrader.downgradeSchema({
      original: {
        schemas: {},
      },
      downgraded: {},
    })(escaped.value) as ILlmSchemaV3;
    LlmTypeCheckerV3.visit({
      closure: (next) => {
        if (
          LlmTypeCheckerV3.isOneOf(next) &&
          (next as any).discriminator !== undefined
        )
          delete (next as any).discriminator;
        else if (LlmTypeCheckerV3.isObject(next)) {
          next.properties ??= {};
          next.required ??= [];
        }
        if (props.config.constraint === false) {
          if (
            LlmTypeCheckerV3.isInteger(next) ||
            LlmTypeCheckerV3.isNumber(next)
          )
            OpenApiConstraintShifter.shiftNumeric(
              next as
                | OpenApi.IJsonSchema.IInteger
                | OpenApi.IJsonSchema.INumber,
            );
          else if (LlmTypeCheckerV3.isString(next))
            OpenApiConstraintShifter.shiftString(
              next as OpenApi.IJsonSchema.IString,
            );
          else if (LlmTypeCheckerV3.isArray(next))
            OpenApiConstraintShifter.shiftArray(
              next as OpenApi.IJsonSchema.IArray,
            );
        }
      },
      schema: downgraded,
    });
    return {
      success: true,
      value: downgraded,
    };
  };

  /* -----------------------------------------------------------
    SEPARATORS
  ----------------------------------------------------------- */
  export const separateParameters = (props: {
    predicate: (schema: ILlmSchemaV3) => boolean;
    parameters: ILlmSchemaV3.IParameters;
  }): ILlmFunction.ISeparated<"3.0"> => {
    const [llm, human] = separateObject({
      predicate: props.predicate,
      schema: props.parameters,
    });
    return {
      llm: (llm as ILlmSchemaV3.IParameters | null) ?? {
        type: "object",
        properties: {},
        additionalProperties: false,
        required: [],
      },
      human: human as ILlmSchemaV3.IParameters | null,
      validate: llm
        ? OpenApiValidator.create({
            components: {},
            schema: invert({ schema: llm }),
            required: true,
          })
        : undefined,
    };
  };

  const separateStation = (props: {
    predicate: (schema: ILlmSchemaV3) => boolean;
    schema: ILlmSchemaV3;
  }): [ILlmSchemaV3 | null, ILlmSchemaV3 | null] => {
    if (props.predicate(props.schema) === true) return [null, props.schema];
    else if (
      LlmTypeCheckerV3.isUnknown(props.schema) ||
      LlmTypeCheckerV3.isOneOf(props.schema)
    )
      return [props.schema, null];
    else if (LlmTypeCheckerV3.isObject(props.schema))
      return separateObject({
        predicate: props.predicate,
        schema: props.schema,
      });
    else if (LlmTypeCheckerV3.isArray(props.schema))
      return separateArray({
        predicate: props.predicate,
        schema: props.schema,
      });
    return [props.schema, null];
  };

  const separateArray = (props: {
    predicate: (schema: ILlmSchemaV3) => boolean;
    schema: ILlmSchemaV3.IArray;
  }): [ILlmSchemaV3.IArray | null, ILlmSchemaV3.IArray | null] => {
    const [x, y] = separateStation({
      predicate: props.predicate,
      schema: props.schema.items,
    });
    return [
      x !== null
        ? {
            ...props.schema,
            items: x,
          }
        : null,
      y !== null
        ? {
            ...props.schema,
            items: y,
          }
        : null,
    ];
  };

  const separateObject = (props: {
    predicate: (schema: ILlmSchemaV3) => boolean;
    schema: ILlmSchemaV3.IObject;
  }): [ILlmSchemaV3.IObject | null, ILlmSchemaV3.IObject | null] => {
    // EMPTY OBJECT
    if (
      Object.keys(props.schema.properties ?? {}).length === 0 &&
      !!props.schema.additionalProperties === false
    )
      return [props.schema, null];

    const llm = {
      ...props.schema,
      properties: {} as Record<string, ILlmSchemaV3>,
      additionalProperties: props.schema.additionalProperties,
    } satisfies ILlmSchemaV3.IObject;
    const human = {
      ...props.schema,
      properties: {} as Record<string, ILlmSchemaV3>,
      additionalProperties: props.schema.additionalProperties,
    } satisfies ILlmSchemaV3.IObject;

    for (const [key, value] of Object.entries(props.schema.properties ?? {})) {
      const [x, y] = separateStation({
        predicate: props.predicate,
        schema: value,
      });
      if (x !== null) llm.properties[key] = x;
      if (y !== null) human.properties[key] = y;
    }
    if (
      typeof props.schema.additionalProperties === "object" &&
      props.schema.additionalProperties !== null
    ) {
      const [dx, dy] = separateStation({
        predicate: props.predicate,
        schema: props.schema.additionalProperties,
      });
      llm.additionalProperties = dx ?? false;
      human.additionalProperties = dy ?? false;
    }
    return [
      !!Object.keys(llm.properties).length || !!llm.additionalProperties
        ? shrinkRequired(llm)
        : null,
      !!Object.keys(human.properties).length || !!human.additionalProperties
        ? shrinkRequired(human)
        : null,
    ];
  };

  const shrinkRequired = (s: ILlmSchemaV3.IObject): ILlmSchemaV3.IObject => {
    s.required = s.required.filter((key) => s.properties[key] !== undefined);
    return s;
  };

  /* -----------------------------------------------------------
    INVERTERS
  ----------------------------------------------------------- */
  export const invert = (props: {
    schema: ILlmSchemaV3;
  }): OpenApi.IJsonSchema => {
    const upgraded: OpenApi.IJsonSchema = OpenApiV3Upgrader.convertSchema({})(
      props.schema,
    );
    OpenApiTypeChecker.visit({
      closure: (schema) => {
        if (OpenApiTypeChecker.isArray(schema))
          Object.assign(schema, {
            ...schema,
            ...LlmDescriptionInverter.array(schema.description),
          });
        else if (
          OpenApiTypeChecker.isInteger(schema) ||
          OpenApiTypeChecker.isNumber(schema)
        )
          Object.assign(schema, {
            ...schema,
            ...LlmDescriptionInverter.numeric(schema.description),
          });
        else if (OpenApiTypeChecker.isString(schema))
          Object.assign(schema, {
            ...schema,
            ...LlmDescriptionInverter.string(schema.description),
          });
      },
      components: {},
      schema: upgraded,
    });
    return upgraded;
  };
}
