import { IAgenticaController } from "@agentica/core";
import {
  AutoBeOpenApi,
  AutoBeProgressEventBase,
  AutoBeTestAuthorizeFunction,
  AutoBeTestGenerateFunction,
  AutoBeTestPrepareFunction,
  AutoBeTestScenario,
  AutoBeTestWriteEvent,
} from "@autobe/interface";
import { NamingConvention } from "@typia/utils";
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 { forceRetry } from "../../utils/forceRetry";
import { validateEmptyCode } from "../../utils/validateEmptyCode";
import { getTestScenarioArtifacts } from "./compile/getTestArtifacts";
import { transformTestOperationWriteHistory } from "./histories/transformTestOperationWriteHistory";
import { AutoBeTestOperationProgrammer } from "./programmers/AutoBeTestOperationProgrammer";
import { IAutoBeTestOperationProcedure } from "./structures/IAutoBeTestOperationProcedure";
import { IAutoBeTestOperationWriteApplication } from "./structures/IAutoBeTestOperationWriteApplication";
import { IAutoBeTestScenarioArtifacts } from "./structures/IAutoBeTestScenarioArtifacts";

export async function orchestrateTestOperationWrite(
  ctx: AutoBeContext,
  props: {
    instruction: string;
    document: AutoBeOpenApi.IDocument;
    scenarios: AutoBeTestScenario[];
    authorizes: AutoBeTestAuthorizeFunction[];
    prepares: AutoBeTestPrepareFunction[];
    generates: AutoBeTestGenerateFunction[];
    progress: AutoBeProgressEventBase;
  },
): Promise<IAutoBeTestOperationProcedure[]> {
  const result: Array<IAutoBeTestOperationProcedure | null> =
    await executeCachedBatch(
      ctx,
      /**
       * Generate test code for each scenario. Maps through plans array to
       * create individual test code implementations. Each scenario is processed
       * to generate corresponding test code and progress events.
       */
      props.scenarios.map((scenario) => async (promptCacheKey) => {
        const artifacts: IAutoBeTestScenarioArtifacts =
          await getTestScenarioArtifacts(ctx, scenario);
        const usedActors: Set<string> = new Set(
          artifacts.document.operations
            .map((o) => o.authorizationActor)
            .filter((a) => a !== null),
        );

        const authorizationFunctions: AutoBeTestAuthorizeFunction[] =
          props.authorizes.filter((f) => usedActors.has(f.actor));
        const generationFunctions: AutoBeTestGenerateFunction[] =
          props.generates.filter((f) =>
            artifacts.document.operations.some(
              (o) =>
                o.method === f.endpoint.method && o.path === f.endpoint.path,
            ),
          );
        const prepareFunctions: AutoBeTestPrepareFunction[] =
          props.prepares.filter((f) =>
            Object.keys(artifacts.document.components.schemas).includes(
              f.typeName,
            ),
          );

        const counter = new Singleton(() => ++props.progress.completed);
        try {
          return await forceRetry(async () => {
            const event: AutoBeTestWriteEvent = await process(ctx, {
              document: props.document,
              scenario,
              authorizes: authorizationFunctions,
              generates: generationFunctions,
              prepares: prepareFunctions,
              artifacts,
              progress: props.progress,
              counter,
              promptCacheKey,
              instruction: props.instruction,
            });
            ctx.dispatch(event);

            if (event.function.type !== "operation")
              throw new Error(
                `Unexpected testOperationWrite function kind: ${event.function.type}`,
              );
            return {
              type: "operation",
              artifacts,
              function: event.function,
              authorizes: authorizationFunctions,
              generates: generationFunctions,
              prepares: prepareFunctions,
            } satisfies IAutoBeTestOperationProcedure;
          });
        } catch {
          counter.get();
          return null;
        }
      }),
    );
  return result.filter((r) => r !== null);
}

async function process(
  ctx: AutoBeContext,
  props: {
    document: AutoBeOpenApi.IDocument;
    authorizes: AutoBeTestAuthorizeFunction[];
    generates: AutoBeTestGenerateFunction[];
    prepares: AutoBeTestPrepareFunction[];
    scenario: AutoBeTestScenario;
    artifacts: IAutoBeTestScenarioArtifacts;
    progress: AutoBeProgressEventBase;
    counter: Singleton<number>;
    promptCacheKey: string;
    instruction: string;
  },
): Promise<AutoBeTestWriteEvent> {
  const pointer: IPointer<IAutoBeTestOperationWriteApplication.IProps | null> =
    {
      value: null,
    };
  const { metric, tokenUsage } = await ctx.conversate({
    source: "testWrite",
    controller: createController({
      functionName: props.scenario.functionName,
      build: (next) => {
        next.domain = NamingConvention.snake(next.domain);
        pointer.value = next;
      },
    }),
    enforceFunctionCall: true,
    promptCacheKey: props.promptCacheKey,
    ...(await transformTestOperationWriteHistory(ctx, {
      authorizationFunctions: props.authorizes,
      generationFunctions: props.generates,
      scenario: props.scenario,
      artifacts: props.artifacts,
      instruction: props.instruction,
    })),
  });
  if (pointer.value === null) {
    props.counter.get();
    throw new Error("Failed to create test code.");
  }

  const location: string = `test/features/api/${pointer.value.domain}/${props.scenario.functionName}.ts`;
  return {
    type: "testWrite",
    id: v7(),
    created_at: new Date().toISOString(),
    function: {
      type: "operation",
      domain: pointer.value.domain,
      scenario: props.scenario,
      name: props.scenario.functionName,
      location,
      content: await AutoBeTestOperationProgrammer.replaceImportStatements({
        compiler: await ctx.compiler(),
        artifacts: props.artifacts,
        authorizes: props.authorizes,
        prepares: props.prepares,
        generates: props.generates,
        location,
        content: pointer.value.revise.final ?? pointer.value.draft,
      }),
    },
    metric,
    tokenUsage,
    completed: props.counter.get(),
    total: props.progress.total,
    step: ctx.state().interface?.step ?? 0,
  } satisfies AutoBeTestWriteEvent;
}

function createController(props: {
  functionName: string;
  build: (next: IAutoBeTestOperationWriteApplication.IProps) => void;
}): IAgenticaController.IClass {
  const validate = (
    input: unknown,
  ): IValidation<IAutoBeTestOperationWriteApplication.IProps> => {
    const result: IValidation<IAutoBeTestOperationWriteApplication.IProps> =
      typia.validate<IAutoBeTestOperationWriteApplication.IProps>(input);
    if (result.success === false) return result;

    const errors: IValidation.IError[] = validateEmptyCode({
      name: props.functionName,
      draft: result.data.draft,
      revise: result.data.revise,
      asynchronous: true,
      path: "$input",
    });
    return errors.length
      ? {
          success: false,
          errors,
          data: result.data,
        }
      : result;
  };
  const application: ILlmApplication =
    typia.llm.application<IAutoBeTestOperationWriteApplication>({
      validate: {
        write: validate,
      },
    });
  return {
    protocol: "class",
    name: "Create Test Code",
    application,
    execute: {
      write: (next) => {
        props.build(next);
      },
    } satisfies IAutoBeTestOperationWriteApplication,
  };
}
