import { getFormProps, getInputProps, getSelectProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { CheckIcon, XMarkIcon } from "@heroicons/react/20/solid";
import {
  type FetcherWithComponents,
  Form,
  useActionData,
  useLocation,
  useNavigation,
} from "@remix-run/react";
import type { ActionFunctionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { parseExpression } from "cron-parser";
import cronstrue from "cronstrue";
import { useState } from "react";
import {
  EnvironmentCombo,
  environmentTextClassName,
  environmentTitle,
} from "~/components/environments/EnvironmentLabel";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { CheckboxWithLabel } from "~/components/primitives/Checkbox";
import { DateTime } from "~/components/primitives/DateTime";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormError } from "~/components/primitives/FormError";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { Hint } from "~/components/primitives/Hint";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Select, SelectItem } from "~/components/primitives/Select";
import { Spinner } from "~/components/primitives/Spinner";
import {
  Table,
  TableBody,
  TableCell,
  TableHeader,
  TableHeaderCell,
  TableRow,
} from "~/components/primitives/Table";
import { TextLink } from "~/components/primitives/TextLink";
import { TimezoneList } from "~/components/scheduled/timezones";
import { prisma } from "~/db.server";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import type { EditableScheduleElements } from "~/presenters/v3/EditSchedulePresenter.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
import { EnvironmentParamSchema, docsPath, v3EnvironmentPath } from "~/utils/pathBuilder";
import { CronPattern, UpsertSchedule } from "~/v3/schedules";
import { UpsertTaskScheduleService } from "~/v3/services/upsertTaskSchedule.server";
import { AIGeneratedCronField } from "../resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language";

const cronFormat = `*    *    *    *    *
┬    ┬    ┬    ┬    ┬
│    │    │    │    |
│    │    │    │    └ day of week (0 - 7, 1L - 7L) (0 or 7 is Sun)
│    │    │    └───── month (1 - 12)
│    │    └────────── day of month (1 - 31, L)
│    └─────────────── hour (0 - 23)
└──────────────────── minute (0 - 59)`;

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

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

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

  // `_format=json` → return JSON instead of redirecting; caller toasts.
  const wantsJson = formData.get("_format") === "json";

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

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

    const createSchedule = new UpsertTaskScheduleService();
    const result = await createSchedule.call(project.id, submission.value);

    const message =
      submission.value?.friendlyId === result.id ? "Schedule updated" : "Schedule created";

    if (wantsJson) {
      return json({ ok: true as const, message });
    }

    return redirectWithSuccessMessage(
      v3EnvironmentPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }),
      request,
      message
    );
  } catch (error: any) {
    logger.error("Failed to create schedule", error);

    const errorMessage = `Something went wrong. Please try again.`;
    if (wantsJson) {
      return json({ ok: false as const, message: errorMessage }, { status: 500 });
    }
    return redirectWithErrorMessage(
      v3EnvironmentPath({ slug: organizationSlug }, { slug: projectParam }, { slug: envParam }),
      request,
      errorMessage
    );
  }
};

type CronPatternResult =
  | {
      isValid: true;
      description: string;
    }
  | {
      isValid: false;
      error: string;
    };

export function UpsertScheduleForm({
  schedule,
  possibleTasks,
  possibleEnvironments,
  possibleTimezones,
  showGenerateField,
  defaultTaskIdentifier,
  onCancel,
  submitFetcher,
}: EditableScheduleElements & {
  showGenerateField: boolean;
  /** Pre-fills the Task field on new schedules. Ignored when editing. */
  defaultTaskIdentifier?: string;
  /** When set, Cancel calls back instead of navigating. */
  onCancel?: () => void;
  /** Submits via this fetcher with `_format=json` so the host can toast/close itself. */
  submitFetcher?: FetcherWithComponents<unknown>;
}) {
  const actionData = useActionData();
  // Only feed conform-shaped data (`status`) to `useForm` — `{ ok, message }`
  // envelopes lack it and crash conform.
  const fetcherSubmission =
    submitFetcher?.data && typeof submitFetcher.data === "object" && "status" in submitFetcher.data
      ? submitFetcher.data
      : undefined;
  const lastSubmission = submitFetcher ? fetcherSubmission : actionData;
  const [selectedTimezone, setSelectedTimezone] = useState<string>(schedule?.timezone ?? "UTC");
  const isUtc = selectedTimezone === "UTC";
  const [cronPattern, setCronPattern] = useState<string>(schedule?.cron ?? "");
  const navigation = useNavigation();
  const isLoading = submitFetcher ? submitFetcher.state !== "idle" : navigation.state !== "idle";
  const organization = useOrganization();
  const project = useProject();
  const environment = useEnvironment();
  const location = useLocation();

  const [form, { taskIdentifier, cron, timezone, externalId, environments, deduplicationKey }] =
    useForm({
      // Disambiguate per-schedule so both sheets (create + edit) can
      // coexist without duplicate DOM ids breaking `htmlFor` / conform.
      id: schedule?.friendlyId ? `edit-schedule-${schedule.friendlyId}` : "create-schedule",
      // TODO: type this
      lastResult: lastSubmission as any,
      shouldRevalidate: "onSubmit",
      onValidate({ formData }) {
        return parseWithZod(formData, { schema: UpsertSchedule });
      },
    });

  let cronPatternResult: CronPatternResult | undefined = undefined;
  let nextRuns: Date[] | undefined = undefined;

  if (cronPattern !== "") {
    const result = CronPattern.safeParse(cronPattern);

    if (!result.success) {
      cronPatternResult = {
        isValid: false,
        error: result.error.errors[0].message,
      };
    } else {
      try {
        const expression = parseExpression(
          cronPattern,
          isUtc ? { utc: true } : { tz: selectedTimezone }
        );
        cronPatternResult = {
          isValid: true,
          description: cronstrue.toString(cronPattern),
        };
        nextRuns = Array.from({ length: 5 }, (_, i) => {
          const utc = expression.next().toDate();
          return utc;
        });
      } catch (e) {
        cronPatternResult = {
          isValid: false,
          error: e instanceof Error ? e.message : JSON.stringify(e),
        };
      }
    }
  }

  const mode = schedule ? "edit" : "new";
  const FormComponent = submitFetcher?.Form ?? Form;

  return (
    <FormComponent
      method="post"
      action={`/resources/orgs/${organization.slug}/projects/${project.slug}/env/${environment.slug}/schedules/new`}
      {...getFormProps(form)}
      className="grid h-full max-h-full grid-rows-[2.5rem_1fr_auto] overflow-hidden bg-background-bright"
    >
      <div className="mx-3 flex min-w-0 items-center justify-between gap-2 overflow-hidden border-b border-grid-dimmed">
        <Header2 className="truncate">
          {schedule?.friendlyId
            ? "Edit schedule"
            : defaultTaskIdentifier
              ? `New schedule for ${defaultTaskIdentifier}`
              : "New schedule"}
        </Header2>
      </div>
      <div className="overflow-y-scroll scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
        <div className="p-3">
          {submitFetcher ? <input type="hidden" name="_format" value="json" /> : null}
          {schedule && <input type="hidden" name="friendlyId" value={schedule.friendlyId} />}
          <Fieldset>
            {(() => {
              // Lock the task via hidden input when it's implied (sheet on a task page, or editing).
              const lockedTaskIdentifier = schedule?.taskIdentifier ?? defaultTaskIdentifier;
              return lockedTaskIdentifier ? (
                <input type="hidden" name={taskIdentifier.name} value={lockedTaskIdentifier} />
              ) : (
                <InputGroup>
                  <Label htmlFor={taskIdentifier.id}>Task</Label>
                  <Select
                    {...getSelectProps(taskIdentifier)}
                    placeholder="Select a task"
                    defaultValue={schedule?.taskIdentifier}
                    heading={"Filter..."}
                    items={possibleTasks}
                    filter={(task, search) => task.toLowerCase().includes(search.toLowerCase())}
                    dropdownIcon
                    variant="tertiary/medium"
                  >
                    {(matches) => (
                      <>
                        {matches?.map((task) => (
                          <SelectItem key={task} value={task}>
                            {task}
                          </SelectItem>
                        ))}
                      </>
                    )}
                  </Select>
                  <FormError id={taskIdentifier.errorId}>{taskIdentifier.errors}</FormError>
                </InputGroup>
              );
            })()}
            {showGenerateField && <AIGeneratedCronField onSuccess={setCronPattern} />}
            <InputGroup>
              <Label
                htmlFor={cron.id}
                tooltip={
                  <div className="spacy-y-3">
                    <Paragraph variant="extra-small">We support this CRON format:</Paragraph>
                    <code>
                      <pre>{cronFormat}</pre>
                    </code>
                    <Paragraph variant="extra-small">"L" means the last.</Paragraph>
                  </div>
                }
              >
                CRON pattern (UTC)
              </Label>
              <Input
                {...getInputProps(cron, { type: "text" })}
                placeholder="? ? ? ? ?"
                required={true}
                value={cronPattern}
                onChange={(e) => {
                  setCronPattern(e.target.value);
                }}
              />
              {cronPatternResult === undefined ? (
                <Hint>Enter a CRON pattern or use natural language above.</Hint>
              ) : cronPatternResult.isValid ? (
                <ValidCronMessage isValid={true} message={`${cronPatternResult.description}.`} />
              ) : (
                <ValidCronMessage isValid={false} message={cronPatternResult.error} />
              )}
            </InputGroup>
            <InputGroup>
              <Label htmlFor={timezone.id}>Timezone</Label>
              <Select
                {...getSelectProps(timezone)}
                placeholder="Select a timezone"
                defaultValue={selectedTimezone}
                value={selectedTimezone}
                setValue={(e) => {
                  if (Array.isArray(e)) return;
                  setSelectedTimezone(e);
                }}
                items={possibleTimezones}
                filter={{ keys: [(item) => item.replace(/\//g, " ").replace(/_/g, " ")] }}
                dropdownIcon
                variant="tertiary/medium"
              >
                {(matches) => <TimezoneList timezones={matches} />}
              </Select>
              <Hint>
                {isUtc
                  ? "UTC will not change with daylight savings time."
                  : "This will automatically adjust for daylight savings time."}
              </Hint>
              <FormError id={timezone.errorId}>{timezone.errors}</FormError>
            </InputGroup>
            {nextRuns !== undefined && (
              <div className="flex flex-col gap-1">
                <Header3>Next 5 runs</Header3>
                <Table>
                  <TableHeader>
                    <TableRow>
                      {!isUtc && <TableHeaderCell>{selectedTimezone}</TableHeaderCell>}
                      <TableHeaderCell>UTC</TableHeaderCell>
                    </TableRow>
                  </TableHeader>
                  <TableBody>
                    {nextRuns.map((run, index) => (
                      <TableRow key={index}>
                        {!isUtc && (
                          <TableCell>
                            <DateTime date={run} timeZone={selectedTimezone} />
                          </TableCell>
                        )}
                        <TableCell>
                          <DateTime date={run} timeZone="UTC" />
                        </TableCell>
                      </TableRow>
                    ))}
                  </TableBody>
                </Table>
              </div>
            )}
            <InputGroup>
              <Label>Environment</Label>
              <div className="flex flex-wrap items-center gap-2">
                {/* This first condition supports old schedules where we let you have multiple environments */}
                {schedule && schedule?.environments.length > 1 ? (
                  possibleEnvironments.map((environment) => (
                    <CheckboxWithLabel
                      key={environment.id}
                      id={environment.id}
                      value={environment.id}
                      name="environments"
                      type="radio"
                      label={
                        <span
                          className={cn("text-xs uppercase", environmentTextClassName(environment))}
                        >
                          {environmentTitle(environment, environment.userName)}
                        </span>
                      }
                      defaultChecked={
                        schedule?.instances.find((i) => i.environmentId === environment.id) !==
                        undefined
                      }
                      variant="button"
                    />
                  ))
                ) : (
                  <>
                    <input type="hidden" name="environments" value={environment.id} />
                    <EnvironmentCombo environment={environment} />
                  </>
                )}
              </div>
              {environment.type === "DEVELOPMENT" && (
                <Hint>
                  Note that scheduled tasks in dev environments will only run while you are
                  connected with the dev CLI.
                </Hint>
              )}
              <FormError id={environments.errorId}>{environments.errors}</FormError>
            </InputGroup>
            <InputGroup>
              <Label required={false} htmlFor={externalId.id}>
                External ID
              </Label>
              <Input
                {...getInputProps(externalId, { type: "text" })}
                placeholder="Optionally specify your own ID, e.g. user id"
                defaultValue={schedule?.externalId ?? undefined}
              />
              <Hint>
                Optionally, you can specify your own IDs (like a user ID) and then use it inside the
                run function of your task. This allows you to have per-user CRON tasks.{" "}
                <TextLink to={docsPath("v3/tasks-scheduled")}>Read the docs.</TextLink>
              </Hint>
              <FormError id={externalId.errorId}>{externalId.errors}</FormError>
            </InputGroup>
            <InputGroup>
              <Label required={false} htmlFor={deduplicationKey.id}>
                Deduplication key
              </Label>
              <Input
                {...getInputProps(deduplicationKey, { type: "text" })}
                disabled={schedule !== undefined}
                defaultValue={
                  schedule?.userProvidedDeduplicationKey ? schedule?.deduplicationKey : undefined
                }
              />
              {schedule && (
                <Paragraph variant="small">
                  You can't edit the Deduplication key on an existing schedule.
                </Paragraph>
              )}
              <Hint>
                Optionally specify a key, you can only create one schedule with this key. This is
                very useful when using the SDK and you don't want to create duplicate schedules for
                a user.
              </Hint>
              <FormError id={deduplicationKey.errorId}>{deduplicationKey.errors}</FormError>
            </InputGroup>
            <FormError>{form.errors}</FormError>
          </Fieldset>
        </div>
      </div>
      <div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-2 py-2">
        <div className="flex items-center gap-4">
          {onCancel ? (
            <Button variant="secondary/small" onClick={onCancel} type="button">
              Cancel
            </Button>
          ) : (
            <LinkButton
              to={`${v3EnvironmentPath(organization, project, environment)}${location.search}`}
              variant="secondary/small"
            >
              Cancel
            </LinkButton>
          )}
        </div>
        <div className="flex items-center gap-4">
          <Button
            variant="primary/small"
            type="submit"
            disabled={isLoading}
            shortcut={{ key: "enter", modifiers: ["mod"], enabledOnInputElements: true }}
            LeadingIcon={isLoading ? Spinner : undefined}
          >
            {buttonText(mode, isLoading)}
          </Button>
        </div>
      </div>
    </FormComponent>
  );
}

function buttonText(mode: "edit" | "new", isLoading: boolean) {
  switch (mode) {
    case "edit":
      return isLoading ? "Updating schedule" : "Update schedule";
    case "new":
      return isLoading ? "Creating schedule" : "Create schedule";
  }
}

function ValidCronMessage({ isValid, message }: { isValid: boolean; message: string }) {
  return (
    <Paragraph variant="small">
      <span className="mr-1">
        {isValid ? (
          <CheckIcon className="-mt-0.5 mr-1 inline-block h-4 w-4 text-success" />
        ) : (
          <XMarkIcon className="-mt-0.5 mr-1 inline-block h-4 w-4 text-error" />
        )}
        <span className={isValid ? "text-success" : "text-error"}>
          {isValid ? "Valid pattern:" : "Invalid pattern:"}
        </span>
      </span>
      <span>{message}</span>
    </Paragraph>
  );
}
