import { IAgenticaController, MicroAgentica } from "@agentica/core";
import { AutoBeOpenApi } from "@autobe/interface";
import { ILlmApplication, ILlmSchema } from "@samchon/openapi";
import { OpenApiV3_1Emender } from "@samchon/openapi/lib/converters/OpenApiV3_1Emender";
import { IPointer } from "tstl";
import typia from "typia";
import { v4 } from "uuid";

import { AutoBeSystemPromptConstant } from "../../constants/AutoBeSystemPromptConstant";
import { AutoBeContext } from "../../context/AutoBeContext";
import { assertSchemaModel } from "../../context/assertSchemaModel";
import { divideArray } from "../../utils/divideArray";
import { enforceToolCall } from "../../utils/enforceToolCall";
import { transformInterfaceHistories } from "./transformInterfaceHistories";

export async function orchestrateInterfaceComponents<
  Model extends ILlmSchema.Model,
>(
  ctx: AutoBeContext<Model>,
  operations: AutoBeOpenApi.IOperation[],
  capacity: number = 12,
): Promise<AutoBeOpenApi.IComponents> {
  const typeNames: Set<string> = new Set();
  for (const op of operations) {
    if (op.requestBody !== null) typeNames.add(op.requestBody.typeName);
    if (op.responseBody !== null) typeNames.add(op.responseBody.typeName);
  }
  const matrix: string[][] = divideArray({
    array: Array.from(typeNames),
    capacity,
  });
  let progress: number = 0;

  const x: AutoBeOpenApi.IComponents = {
    schemas: {},
  };
  for (const y of await Promise.all(
    matrix.map(async (it) => {
      const row: AutoBeOpenApi.IComponents = await divideAndConquer(
        ctx,
        operations,
        it,
        3,
        (count) => {
          progress += count;
        },
      );
      ctx.dispatch({
        type: "interfaceComponents",
        components: row,
        completed: progress,
        total: typeNames.size,
        step: ctx.state().analyze?.step ?? 0,
        created_at: new Date().toISOString(),
      });
      return row;
    }),
  )) {
    Object.assign(x.schemas, y.schemas);
    if (y.authorization) x.authorization = y.authorization;
  }
  return x;
}

async function divideAndConquer<Model extends ILlmSchema.Model>(
  ctx: AutoBeContext<Model>,
  operations: AutoBeOpenApi.IOperation[],
  typeNames: string[],
  retry: number,
  progress: (completed: number) => void,
): Promise<AutoBeOpenApi.IComponents> {
  const remained: Set<string> = new Set(typeNames);
  const components: AutoBeOpenApi.IComponents = {
    schemas: {},
  };
  for (let i: number = 0; i < retry; ++i) {
    if (remained.size === 0) break;
    const before: number = remained.size;
    const newbie: AutoBeOpenApi.IComponents = await process(
      ctx,
      operations,
      components,
      remained,
    );
    for (const key of Object.keys(newbie.schemas)) {
      components.schemas[key] = newbie.schemas[key];
      remained.delete(key);
    }
    if (before - remained.size !== 0) progress(before - remained.size);
  }
  return components;
}

async function process<Model extends ILlmSchema.Model>(
  ctx: AutoBeContext<Model>,
  operations: AutoBeOpenApi.IOperation[],
  oldbie: AutoBeOpenApi.IComponents,
  remained: Set<string>,
): Promise<AutoBeOpenApi.IComponents> {
  const pointer: IPointer<AutoBeOpenApi.IComponents | null> = {
    value: null,
  };
  const agentica: MicroAgentica<Model> = new MicroAgentica({
    model: ctx.model,
    vendor: ctx.vendor,
    config: {
      ...(ctx.config ?? {}),
      executor: {
        describe: null,
      },
    },
    histories: [
      ...transformInterfaceHistories(
        ctx.state(),
        AutoBeSystemPromptConstant.INTERFACE_SCHEMA,
      ),
      {
        id: v4(),
        created_at: new Date().toISOString(),
        type: "assistantMessage",
        text: [
          "Here is the OpenAPI operations generated by phase 2.",
          "",
          "```json",
          JSON.stringify(operations),
          "```",
        ].join("\n"),
      },
    ],
    tokenUsage: ctx.usage(),
    controllers: [
      createApplication({
        model: ctx.model,
        build: async (components) => {
          pointer.value ??= {
            schemas: {},
          };
          pointer.value.authorization ??= components.authorization;
          Object.assign(pointer.value.schemas, components.schemas);
        },
        pointer,
      }),
    ],
  });
  enforceToolCall(agentica);

  const already: string[] = Object.keys(oldbie.schemas);
  await agentica.conversate(
    [
      "Make type components please.",
      "",
      "Here is the list of request/response bodies' type names from",
      "OpenAPI operations. Make type components of them. If more object",
      "types are required during making the components, please make them",
      "too.",
      "",
      ...Array.from(remained).map((k) => `- \`${k}\``),
      ...(already.length !== 0
        ? [
            "",
            "> By the way, here is the list of components schemas what you've",
            "> already made. So, you don't need to make them again.",
            ">",
            ...already.map((k) => `> - \`${k}\``),
          ]
        : []),
    ].join("\n"),
  );
  if (pointer.value === null) {
    // never be happened
    throw new Error("Failed to create components.");
  }
  return OpenApiV3_1Emender.convertComponents(
    pointer.value,
  ) as AutoBeOpenApi.IComponents;
}

function createApplication<Model extends ILlmSchema.Model>(props: {
  model: Model;
  build: (components: AutoBeOpenApi.IComponents) => Promise<void>;
  pointer: IPointer<AutoBeOpenApi.IComponents | null>;
}): IAgenticaController.IClass<Model> {
  assertSchemaModel(props.model);

  const application: ILlmApplication<Model> = collection[
    props.model
  ] as unknown as ILlmApplication<Model>;
  return {
    protocol: "class",
    name: "interface",
    application,
    execute: {
      makeComponents: async (next) => {
        await props.build(next.components);
      },
    } satisfies IApplication,
  };
}

const claude = typia.llm.application<
  IApplication,
  "claude",
  { reference: true }
>();
const collection = {
  chatgpt: typia.llm.application<
    IApplication,
    "chatgpt",
    { reference: true }
  >(),
  claude,
  llama: claude,
  deepseek: claude,
  "3.1": claude,
  "3.0": typia.llm.application<IApplication, "3.0">(),
};

interface IApplication {
  /**
   * Generate OpenAPI components containing named schema types.
   *
   * This method receives a complete set of schema components and integrates
   * them into the final OpenAPI specification. It processes all entity schemas,
   * their variants, and related type definitions to ensure a comprehensive and
   * consistent API design.
   *
   * The provided components should include schemas for all entities identified
   * in the previous phases of API path/method definition and operation
   * creation. This ensures that the final OpenAPI document has complete type
   * coverage for all operations.
   *
   * CRITICAL: All schema definitions must follow the established naming
   * conventions (IEntityName, IEntityName.ICreate, etc.) and must be thoroughly
   * documented with descriptions that reference the original Prisma schema
   * comments.
   *
   * @param props Properties containing components to generate.
   */
  makeComponents(props: IMakeComponentsProps): void;
}

interface IMakeComponentsProps {
  /**
   * Complete set of schema components for the OpenAPI specification.
   *
   * This property contains comprehensive type definitions for all entities in
   * the system. It is the central repository of all named schema types that
   * will be used throughout the API specification.
   *
   * CRITICAL REQUIREMENT: All object types MUST be defined as named types in
   * the components.schemas section. Inline anonymous object definitions are
   * strictly prohibited.
   *
   * This components object should include:
   *
   * - Main entity types (IEntityName)
   * - Operation-specific variants (.ICreate, .IUpdate, .ISummary, etc.)
   * - Container types (IPage<T> for pagination)
   * - Enumeration types
   *
   * All schema definitions must include detailed descriptions that reference
   * the original Prisma schema comments and thoroughly document each property.
   * Every property that references an object must use a $ref to a named type in
   * the components.schemas section. This applies to all objects in request
   * bodies, response bodies, and properties that are objects or arrays of
   * objects.
   *
   * Example structure:
   *
   * ```typescript
   * {
   *   components: {
   *     schemas: {
   *       IUser: {
   *         type: "object",
   *         properties: {
   *           id: { type: "string", format: "uuid" },
   *           email: { type: "string", format: "email" },
   *           profile: { "$ref": "#/components/schemas/IUserProfile" }
   *         },
   *         required: ["id", "email"],
   *         description: "User entity representing system account holders..."
   *       },
   *       "IUser.ICreate": { ... },
   *       // Additional schemas
   *     }
   *   }
   * }
   * ```
   */
  components: AutoBeOpenApi.IComponents;
}
