import {
  AutoBeAnalyzeHistory,
  AutoBeAssistantMessageHistory,
  AutoBeDatabase,
  AutoBeDatabaseCompleteEvent,
  AutoBeDatabaseComponent,
  AutoBeDatabaseComponentTableDesign,
  AutoBeDatabaseGroup,
  AutoBeDatabaseHistory,
  AutoBeDatabaseSchemaDefinition,
  AutoBeDatabaseSchemaEvent,
  AutoBeProgressEventBase,
  IAutoBeCompiler,
  IAutoBeDatabaseValidation,
} from "@autobe/interface";
import { writePrismaApplication } from "@autobe/utils";
import { NamingConvention } from "@typia/utils";
import { v7 } from "uuid";

import { AutoBeContext } from "../../context/AutoBeContext";
import { predicateStateMessage } from "../../utils/predicateStateMessage";
import { IAutoBeFacadeApplicationProps } from "../facade/histories/IAutoBeFacadeApplicationProps";
import { orchestrateDatabaseAuthorization } from "./orchestrateDatabaseAuthorization";
import { orchestrateDatabaseComponent } from "./orchestrateDatabaseComponent";
import { orchestrateDatabaseCorrect } from "./orchestrateDatabaseCorrect";
import { orchestrateDatabaseGroup } from "./orchestrateDatabaseGroup";
import { orchestrateDatabaseSchema } from "./orchestrateDatabaseSchema";

export const orchestrateDatabase = async (
  ctx: AutoBeContext,
  props: IAutoBeFacadeApplicationProps,
): Promise<AutoBeDatabaseHistory | AutoBeAssistantMessageHistory> => {
  // PREDICATION
  const start: Date = new Date();
  const predicate: string | null = predicateStateMessage(
    ctx.state(),
    "database",
  );
  if (predicate !== null)
    return ctx.assistantMessage({
      type: "assistantMessage",
      id: v7(),
      created_at: start.toISOString(),
      text: predicate,
      completed_at: new Date().toISOString(),
    });
  ctx.dispatch({
    type: "databaseStart",
    id: v7(),
    created_at: start.toISOString(),
    reason: props.instruction,
    step: ctx.state().analyze?.step ?? 0,
  });

  // NORMALIZE PREFIX
  const analyze: AutoBeAnalyzeHistory | null = ctx.state().analyze;
  if (analyze?.prefix) {
    analyze.prefix = NamingConvention.snake(analyze.prefix);
  }

  // GROUPS
  const groups: AutoBeDatabaseGroup[] = await orchestrateGroup(ctx, props);
  const components: AutoBeDatabaseComponent[] = await orchestrateComponent(
    ctx,
    {
      groups,
      instruction: props.instruction,
    },
  );
  const application: AutoBeDatabase.IApplication = await orchestrateSchema(
    ctx,
    {
      instruction: props.instruction,
      components,
    },
  );

  // VALIDATE
  const validation: IAutoBeDatabaseValidation =
    await orchestrateDatabaseCorrect(ctx, application);
  const files: Record<string, string> = writePrismaApplication({
    dbms: "postgres",
    application: validation.data,
  });

  // PROPAGATE
  const compiler: IAutoBeCompiler = await ctx.compiler();
  return ctx.dispatch({
    type: "databaseComplete",
    id: v7(),
    result: validation,
    schemas: files,
    compiled: await compiler.database.compilePrismaSchemas({
      files,
    }),
    aggregates: ctx.getCurrentAggregates("database"),
    step: ctx.state().analyze?.step ?? 0,
    elapsed: new Date().getTime() - start.getTime(),
    created_at: new Date().toISOString(),
  } satisfies AutoBeDatabaseCompleteEvent);
};

const orchestrateGroup = async (
  ctx: AutoBeContext,
  props: IAutoBeFacadeApplicationProps,
): Promise<AutoBeDatabaseGroup[]> => {
  return await orchestrateDatabaseGroup(ctx, props.instruction);
};

const orchestrateAuthorization = async (
  ctx: AutoBeContext,
  props: {
    instruction: string;
    groups: AutoBeDatabaseGroup[];
  },
): Promise<AutoBeDatabaseComponent | null> => {
  return await orchestrateDatabaseAuthorization(ctx, {
    instruction: props.instruction,
    groups: props.groups,
  });
};

const orchestrateComponent = async (
  ctx: AutoBeContext,
  props: {
    instruction: string;
    groups: AutoBeDatabaseGroup[];
  },
): Promise<AutoBeDatabaseComponent[]> => {
  const authorization: AutoBeDatabaseComponent | null =
    await orchestrateAuthorization(ctx, {
      groups: props.groups,
      instruction: props.instruction,
    });
  const components: AutoBeDatabaseComponent[] =
    await orchestrateDatabaseComponent(ctx, {
      instruction: props.instruction,
      groups: props.groups,
    });
  return [...(authorization ? [authorization] : []), ...components];
};

const orchestrateSchema = async (
  ctx: AutoBeContext,
  props: {
    instruction: string;
    components: AutoBeDatabaseComponent[];
  },
): Promise<AutoBeDatabase.IApplication> => {
  //----
  // STATES
  //----
  // clone groups to keep previous events
  const components: AutoBeDatabaseComponent[] = props.components.map((c) => ({
    ...c,
    tables: c.tables.slice(),
  }));

  // completion set
  const written: Set<string> = new Set();
  const failed: Map<string, number> = new Map();
  const complete = () =>
    components
      .flatMap((g) => g.tables)
      .every((t) => written.has(t.name) === true);

  // generated models
  interface IModelPair {
    namespace: string;
    model: AutoBeDatabase.IModel;
  }
  const pairs: IModelPair[] = [];

  //----
  // DEFINER
  //----
  const application = (): AutoBeDatabase.IApplication => ({
    files: components.map((comp) => ({
      filename: comp.filename,
      namespace: comp.namespace,
      models: pairs
        .filter((p) => p.namespace === comp.namespace)
        .map((p) => p.model),
    })),
  });
  const define = (next: {
    namespace: string;
    definition: AutoBeDatabaseSchemaDefinition;
  }): void => {
    // find parent component and matched design
    const myComponent: AutoBeDatabaseComponent = components.find(
      (c) => c.namespace === next.namespace,
    )!;
    const myTable: AutoBeDatabaseComponentTableDesign = myComponent.tables.find(
      (t) => t.name === next.definition.model.name,
    )!;

    // mark as done
    written.add(myTable.name);
    const existing: number = pairs.findIndex(
      (p) =>
        p.namespace === next.namespace &&
        p.model.name === next.definition.model.name,
    );
    if (existing !== -1)
      pairs[existing] = {
        namespace: next.namespace,
        model: next.definition.model,
      };
    else
      pairs.push({ namespace: next.namespace, model: next.definition.model });

    // prepare new designs
    for (const design of next.definition.newDesigns)
      if (
        written.has(design.name) === false &&
        myComponent.tables.find((t) => t.name === design.name) === undefined &&
        components
          .flatMap((c) => c.tables)
          .find((t) => t.name === design.name) === undefined
      )
        myComponent.tables.push(design);
  };

  //----
  // THE LOOP
  //----
  const writeProgress: AutoBeProgressEventBase = { total: 0, completed: 0 };
  while (complete() === false) {
    const events: AutoBeDatabaseSchemaEvent[] = await orchestrateDatabaseSchema(
      ctx,
      {
        instruction: props.instruction,
        components,
        written,
        failed,
        progress: writeProgress,
      },
    );
    for (const e of events)
      define({
        namespace: e.namespace,
        definition: e.definition,
      });
  }
  return application();
};
