import {
  AutoBeOpenApi,
  AutoBeProgressEventBase,
  AutoBeTestPrepareFunction,
  AutoBeTestPrepareMapping,
  AutoBeTestValidateEvent,
  IAutoBeCompiler,
} from "@autobe/interface";
import { StringUtil } from "@autobe/utils";
import { NamingConvention, OpenApiTypeChecker } from "@typia/utils";
import { IValidation } from "typia";

import { AutoBeContext } from "../../../context/AutoBeContext";
import { validateEmptyCode } from "../../../utils/validateEmptyCode";
import { AutoBeRealizeCollectorProgrammer } from "../../realize/programmers/AutoBeRealizeCollectorProgrammer";
import { IAutoBeTestPrepareProcedure } from "../structures/IAutoBeTestPrepareProcedure";
import { AutoBeTestFunctionProgrammer } from "./AutoBeTestFunctionProgrammer";

export namespace AutoBeTestPrepareProgrammer {
  /* ----------------------------------------------------------------
    GETTERS
  ---------------------------------------------------------------- */
  export function is(key: string, value: AutoBeOpenApi.IJsonSchema): boolean {
    return (
      key.endsWith(".ICreate") && OpenApiTypeChecker.isObject(value) === true
    );
  }

  export function size(document: AutoBeOpenApi.IDocument): number {
    return Object.entries(document.components.schemas).filter(([key, value]) =>
      AutoBeTestPrepareProgrammer.is(key, value),
    ).length;
  }

  export function getFunctionName(typeName: string): string {
    const snake: string = NamingConvention.snake(
      typeName.split(".")[0]!.slice(1),
    );
    return `prepare_random_${snake}`;
  }

  /* ----------------------------------------------------------------
    WRITERS
  ---------------------------------------------------------------- */
  export function writeTemplateCode(props: {
    typeName: string;
    schema: AutoBeOpenApi.IJsonSchema.IObject;
  }): string {
    return StringUtil.trim`
      export function ${getFunctionName(props.typeName)}(
        input?: DeepPartial<${props.typeName}> | undefined,
      ): ${props.typeName} {
        return {
${Object.keys(props.schema.properties).map(
  (key) =>
    `    ${NamingConvention.variable(key) ? key : `[${JSON.stringify(key)}]`}: ...,`,
)}
        };
      }
    `;
  }

  export function writeNonPropertyCode(props: {
    typeName: string;
    schema: AutoBeOpenApi.IJsonSchema.IObject;
  }): string {
    return StringUtil.trim`
      export function ${getFunctionName(props.typeName)}(
        input?: DeepPartial<${props.typeName}> | undefined,
      ): ${props.typeName} {
        input;
        return {};
      }
    `;
  }

  export async function writeStructures(
    ctx: AutoBeContext,
    typeName: string,
  ): Promise<Record<string, string>> {
    return {
      ...(await AutoBeRealizeCollectorProgrammer.writeStructures(
        ctx,
        typeName,
      )),
      ...(await (await ctx.compiler()).test.getDefaultTypes()),
    };
  }

  /* ----------------------------------------------------------------
    COMPILERS
  ---------------------------------------------------------------- */
  export async function compile(props: {
    compiler: IAutoBeCompiler;
    document: AutoBeOpenApi.IDocument;
    procedure: IAutoBeTestPrepareProcedure;
    progress: AutoBeProgressEventBase;
    step: number;
  }): Promise<AutoBeTestValidateEvent<AutoBeTestPrepareFunction>> {
    const components: AutoBeOpenApi.IComponents = {
      authorizations: [],
      schemas: {},
    };
    OpenApiTypeChecker.visit({
      components: props.document.components,
      schema: { $ref: `#/components/schemas/${props.procedure.typeName}` },
      closure: (s) => {
        if (OpenApiTypeChecker.isReference(s)) {
          const key: string = s.$ref.split("/").pop()!;
          components.schemas[key] = props.document.components.schemas[key];
        }
      },
    });
    return await AutoBeTestFunctionProgrammer.compile({
      compiler: props.compiler,
      document: {
        operations: [],
        components,
      },
      function: props.procedure.function,
      files: {
        [props.procedure.function.location]: props.procedure.function.content,
        ["src/api/functional/index.ts"]:
          "export const NO_SDK_FUNCTION_AT_ALL = 1",
      },
      progress: props.progress,
      step: props.step,
    });
  }

  export async function replaceImportStatements(props: {
    compiler: IAutoBeCompiler;
    typeName: string;
    schemas: Record<string, AutoBeOpenApi.IJsonSchemaDescriptive>;
    content: string;
  }): Promise<string> {
    let code: string = await props.compiler.typescript.removeImportStatements(
      props.content,
    );
    const imports: string[] = writeImportStatements(props);
    code = [...imports, code].join("\n");
    return await props.compiler.typescript.beautify(code);
  }

  function writeImportStatements(props: {
    typeName: string;
    schemas: Record<string, AutoBeOpenApi.IJsonSchema>;
  }): string[] {
    const typeReferences: Set<string> = new Set();
    const visit = (key: string) =>
      OpenApiTypeChecker.visit({
        schema: {
          $ref: `#/components/schemas/${key}`,
        },
        components: { schemas: props.schemas },
        closure: (next) => {
          if (OpenApiTypeChecker.isReference(next))
            typeReferences.add(next.$ref.split("/").pop()!.split(".")[0]!);
        },
      });
    visit(props.typeName);

    const imports: string[] = [
      `import { ArrayUtil, RandomGenerator } from "@nestia/e2e";`,
      `import { randint } from "tstl";`,
      `import typia, { tags } from "typia";`,
      "",
      `import { DeepPartial } from "@ORGANIZATION/PROJECT-api/lib/typings/DeepPartial";`,
      `import { IEntity } from "@ORGANIZATION/PROJECT-api/lib/structures/IEntity";`,
      ...Array.from(typeReferences).map(
        (ref) =>
          `import { ${ref} } from "@ORGANIZATION/PROJECT-api/lib/structures/${ref}";`,
      ),
    ];
    return imports;
  }

  /* ----------------------------------------------------------------
    VALIDATORS
  ---------------------------------------------------------------- */
  export function validate(props: {
    typeName: string;
    schema: AutoBeOpenApi.IJsonSchema.IObject;
    mappings: AutoBeTestPrepareMapping[];
    draft: string;
    revise: {
      final: string | null;
    };
  }): IValidation.IError[] {
    // validate empty code
    const functionName: string = getFunctionName(props.typeName);
    const errors: IValidation.IError[] = validateEmptyCode({
      name: functionName,
      asynchronous: false,
      draft: props.draft,
      revise: props.revise,
      path: "$input",
    });

    // validate property mapping plans
    const expected: Set<string> = new Set(Object.keys(props.schema.properties));
    const actual: Set<string> = new Set(props.mappings.map((m) => m.property));

    if (expected.size === 0 && actual.size !== 0) {
      errors.push({
        path: `$input.mappings[]`,
        value: props.mappings,
        expected: "[] // (empty array)",
        description: StringUtil.trim`
          The schema does not have any regular properties to map.
          It has only dynamic properties that is represented by
          "Record<string, T>" type.

          Therefore, the mapping plan must be an empty array.
        `,
      });
      return errors;
    }

    // must be, but non-existing
    for (const e of expected) {
      if (actual.has(e) === true) continue;
      errors.push({
        path: `$input.mappings[]`,
        value: undefined,
        expected: StringUtil.trim`{
          property: ${JSON.stringify(e)},
          how: string;
        }`,
        description: StringUtil.trim`
          You missed mapping for property ${JSON.stringify(e)}.

          Make sure to provide mapping for all properties defined in the schema.
        `,
      });
    }

    // must not be, but existing
    props.mappings.forEach((m, i) => {
      if (expected.has(m.property) === true) return;
      errors.push({
        path: `$input.mappings[${i}].property`,
        value: m.property,
        expected: Array.from(expected)
          .map((s) => JSON.stringify(s))
          .join(" | "),
        description: StringUtil.trim`
          Property ${JSON.stringify(m.property)} does not exist in the schema.

          Actually existing properties are as follows:

          ${Array.from(expected)
            .map((s) => `- ${s}`)
            .join("\n")}
        `,
      });
    });

    return errors;
  }
}
