import {
  AgenticaJsonParseError,
  AgenticaValidationError,
  IMicroAgenticaConfig,
  MicroAgentica,
  MicroAgenticaHistory,
} from "@agentica/core";
import {
  AutoBeAnalyzeCompleteEvent,
  AutoBeAnalyzeHistory,
  AutoBeAnalyzeStartEvent,
  AutoBeAssistantMessageEvent,
  AutoBeDatabaseCompleteEvent,
  AutoBeDatabaseHistory,
  AutoBeDatabaseStartEvent,
  AutoBeEvent,
  AutoBeFunctionCallingMetric,
  AutoBeHistory,
  AutoBeInterfaceCompleteEvent,
  AutoBeInterfaceHistory,
  AutoBeInterfaceStartEvent,
  AutoBePhase,
  AutoBeProcessAggregate,
  AutoBeProcessAggregateCollection,
  AutoBeRealizeCompleteEvent,
  AutoBeRealizeHistory,
  AutoBeRealizeStartEvent,
  AutoBeTestCompleteEvent,
  AutoBeTestHistory,
  AutoBeTestStartEvent,
  IAutoBeCompiler,
  IAutoBeCompilerListener,
  IAutoBeGetFilesOptions,
  IAutoBeTokenUsageJson,
} from "@autobe/interface";
import {
  AutoBeProcessAggregateFactory,
  StringUtil,
  TokenUsageComputer,
} from "@autobe/utils";
import { APIError, BadRequestError } from "openai";
import { Semaphore, Singleton } from "tstl";
import typia from "typia";
import { v7 } from "uuid";

import { AutoBeConfigConstant } from "../constants/AutoBeConfigConstant";
import { AutoBeContext } from "../context/AutoBeContext";
import { AutoBeState } from "../context/AutoBeState";
import { AutoBeTokenUsage } from "../context/AutoBeTokenUsage";
import { AutoBeTokenUsageComponent } from "../context/AutoBeTokenUsageComponent";
import { IAutoBeConfig } from "../structures/IAutoBeConfig";
import { IAutoBeVendor } from "../structures/IAutoBeVendor";
import { TimedConversation } from "../utils/TimedConversation";
import { forceRetry } from "../utils/forceRetry";
import { consentFunctionCall } from "./consentFunctionCall";
import { getCriticalCompiler } from "./getCriticalCompiler";
import { mergeSystemMessages } from "./mergeSystemMessages";
// import { supportFunctionCallFallback } from "./supportFunctionCallFallback";
import { supportMistral } from "./supportMistral";

export const createAutoBeContext = (props: {
  vendor: IAutoBeVendor;
  compiler: () => Promise<IAutoBeCompiler>;
  compilerListener: IAutoBeCompilerListener;
  config: IAutoBeConfig;
  state: () => AutoBeState;
  files: (options: IAutoBeGetFilesOptions) => Promise<Record<string, string>>;
  histories: () => AutoBeHistory[];
  usage: () => AutoBeTokenUsage;
  dispatch: (event: AutoBeEvent) => Promise<void>;
  aggregates: AutoBeProcessAggregateCollection;
}): AutoBeContext => {
  const config: Required<Omit<IAutoBeConfig, "backoffStrategy" | "timezone">> =
    {
      retry: props.config.retry ?? AutoBeConfigConstant.VALIDATION_RETRY,
      locale: props.config.locale ?? "en-US",
      timeout: props.config.timeout ?? AutoBeConfigConstant.TIMEOUT,
    };
  const critical: Semaphore = new Semaphore(2);
  return {
    vendor: props.vendor,
    retry: config.retry,
    locale: config.locale,
    aggregates: props.aggregates,
    compilerListener: props.compilerListener,
    compiler: async () => {
      const compiler = await props.compiler();
      return getCriticalCompiler(critical, compiler);
    },
    files: props.files,
    histories: props.histories,
    state: props.state,
    usage: props.usage,
    dispatch: createDispatch(props),
    assistantMessage: (message) => {
      props.histories().push(message);
      setTimeout(() => {
        void props.dispatch(message).catch(() => {});
      });
      return message;
    },
    conversate: async (next, closure): Promise<AutoBeContext.IResult> => {
      const aggregate: AutoBeProcessAggregate =
        AutoBeProcessAggregateFactory.createAggregate();
      const progress: IProgress = {
        request: 0,
        response: 0,
        timeout: 0,
      };

      const metric = (key: keyof AutoBeFunctionCallingMetric): void => {
        const accumulate = (collection: AutoBeProcessAggregateCollection) => {
          ++collection.total.metric[key];
          collection[next.source as "analyzeWriteModule"] ??=
            AutoBeProcessAggregateFactory.createAggregate();
          ++collection[next.source as "analyzeWriteModule"]!.metric[key];
        };
        ++aggregate.metric[key];
        accumulate(props.aggregates);
      };

      const consume = (tokenUsage: IAutoBeTokenUsageJson.IComponent): void => {
        const accumulate = (
          collection: AutoBeProcessAggregateCollection,
        ): void => {
          TokenUsageComputer.increment(collection.total.tokenUsage, tokenUsage);
          collection[next.source as "analyzeWriteModule"] ??=
            AutoBeProcessAggregateFactory.createAggregate();
          TokenUsageComputer.increment(
            collection[next.source as "analyzeWriteModule"]!.tokenUsage,
            tokenUsage,
          );
        };
        TokenUsageComputer.increment(aggregate.tokenUsage, tokenUsage);
        accumulate(props.aggregates);
        props
          .usage()
          .record(tokenUsage, [
            STAGES.find((stage) => next.source.startsWith(stage)) ?? "analyze",
          ]);
      };

      const execute = async (): Promise<AutoBeContext.IResult> => {
        // CREATE AGENT
        const agent: MicroAgentica = new MicroAgentica({
          vendor: props.vendor,
          config: {
            ...(props.config ?? {}),
            executor: {
              describe: false,
            },
            retry:
              next.retry ??
              props.config?.retry ??
              AutoBeConfigConstant.VALIDATION_RETRY,
            // stream: false,
            stream: next.enforceFunctionCall === false,
          } satisfies IMicroAgenticaConfig,
          histories: next.histories,
          controllers: [next.controller],
        });
        supportMistral(agent, props.vendor);
        // supportFunctionCallFallback(agent, props.vendor);
        mergeSystemMessages(agent, props.vendor);

        // ADD EVENT LISTENERS
        agent.on("request", async (event): Promise<void> => {
          if (
            next.enforceFunctionCall === true &&
            !!event.body.tools?.length &&
            (props.vendor.useToolChoice ?? true) === true
          )
            event.body.tool_choice = "required";
          else if (event.body.tool_choice !== undefined)
            delete event.body.tool_choice;
          if (event.body.parallel_tool_calls !== undefined)
            delete event.body.parallel_tool_calls;
          if (next.promptCacheKey)
            event.body.prompt_cache_key = next.promptCacheKey;
          // event.body.max_tokens = 32768; // for deepseek v3.1

          await props.dispatch({
            ...event,
            type: "vendorRequest",
            source: next.source,
            retry: progress.request++,
          });
        });
        agent.on("response", (event) => {
          void props
            .dispatch({
              ...event,
              type: "vendorResponse",
              source: next.source,
              retry: progress.response++,
            })
            .catch(() => {});
        });
        agent.on("call", () => {
          metric("attempt");
        });
        agent.on("jsonParseError", (event) => {
          metric("invalidJson");
          void props
            .dispatch({
              ...event,
              function: event.operation.function.name,
              source: next.source,
            })
            .catch(() => {});
        });
        agent.on("validate", (event) => {
          metric("validationFailure");
          void props
            .dispatch({
              type: "jsonValidateError",
              id: v7(),
              source: next.source,
              function: event.operation.function.name,
              result: event.result,
              life: event.life,
              created_at: event.created_at,
            })
            .catch(() => {});
        });
        if (closure) closure(agent);

        // DO CONVERSATE
        const message: string =
          next.enforceFunctionCall === true
            ? StringUtil.trim`
                ${next.userMessage}

                > You have to call function(s) of below to accomplish my request.
                >
                > Never hesitate the function calling. Never ask for me permission
                > to execute the function. Never explain me your plan with waiting
                > for my approval.
                >
                > I gave you every information for the function calling, so just
                > call it. I repeat that, never hesitate the function calling.
                > Just do it without any explanation.
                >
                ${next.controller.application.functions
                  .map((f) => `> - ${f.name}`)
                  .join("\n")}
              `
            : next.userMessage;
        const result: TimedConversation.IResult =
          await TimedConversation.process({
            timeout: config.timeout,
            agent,
            message,
          });
        const tokenUsage: IAutoBeTokenUsageJson.IComponent = agent
          .getTokenUsage()
          .toJSON().aggregate;
        props
          .usage()
          .record(tokenUsage, [
            STAGES.find((stage) => next.source.startsWith(stage)) ?? "analyze",
          ]);
        consume(tokenUsage);

        const success = (histories: MicroAgenticaHistory[]) => {
          metric("success");
          return {
            histories,
            tokenUsage: aggregate.tokenUsage,
            metric: aggregate.metric,
            __agent: agent,
          };
        };
        if (result.type === "error") throw result.error;
        else if (result.type === "timeout") {
          void props
            .dispatch({
              type: "vendorTimeout",
              id: v7(),
              source: next.source,
              timeout: config.timeout!,
              retry: progress.timeout++,
              created_at: new Date().toISOString(),
            })
            .catch(() => {});
          throw result.error;
        } else if (
          true === next.enforceFunctionCall &&
          false === result.histories.some((h) => h.type === "execute")
        ) {
          const failure = () => {
            throw new Error(
              StringUtil.trim`
                Failed to function calling in the ${next.source} step.

                Here is the list of history types that occurred during the conversation:

                ${result.histories.map((h) => `- ${h.type}`).join("\n")}

                \`\`\`json
                ${JSON.stringify(result.histories, null, 2)}
                \`\`\`
              `,
            );
          };
          const last: MicroAgenticaHistory | undefined =
            result.histories.at(-1);
          if (
            last?.type === "assistantMessage" ||
            (result.histories.length === 1 && last?.type === "userMessage")
          ) {
            metric("consent");
            const consent: string | null = await consentFunctionCall({
              source: next.source,
              dispatch: (e) => {
                props.dispatch(e).catch(() => {});
              },
              config: props.config,
              vendor: props.vendor,
              assistantMessage:
                last?.type === "assistantMessage" ? last.text.trim() : "",
            });
            if (consent !== null) {
              const newHistories: MicroAgenticaHistory[] =
                await agent.conversate(consent);
              const newTokenUsage: IAutoBeTokenUsageJson.IComponent =
                AutoBeTokenUsageComponent.minus(
                  new AutoBeTokenUsageComponent(
                    agent.getTokenUsage().toJSON().aggregate,
                  ),
                  new AutoBeTokenUsageComponent(tokenUsage),
                );
              consume(newTokenUsage);
              if (newHistories.some((h) => h.type === "execute"))
                return success(newHistories);
            }
          }
          // Retry with explicit failure feedback
          const functionNames: string = next.controller.application.functions
            .map((f) => f.name)
            .join(", ");
          for (
            let retry = 0;
            retry < AutoBeConfigConstant.FUNCTION_CALLING_RETRY - 1;
            retry++
          ) {
            metric("consent");
            const retryMessage: string =
              `You failed to call any function. ` +
              `You MUST call one of these functions immediately: ${functionNames}. ` +
              `Do not explain anything. Just call the function right now.`;
            const retryHistories: MicroAgenticaHistory[] =
              await agent.conversate(retryMessage);
            const retryTokenUsage: IAutoBeTokenUsageJson.IComponent =
              AutoBeTokenUsageComponent.minus(
                new AutoBeTokenUsageComponent(
                  agent.getTokenUsage().toJSON().aggregate,
                ),
                new AutoBeTokenUsageComponent(tokenUsage),
              );
            consume(retryTokenUsage);
            if (retryHistories.some((h) => h.type === "execute"))
              return success(retryHistories);
          }
          failure();
        }
        return success(result.histories);
      };
      return await forceRetry(
        execute,
        AutoBeConfigConstant.API_ERROR_RETRY,
        (error) => {
          // Context overflow and other permanent 400 errors should not be
          // retried — the same payload will always produce the same failure.
          if (error instanceof BadRequestError) {
            const errBody = error as unknown as {
              error?: { metadata?: { raw?: string }; message?: string };
            };
            const msg = String(
              errBody.error?.metadata?.raw ??
                errBody.error?.message ??
                error.message ??
                "",
            );
            const permanent = [
              "context_length_exceeded",
              "maximum context length",
              "request too large",
            ];
            if (permanent.some((p) => msg.includes(p))) return false;
          }
          return (
            error instanceof APIError ||
            error instanceof BadRequestError ||
            error instanceof AgenticaJsonParseError ||
            error instanceof AgenticaValidationError ||
            error instanceof TypeError ||
            (error instanceof Error &&
              error.message.startsWith("OpenRouter upstream error")) ||
            (error instanceof Error &&
              OPENAI_API_ERROR_KEYS.get().every((key) =>
                error.hasOwnProperty(key),
              ))
          );
        },
      );
    },
    getCurrentAggregates: (phase) => {
      const previous: AutoBeProcessAggregateCollection =
        AutoBeProcessAggregateFactory.reduce(
          props
            .histories()
            .filter(
              (h) =>
                h.type === "analyze" ||
                h.type === "database" ||
                h.type === "interface" ||
                h.type === "test" ||
                h.type === "realize",
            )
            .map((h) => h.aggregates),
        );
      return AutoBeProcessAggregateFactory.filterPhase(
        AutoBeProcessAggregateFactory.minus(props.aggregates, previous),
        phase,
      );
    },
  };
};

const createDispatch = (props: {
  state: () => AutoBeState;
  histories: () => AutoBeHistory[];
  dispatch: (event: AutoBeEvent) => Promise<void>;
}) => {
  let analyzeStart: AutoBeAnalyzeStartEvent | null = null;
  let databaseStart: AutoBeDatabaseStartEvent | null = null;
  let interfaceStart: AutoBeInterfaceStartEvent | null = null;
  let testStart: AutoBeTestStartEvent | null = null;
  let realizeStart: AutoBeRealizeStartEvent | null = null;
  return <Event extends Exclude<AutoBeEvent, AutoBeAssistantMessageEvent>>(
    event: Event,
  ): AutoBeContext.DispatchHistory<Event> => {
    // starts
    if (event.type === "analyzeStart") analyzeStart = event;
    else if (event.type === "databaseStart") databaseStart = event;
    else if (event.type === "interfaceStart") interfaceStart = event;
    else if (event.type === "testStart") testStart = event;
    else if (event.type === "realizeStart") realizeStart = event;
    // completes
    else if (event.type === "analyzeComplete")
      return transformAndDispatch<AutoBeAnalyzeCompleteEvent>({
        dispatch: props.dispatch,
        histories: props.histories,
        state: props.state,
        event,
        history: {
          type: "analyze",
          id: v7(),
          prefix: event.prefix,
          actors: event.actors,
          files: event.files,
          aggregates: event.aggregates,
          step: event.step,
          created_at: analyzeStart?.created_at ?? new Date().toISOString(),
          completed_at: event.created_at,
        } satisfies AutoBeAnalyzeHistory,
      }) as AutoBeContext.DispatchHistory<Event>;
    else if (event.type === "databaseComplete")
      return transformAndDispatch<AutoBeDatabaseCompleteEvent>({
        dispatch: props.dispatch,
        histories: props.histories,
        state: props.state,
        event,
        history: {
          type: "database",
          id: v7(),
          instruction: databaseStart?.reason ?? "",
          schemas: event.schemas,
          result: event.result,
          compiled: event.compiled,
          aggregates: event.aggregates,
          step: event.step,
          created_at: databaseStart?.created_at ?? new Date().toISOString(),
          completed_at: event.created_at,
        } satisfies AutoBeDatabaseHistory,
      }) as AutoBeContext.DispatchHistory<Event>;
    else if (event.type === "interfaceComplete")
      return transformAndDispatch({
        dispatch: props.dispatch,
        histories: props.histories,
        state: props.state,
        event,
        history: {
          type: "interface",
          id: v7(),
          instruction: interfaceStart?.reason ?? "",
          authorizations: event.authorizations,
          document: event.document,
          missed: event.missed,
          aggregates: event.aggregates,
          step: event.step,
          created_at: interfaceStart?.created_at ?? new Date().toISOString(),
          completed_at: new Date().toISOString(),
        } satisfies AutoBeInterfaceHistory,
      }) as AutoBeContext.DispatchHistory<Event>;
    else if (event.type === "testComplete")
      return transformAndDispatch<AutoBeTestCompleteEvent>({
        dispatch: props.dispatch,
        histories: props.histories,
        state: props.state,
        event,
        history: {
          type: "test",
          id: v7(),
          instruction: testStart?.reason ?? "",
          functions: event.functions,
          compiled: event.compiled,
          aggregates: event.aggregates,
          step: event.step,
          created_at: testStart?.created_at ?? new Date().toISOString(),
          completed_at: new Date().toISOString(),
        } satisfies AutoBeTestHistory,
      }) as AutoBeContext.DispatchHistory<Event>;
    else if (event.type === "realizeComplete")
      return transformAndDispatch<AutoBeRealizeCompleteEvent>({
        dispatch: props.dispatch,
        histories: props.histories,
        state: props.state,
        event,
        history: {
          type: "realize",
          id: v7(),
          instruction: realizeStart?.reason ?? "",
          authorizations: event.authorizations,
          functions: event.functions,
          controllers: event.controllers,
          compiled: event.compiled,
          aggregates: event.aggregates,
          step: event.step,
          created_at: realizeStart?.created_at ?? new Date().toISOString(),
          completed_at: new Date().toISOString(),
        } satisfies AutoBeRealizeHistory,
      }) as AutoBeContext.DispatchHistory<Event>;

    void props.dispatch(event).catch(() => {});
    return null as AutoBeContext.DispatchHistory<Event>;
  };
};
const transformAndDispatch = <
  Event extends
    | AutoBeAnalyzeCompleteEvent
    | AutoBeDatabaseCompleteEvent
    | AutoBeInterfaceCompleteEvent
    | AutoBeTestCompleteEvent
    | AutoBeRealizeCompleteEvent,
>(props: {
  dispatch: (event: Event) => Promise<void>;
  histories: () => AutoBeHistory[];
  state: () => AutoBeState;
  event: Event;
  history: NonNullable<AutoBeContext.DispatchHistory<Event>>;
}): NonNullable<AutoBeContext.DispatchHistory<Event>> => {
  props.histories().push(props.history);
  // biome-ignore lint: intended
  props.state()[props.history.type] = props.history as any;
  void props.dispatch(props.event).catch(() => {});
  return props.history;
};

const STAGES =
  typia.misc.literals<
    keyof Pick<IAutoBeTokenUsageJson, "facade" | AutoBePhase>
  >();

const OPENAI_API_ERROR_KEYS = new Singleton(() =>
  Object.keys(new APIError(undefined, undefined, undefined, undefined)),
);

interface IProgress {
  request: number;
  response: number;
  timeout: number;
}
