import { IAgenticaController } from "@agentica/core";
import {
  AutoBeDatabaseComponent,
  AutoBeDatabaseComponentTableDesign,
  AutoBeDatabaseSchemaEvent,
  AutoBeEventSource,
  AutoBeProgressEventBase,
} from "@autobe/interface";
import { IPointer, Singleton } from "tstl";
import typia, { ILlmApplication, IValidation } from "typia";
import { v7 } from "uuid";

import { AutoBeContext } from "../../context/AutoBeContext";
import { executeCachedBatch } from "../../utils/executeCachedBatch";
import { AutoBePreliminaryController } from "../common/AutoBePreliminaryController";
import { transformDatabaseSchemaHistory } from "./histories/transformDatabaseSchemaHistory";
import { AutoBeDatabaseSchemaProgrammer } from "./programmers/AutoBeDatabaseSchemaProgrammer";
import { IAutoBeDatabaseSchemaApplication } from "./structures/IAutoBeDatabaseSchemaApplication";

export async function orchestrateDatabaseSchema(
  ctx: AutoBeContext,
  props: {
    instruction: string;
    components: AutoBeDatabaseComponent[];
    written: Set<string>;
    failed: Map<string, number>;
    progress: AutoBeProgressEventBase;
  },
): Promise<AutoBeDatabaseSchemaEvent[]> {
  const start: Date = new Date();
  const total: number = props.components
    .map(
      (c) => c.tables.filter((n) => props.written.has(n.name) === false).length,
    )
    .reduce((x, y) => x + y, 0);
  props.progress.total += total;

  // Flatten component list into individual table tasks
  const designPairs: Array<{
    component: AutoBeDatabaseComponent;
    design: AutoBeDatabaseComponentTableDesign;
  }> = props.components.flatMap((component) =>
    component.tables
      .filter((table) => props.written.has(table.name) === false)
      .map((table) => ({
        component,
        design: table,
      })),
  );
  const events: Array<AutoBeDatabaseSchemaEvent | null> =
    await executeCachedBatch(
      ctx,
      designPairs.map((task) => async (promptCacheKey) => {
        const counter = new Singleton(() => ++props.progress.completed);
        try {
          const otherComponents: AutoBeDatabaseComponent[] =
            props.components.filter((c) => c !== task.component);
          const event: AutoBeDatabaseSchemaEvent = await process(ctx, {
            instruction: props.instruction,
            progress: props.progress,
            counter,
            component: task.component,
            design: task.design,
            otherComponents,
            start,
            promptCacheKey,
          });
          ctx.dispatch(event);
          return event;
        } catch (error) {
          counter.get();
          console.log("database schema error", task.design.name, error);

          const count: number | undefined = props.failed.get(task.design.name);
          if (count === undefined) props.failed.set(task.design.name, 1);
          else if (count < 3) props.failed.set(task.design.name, count + 1);
          else throw error;

          return null;
        }
      }),
    );
  return events.filter((e) => e !== null);
}

async function process(
  ctx: AutoBeContext,
  props: {
    instruction: string;
    progress: AutoBeProgressEventBase;
    counter: Singleton<number>;
    component: AutoBeDatabaseComponent;
    design: AutoBeDatabaseComponentTableDesign;
    otherComponents: AutoBeDatabaseComponent[];
    start: Date;
    promptCacheKey: string;
  },
): Promise<AutoBeDatabaseSchemaEvent> {
  const preliminary: AutoBePreliminaryController<
    | "analysisSections"
    | "previousAnalysisSections"
    | "previousDatabaseSchemas"
    | "complete"
  > = new AutoBePreliminaryController({
    dispatch: (e) => ctx.dispatch(e),
    application: typia.json.application<IAutoBeDatabaseSchemaApplication>(),
    source: SOURCE,
    kinds: [
      "analysisSections",
      "previousAnalysisSections",
      "previousDatabaseSchemas",
      "complete",
    ],
    state: ctx.state(),
    config: {
      database: "ast",
    },
  });
  return await preliminary.orchestrate(ctx, async (out) => {
    const pointer: IPointer<IAutoBeDatabaseSchemaApplication.IWrite | null> = {
      value: null,
    };
    const result: AutoBeContext.IResult = await ctx.conversate({
      source: SOURCE,
      controller: createController({
        preliminary,
        targetComponent: props.component,
        otherComponents: props.otherComponents,
        design: props.design,
        build: (next) => {
          pointer.value = next;
        },
        dispatch: ctx.dispatch,
      }),
      enforceFunctionCall: true,
      promptCacheKey: props.promptCacheKey,
      ...transformDatabaseSchemaHistory({
        component: props.component,
        design: props.design,
        otherComponents: props.otherComponents,
        instruction: props.instruction,
        preliminary,
      }),
    });
    if (pointer.value === null) return out(result)(null);

    return out(result)({
      type: SOURCE,
      id: v7(),
      created_at: props.start.toISOString(),
      plan: pointer.value.plan,
      namespace: props.component.namespace,
      definition: pointer.value.definition,
      acquisition: preliminary.getAcquisition(),
      metric: result.metric,
      tokenUsage: result.tokenUsage,
      completed: props.counter.get(),
      total: props.progress.total,
      step: ctx.state().analyze?.step ?? 0,
    } satisfies AutoBeDatabaseSchemaEvent);
  });
}

function createController(props: {
  preliminary: AutoBePreliminaryController<
    | "analysisSections"
    | "previousAnalysisSections"
    | "previousDatabaseSchemas"
    | "complete"
  >;
  targetComponent: AutoBeDatabaseComponent;
  otherComponents: AutoBeDatabaseComponent[];
  design: AutoBeDatabaseComponentTableDesign;
  build: (next: IAutoBeDatabaseSchemaApplication.IWrite) => void;
  dispatch: AutoBeContext["dispatch"];
}): IAgenticaController.IClass {
  const validate: Validator = (input) => {
    const result: IValidation<IAutoBeDatabaseSchemaApplication.IProps> =
      typia.validate<IAutoBeDatabaseSchemaApplication.IProps>(input);
    if (result.success === false) return result;
    else if (result.data.request.type !== "write")
      return props.preliminary.validate({
        thinking: result.data.thinking,
        request: result.data.request,
      });

    const errors: IValidation.IError[] = [];
    AutoBeDatabaseSchemaProgrammer.validate({
      path: "$input.request.definition",
      errors,
      targetTable: props.design.name,
      otherTables: [props.targetComponent, ...props.otherComponents]
        .flatMap((c) => c.tables.map((t) => t.name))
        .filter((s) => s !== props.design.name),
      definition: result.data.request.definition,
    });
    if (errors.length !== 0)
      return {
        success: false,
        data: result.data,
        errors,
      };
    return result;
  };
  const application: ILlmApplication = props.preliminary.fixApplication(
    typia.llm.application<IAutoBeDatabaseSchemaApplication>({
      validate: {
        process: validate,
      },
    }),
  );
  return {
    protocol: "class",
    name: SOURCE,
    application,
    execute: {
      process: (next) => {
        if (next.request.type === "write") props.build(next.request);
      },
    } satisfies IAutoBeDatabaseSchemaApplication,
  };
}

type Validator = (
  input: unknown,
) => IValidation<IAutoBeDatabaseSchemaApplication.IProps>;

const SOURCE = "databaseSchema" satisfies AutoBeEventSource;
