import { parseWithZod } from "@conform-to/zod";
import { Form, useLocation, useNavigation, useSubmit } from "@remix-run/react";
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import type { WaitpointTokenStatus } from "@trigger.dev/core/v3";
import { stringifyIO, timeoutError } from "@trigger.dev/core/v3";
import { WaitpointId } from "@trigger.dev/core/v3/isomorphic";
import type { Waitpoint } from "@trigger.dev/database";
import { useCallback, useRef } from "react";
import { z } from "zod";
import { AnimatedHourglassIcon } from "~/assets/icons/AnimatedHourglassIcon";
import { JSONEditor } from "~/components/code/JSONEditor";
import { Button } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import { Paragraph } from "~/components/primitives/Paragraph";
import { SpinnerWhite } from "~/components/primitives/Spinner";
import { InfoIconTooltip } from "~/components/primitives/Tooltip";
import { LiveCountdown } from "~/components/runs/v3/LiveTimer";
import { $replica, type PrismaReplicaClient } from "~/db.server";
import { resolveWaitpointThroughReadThrough } from "~/runEngine/concerns/resolveWaitpointThroughReadThrough.server";
import { env } from "~/env.server";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { processWaitpointCompletionPacket } from "~/runEngine/concerns/waitpointCompletionPacket.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { EnvironmentParamSchema, v3RunsPath } from "~/utils/pathBuilder";
import { engine } from "~/v3/runEngine.server";

const CompleteWaitpointFormData = z.discriminatedUnion("type", [
  z.object({
    type: z.literal("MANUAL"),
    payload: z.string().optional(),
    isTimeout: z.string().optional(),
    successRedirect: z.string(),
    failureRedirect: z.string(),
  }),
  z.object({
    type: z.literal("DATETIME"),
    successRedirect: z.string(),
    failureRedirect: z.string(),
  }),
]);

const Params = EnvironmentParamSchema.extend({
  waitpointFriendlyId: z.string(),
});

export const action = async ({ request, params }: ActionFunctionArgs) => {
  const userId = await requireUserId(request);
  const { organizationSlug, projectParam, envParam, waitpointFriendlyId } = Params.parse(params);

  const formData = await request.formData();
  const submission = parseWithZod(formData, { schema: CompleteWaitpointFormData });

  if (submission.status !== "success") {
    return json(submission.reply());
  }

  try {
    //first check that the user has access to the project
    const project = await $replica.project.findUnique({
      where: {
        slug: projectParam,
        organization: {
          members: {
            some: {
              userId,
            },
          },
        },
      },
      select: { id: true },
    });

    if (!project) {
      throw new Error("Project not found");
    }

    const waitpointId = WaitpointId.toId(waitpointFriendlyId);

    const waitpoint = await resolveWaitpointThroughReadThrough({
      waitpointId,
      environmentId: "",
      read: (client: PrismaReplicaClient) =>
        client.waitpoint.findFirst({
          select: {
            projectId: true,
            environmentId: true,
          },
          where: {
            id: waitpointId,
          },
        }),
    });

    if (waitpoint?.projectId !== project.id) {
      return redirectWithErrorMessage(
        submission.value.failureRedirect,
        request,
        "No waitpoint found"
      );
    }

    switch (submission.value.type) {
      case "DATETIME": {
        const _result = await engine.completeWaitpoint({
          id: waitpointId,
        });

        return redirectWithSuccessMessage(
          submission.value.successRedirect,
          request,
          "Waitpoint skipped"
        );
      }
      case "MANUAL": {
        if (submission.value.isTimeout) {
          try {
            const _result = await engine.completeWaitpoint({
              id: waitpointId,
              output: {
                type: "application/json",
                value: JSON.stringify(timeoutError(new Date())),
                isError: true,
              },
            });

            return redirectWithSuccessMessage(
              submission.value.successRedirect,
              request,
              "Waitpoint timed out"
            );
          } catch (_e) {
            return redirectWithErrorMessage(
              submission.value.failureRedirect,
              request,
              "Invalid payload, must be valid JSON"
            );
          }
        }

        try {
          if (
            submission.value.payload &&
            submission.value.payload.length > env.TASK_PAYLOAD_MAXIMUM_SIZE
          ) {
            return redirectWithErrorMessage(
              submission.value.failureRedirect,
              request,
              "Payload is too large"
            );
          }

          const environment = await findEnvironmentBySlug(project.id, envParam, userId);
          if (!environment) {
            return redirectWithErrorMessage(
              submission.value.failureRedirect,
              request,
              "Environment not found"
            );
          }

          if (environment.id !== waitpoint.environmentId) {
            return redirectWithErrorMessage(
              submission.value.failureRedirect,
              request,
              "No waitpoint found"
            );
          }

          const data = submission.value.payload ? JSON.parse(submission.value.payload) : {};
          const stringifiedData = await stringifyIO(data);
          const finalData = await processWaitpointCompletionPacket(
            stringifiedData,
            environment,
            `${WaitpointId.toFriendlyId(waitpointId)}/token`
          );

          const _result = await engine.completeWaitpoint({
            id: waitpointId,
            output: finalData.data
              ? { type: finalData.dataType, value: finalData.data, isError: false }
              : undefined,
          });

          return redirectWithSuccessMessage(
            submission.value.successRedirect,
            request,
            "Waitpoint completed"
          );
        } catch (_e) {
          return redirectWithErrorMessage(
            submission.value.failureRedirect,
            request,
            "Invalid payload, must be valid JSON"
          );
        }
      }
    }
  } catch (error: any) {
    logger.error("Failed to complete waitpoint", error);

    const errorMessage = `Something went wrong. Please try again.`;
    return redirectWithErrorMessage(
      v3RunsPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }),
      request,
      errorMessage
    );
  }
};

type FormWaitpoint = Pick<Waitpoint, "id" | "type" | "completedAfter"> & {
  status: WaitpointTokenStatus;
};

export function CompleteWaitpointForm({ waitpoint }: { waitpoint: FormWaitpoint }) {
  return (
    <div className="space-y-3">
      {waitpoint.type === "DATETIME" ? (
        waitpoint.completedAfter ? (
          <CompleteDateTimeWaitpointForm
            waitpoint={{
              friendlyId: waitpoint.id,
              completedAfter: waitpoint.completedAfter,
            }}
          />
        ) : (
          <>Waitpoint doesn't have a complete date</>
        )
      ) : (
        <CompleteManualWaitpointForm waitpoint={waitpoint} />
      )}
    </div>
  );
}

function CompleteDateTimeWaitpointForm({
  waitpoint,
}: {
  waitpoint: { friendlyId: string; completedAfter: Date };
}) {
  const location = useLocation();
  const navigation = useNavigation();
  const isLoading = navigation.state !== "idle";
  const organization = useOrganization();
  const project = useProject();
  const environment = useEnvironment();

  const timeToComplete = waitpoint.completedAfter.getTime() - Date.now();
  if (timeToComplete < 0) {
    return (
      <div className="flex items-center justify-center">
        <Paragraph variant="small/bright">Waitpoint completed</Paragraph>
      </div>
    );
  }

  return (
    <Form
      action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/${waitpoint.friendlyId}/complete`}
      method="post"
      className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden border-t border-grid-bright"
    >
      <div className="mx-3 flex items-center">
        <Paragraph variant="small/bright">Manually skip this waitpoint</Paragraph>
      </div>
      <div className="border-t border-grid-dimmed">
        <input type="hidden" name="type" value={"DATETIME"} />
        <input
          type="hidden"
          name="successRedirect"
          value={`${location.pathname}${location.search}`}
        />
        <input
          type="hidden"
          name="failureRedirect"
          value={`${location.pathname}${location.search}`}
        />
        <div className="flex flex-wrap items-center justify-between gap-1 p-2 text-sm tabular-nums">
          <div className="flex items-center gap-1">
            <AnimatedHourglassIcon
              className="text-dimmed-dimmed size-4"
              delay={(waitpoint.completedAfter.getMilliseconds() - Date.now()) / 1000}
            />
            <span className="mt-0.5 ">
              <LiveCountdown endTime={waitpoint.completedAfter} />
            </span>
          </div>
          <DateTime date={waitpoint.completedAfter} />
        </div>
      </div>
      <div className="flex items-center justify-end border-t border-grid-dimmed bg-background-dimmed px-2">
        <Button
          variant="secondary/medium"
          type="submit"
          disabled={isLoading}
          LeadingIcon={isLoading ? SpinnerWhite : undefined}
        >
          {isLoading ? "Completing…" : "Skip waitpoint"}
        </Button>
      </div>
    </Form>
  );
}

function CompleteManualWaitpointForm({ waitpoint }: { waitpoint: { id: string } }) {
  const location = useLocation();
  const navigation = useNavigation();
  const submit = useSubmit();
  const isLoading = navigation.state !== "idle";
  const organization = useOrganization();
  const project = useProject();
  const environment = useEnvironment();

  const currentJson = useRef<string>("{\n\n}");
  const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/${waitpoint.id}/complete`;

  const submitForm = useCallback(
    (e: React.FormEvent<HTMLFormElement>) => {
      const formData = new FormData(e.currentTarget);
      const data: Record<string, string> = {
        type: formData.get("type") as string,
        failureRedirect: formData.get("failureRedirect") as string,
        successRedirect: formData.get("successRedirect") as string,
      };

      data.payload = currentJson.current;

      submit(data, {
        action: formAction,
        method: "post",
      });
      e.preventDefault();
    },
    [currentJson]
  );

  return (
    <>
      <Form
        action={formAction}
        method="post"
        onSubmit={(e) => submitForm(e)}
        className="grid h-full max-h-full grid-rows-[2.5rem_1fr_3.25rem] overflow-hidden border-t border-grid-bright"
      >
        <input type="hidden" name="type" value={"MANUAL"} />
        <input
          type="hidden"
          name="successRedirect"
          value={`${location.pathname}${location.search}`}
        />
        <input
          type="hidden"
          name="failureRedirect"
          value={`${location.pathname}${location.search}`}
        />
        <div className="mx-3 flex items-center gap-1">
          <Paragraph variant="small/bright">Manually complete this waitpoint</Paragraph>
          <InfoIconTooltip
            content={
              "This is will immediately complete this waitpoint with the payload you specify. This is useful during development for testing."
            }
            contentClassName="normal-case tracking-normal max-w-xs"
          />
        </div>
        <div className="overflow-y-auto bg-background-deep scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
          <div className="max-h-[70vh] min-h-40 overflow-y-auto bg-background-deep scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
            <JSONEditor
              autoFocus
              defaultValue={currentJson.current}
              readOnly={false}
              basicSetup
              onChange={(v) => {
                currentJson.current = v;
              }}
              showClearButton={false}
              showCopyButton={false}
              height="100%"
              min-height="100%"
              max-height="100%"
            />
          </div>
        </div>
        <div className="flex items-center justify-end gap-2 border-t border-grid-dimmed bg-background-dimmed px-2">
          <Button
            variant="secondary/medium"
            type="submit"
            disabled={isLoading}
            LeadingIcon={isLoading ? SpinnerWhite : undefined}
          >
            {isLoading ? "Completing…" : "Complete waitpoint"}
          </Button>
        </div>
      </Form>
    </>
  );
}

export function ForceTimeout({ waitpoint }: { waitpoint: { id: string } }) {
  const location = useLocation();
  const navigation = useNavigation();
  const isLoading = navigation.state !== "idle";
  const organization = useOrganization();
  const project = useProject();
  const environment = useEnvironment();

  const formAction = `/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/waitpoints/${waitpoint.id}/complete`;

  return (
    <Form action={formAction} method="post">
      <input type="hidden" name="type" value={"MANUAL"} />
      <input type="hidden" name="isTimeout" value={"1"} />
      <input
        type="hidden"
        name="successRedirect"
        value={`${location.pathname}${location.search}`}
      />
      <input
        type="hidden"
        name="failureRedirect"
        value={`${location.pathname}${location.search}`}
      />
      <Button
        variant="tertiary/small"
        type="submit"
        disabled={isLoading}
        LeadingIcon={isLoading ? SpinnerWhite : undefined}
      >
        {isLoading ? "Forcing timeout…" : "Force timeout"}
      </Button>
    </Form>
  );
}
