import * as fs from "node:fs/promises";
import * as YAML from "yaml";
import { errorFormatter, findFiles } from "@embeddable.com/sdk-utils";
import { z } from "zod";
import ora from "ora";
import { checkNodeVersion } from "./utils";
import { ResolvedEmbeddableConfig } from "./defineConfig";
const CUBE_YAML_FILE_REGEX = /^(.*)\.cube\.ya?ml$/;
const SECURITY_CONTEXT_FILE_REGEX = /^(.*)\.sc\.ya?ml$/;
const CLIENT_CONTEXT_FILE_REGEX = /^(.*)\.cc\.ya?ml$/;

export default async (ctx: ResolvedEmbeddableConfig) => {
  checkNodeVersion();

  const spinnerValidate = ora("Data model validation...").start();

  const cubeFilesList = await findFiles(
    ctx.client.modelsSrc || ctx.client.srcDir,
    CUBE_YAML_FILE_REGEX,
  );
  const securityContextFilesList = await findFiles(
    ctx.client.presetsSrc || ctx.client.srcDir,
    SECURITY_CONTEXT_FILE_REGEX,
  );
  const clientContextFilesList = await findFiles(
    ctx.client.presetsSrc || ctx.client.srcDir,
    CLIENT_CONTEXT_FILE_REGEX,
  );

  const dataModelErrors = await dataModelsValidation(cubeFilesList);

  if (dataModelErrors.length) {
    spinnerValidate.fail("One or more cube.yaml files are invalid:");

    dataModelErrors.forEach((errorMessage) =>
      spinnerValidate.info(errorMessage),
    );

    process.exit(1);
  }

  spinnerValidate.succeed("Data model validation completed");

  const securityContextErrors = await securityContextValidation(
    securityContextFilesList,
  );

  const clientContextErrors = await clientContextValidation(
    clientContextFilesList,
  );

  if (securityContextErrors.length) {
    spinnerValidate.fail("One or more security context files are invalid:");

    securityContextErrors.forEach((errorMessage) =>
      spinnerValidate.info(errorMessage),
    );

    process.exit(1);
  }

  if (clientContextErrors.length) {
    spinnerValidate.fail("One or more client context files are invalid:");

    clientContextErrors.forEach((errorMessage) =>
      spinnerValidate.info(errorMessage),
    );

    process.exit(1);
  }

  return (
    dataModelErrors.length === 0 &&
    securityContextErrors.length === 0 &&
    clientContextErrors.length === 0
  );
};

export async function dataModelsValidation(filesList: [string, string][]) {
  const errors: string[] = [];

  for (const [_, filePath] of filesList) {
    const fileContentRaw = await fs.readFile(filePath, "utf8");

    try {
      const cube = YAML.parse(fileContentRaw);
      if (!cube?.cubes && !cube?.views) {
        return [`${filePath}: At least one cubes or views must be defined`];
      }

      const cubeModelSafeParse = cubeModelSchema.safeParse(cube);
      const viewModelSafeParse = viewModelSchema.safeParse(cube);

      if (cube.cubes && !cubeModelSafeParse.success) {
        errorFormatter(cubeModelSafeParse.error.issues).forEach((error) => {
          errors.push(`${filePath}: ${error}`);
        });
      }

      if (cube.views && !viewModelSafeParse.success) {
        errorFormatter(viewModelSafeParse.error.issues).forEach((error) => {
          errors.push(`${filePath}: ${error}`);
        });
      }
    } catch (e: any) {
      errors.push(`${filePath}: ${e.message}`);
    }
  }

  return errors;
}

export async function securityContextValidation(filesList: [string, string][]) {
  const errors: string[] = [];

  const nameSet = new Set<string>();
  for (const [_, filePath] of filesList) {
    const fileContentRaw = await fs.readFile(filePath, "utf8");

    const cube = YAML.parse(fileContentRaw);

    cube.forEach((item: { name: string }) => {
      if (nameSet.has(item.name)) {
        errors.push(
          `${filePath}: security context with name "${item.name}" already exists`,
        );
      } else {
        nameSet.add(item.name);
      }
    });

    const safeParse = securityContextSchema.safeParse(cube);
    if (!safeParse.success) {
      errorFormatter(safeParse.error.issues).forEach((error) => {
        errors.push(`${filePath}: ${error}`);
      });
    }
  }

  return errors;
}

export async function clientContextValidation(filesList: [string, string][]) {
  const errors: string[] = [];

  const nameSet = new Set<string>();
  for (const [_, filePath] of filesList) {
    const fileContentRaw = await fs.readFile(filePath, "utf8");

    const cube = YAML.parse(fileContentRaw);

    cube.forEach((item: { name: string }) => {
      if (nameSet.has(item.name)) {
        errors.push(
          `${filePath}: client context with name "${item.name}" already exists`,
        );
      } else {
        nameSet.add(item.name);
      }
    });

    const safeParse = clientContextSchema.safeParse(cube);
    if (!safeParse.success) {
      errorFormatter(safeParse.error.issues).forEach((error) => {
        errors.push(`${filePath}: ${error}`);
      });
    }
  }

  return errors;
}

enum MeasureTypeEnum {
  string = "string",
  time = "time",
  boolean = "boolean",
  number = "number",
  count = "count",
  count_distinct = "count_distinct",
  count_distinct_approx = "count_distinct_approx",
  sum = "sum",
  avg = "avg",
  min = "min",
  max = "max",
}

enum DimensionTypeEnum {
  string = "string",
  time = "time",
  boolean = "boolean",
  number = "number",
  geo = "geo",
}

const cubeModelSchema = z
  .object({
    cubes: z
      .object({
        name: z.string(),
        dimensions: z
          .object({
            name: z.string(),
            type: z.nativeEnum(DimensionTypeEnum),
          })
          .array()
          .optional(),
        measures: z
          .object({
            name: z.string(),
            type: z.nativeEnum(MeasureTypeEnum),
          })
          .array()
          .optional(),
      })
      .array()
      .min(1),
  })
  .refine(
    (data) =>
      data.cubes.every(
        (cube) => cube.dimensions?.length || cube.measures?.length,
      ),
    {
      message: "At least one measure or dimension must be defined",
      path: ["cubes"],
    },
  );

const viewModelSchema = z.object({
  views: z
    .object({
      name: z.string(),
      cubes: z
        .object({
          join_path: z.string(),
        })
        .array(),
    })
    .array()
    .min(1),
});

const securityContextSchema = z.array(
  z
    .object({
      name: z.string(),
      securityContext: z.object({}), // can be any object
      environment: z.string().optional(),
      useQueryRewrite: z.boolean().optional(),
    })
    .strict(),
);

const clientContextSchema = z.array(
  z
    .object({
      name: z.string(),
      clientContext: z.object({}), // can be any object
      canvas: z.object({}).optional(),
    })
    .strict(),
);
