import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { CommandLineIcon, FolderIcon } from "@heroicons/react/20/solid";
import { json, type ActionFunction, type LoaderFunctionArgs } from "@remix-run/node";
import { Form, useActionData, useNavigation } from "@remix-run/react";
import type { Prisma } from "@trigger.dev/database";
import React, { useEffect, useState } from "react";
import { redirect, typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { z } from "zod";
import { BackgroundWrapper } from "~/components/BackgroundWrapper";
import { Feedback } from "~/components/Feedback";
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
import { TechnologyPicker } from "~/components/onboarding/TechnologyPicker";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Callout } from "~/components/primitives/Callout";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { FormTitle } from "~/components/primitives/FormTitle";
import { Input } from "~/components/primitives/Input";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Select, SelectItem } from "~/components/primitives/Select";

import { prisma } from "~/db.server";
import { featuresForRequest } from "~/features.server";
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
import { createProject, ExceededProjectLimitError } from "~/models/project.server";
import { requireUserId } from "~/services/session.server";
import {
  newProjectPath,
  OrganizationParamsSchema,
  organizationPath,
  selectPlanPath,
  v3ProjectPath,
} from "~/utils/pathBuilder";
import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";

const WORKING_ON_OTHER = "Other/not sure yet";
const GOALS_OTHER = "Other/not sure yet";

const workingOnOptions = [
  "AI agent",
  "Media processing pipeline",
  "Media generation with AI",
  "Event-driven workflow",
  "Realtime streaming",
  "Internal tool or background job",
  WORKING_ON_OTHER,
] as const;

const goalOptions = [
  "Ship a production workflow",
  "Prototype or explore",
  "Migrate an existing system",
  "Learn how Trigger works",
  "Evaluate against alternatives",
  GOALS_OTHER,
] as const;

function shuffleArray<T>(arr: T[]): T[] {
  const shuffled = [...arr];
  for (let i = shuffled.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
  }
  return shuffled;
}

function MultiSelectField({
  value,
  setValue,
  items,
  icon,
}: {
  value: string[];
  setValue: (value: string[]) => void;
  items: string[];
  icon: React.ReactNode;
}) {
  return (
    <Select<string[], string>
      value={value}
      setValue={setValue}
      placeholder="Select some options"
      variant="secondary/small"
      dropdownIcon
      icon={icon}
      items={items}
      className="h-8 min-w-0 border-0 bg-background-hover pl-2 text-sm text-text-dimmed ring-border-bright transition hover:bg-secondary hover:text-text-dimmed hover:ring-1"
      text={(v) =>
        v.length === 0 ? undefined : (
          <span className="flex min-w-0 items-center text-text-bright">
            <span className="truncate">{v.slice(0, 2).join(", ")}</span>
            {v.length > 2 && <span className="ml-1 flex-none">+{v.length - 2} more</span>}
          </span>
        )
      }
    >
      {(items) =>
        items.map((item) => (
          <SelectItem key={item} value={item} checkPosition="left">
            <span className="text-text-bright">{item}</span>
          </SelectItem>
        ))
      }
    </Select>
  );
}

export async function loader({ params, request }: LoaderFunctionArgs) {
  const userId = await requireUserId(request);
  const { organizationSlug } = OrganizationParamsSchema.parse(params);

  const organization = await prisma.organization.findFirst({
    where: { slug: organizationSlug, members: { some: { userId } } },
    select: {
      id: true,
      title: true,
      isActivated: true,
      v2Enabled: true,
      hasRequestedV3: true,
      _count: {
        select: {
          projects: {
            where: {
              deletedAt: null,
            },
          },
        },
      },
    },
  });

  if (!organization) {
    throw new Response(null, { status: 404, statusText: "Organization not found" });
  }

  const { isManagedCloud } = featuresForRequest(request);
  if (isManagedCloud && !organization.isActivated) {
    return redirect(selectPlanPath({ slug: organizationSlug }));
  }

  const url = new URL(request.url);
  const message = url.searchParams.get("message");

  return typedjson({
    organization: {
      id: organization.id,
      title: organization.title,
      slug: organizationSlug,
      projectsCount: organization._count.projects,
      isActivated: organization.isActivated,
      v2Enabled: organization.v2Enabled,
      hasRequestedV3: organization.hasRequestedV3,
    },
    defaultVersion: url.searchParams.get("version") ?? "v2",
    message: message ? decodeURIComponent(message) : undefined,
  });
}

const schema = z.object({
  projectName: z.string().min(3, "Project name must have at least 3 characters").max(50),
  projectVersion: z.enum(["v2", "v3"]),
  workingOn: z.string().optional(),
  workingOnOther: z.string().optional(),
  technologies: z.string().optional(),
  technologiesOther: z.string().optional(),
  goals: z.string().optional(),
  goalsOther: z.string().optional(),
  workingOnPositions: z.string().optional(),
  goalsPositions: z.string().optional(),
});

export const action: ActionFunction = async ({ request, params }) => {
  const userId = await requireUserId(request);
  const { organizationSlug } = params;
  invariant(organizationSlug, "organizationSlug is required");

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

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

  const url = new URL(request.url);
  const code = url.searchParams.get("code");
  const configurationId = url.searchParams.get("configurationId");
  const next = url.searchParams.get("next");

  const stringArraySchema = z.array(z.string());

  function safeParseStringArray(value: string | undefined): string[] | undefined {
    if (!value) return undefined;
    try {
      const result = stringArraySchema.safeParse(JSON.parse(value));
      return result.success && result.data.length > 0 ? result.data : undefined;
    } catch {
      return undefined;
    }
  }

  const numberArraySchema = z.array(z.number());
  function safeParseNumberArray(value: string | undefined): number[] | undefined {
    if (!value) return undefined;
    try {
      const result = numberArraySchema.safeParse(JSON.parse(value));
      return result.success && result.data.length > 0 ? result.data : undefined;
    } catch {
      return undefined;
    }
  }

  const onboardingData: Record<string, Prisma.InputJsonValue> = {};

  const workingOn = safeParseStringArray(submission.value.workingOn);
  if (workingOn) {
    onboardingData.workingOn = workingOn;
    const workingOnPositions = safeParseNumberArray(submission.value.workingOnPositions);
    if (workingOnPositions) onboardingData.workingOnPositions = workingOnPositions;
  }

  if (submission.value.workingOnOther) {
    onboardingData.workingOnOther = submission.value.workingOnOther;
  }

  const technologies = safeParseStringArray(submission.value.technologies);
  if (technologies) onboardingData.technologies = technologies;

  const technologiesOther = safeParseStringArray(submission.value.technologiesOther);
  if (technologiesOther) onboardingData.technologiesOther = technologiesOther;

  const goals = safeParseStringArray(submission.value.goals);
  if (goals) {
    onboardingData.goals = goals;
    const goalsPositions = safeParseNumberArray(submission.value.goalsPositions);
    if (goalsPositions) onboardingData.goalsPositions = goalsPositions;
  }

  if (submission.value.goalsOther) {
    onboardingData.goalsOther = submission.value.goalsOther;
  }

  try {
    const project = await createProject({
      organizationSlug: organizationSlug,
      name: submission.value.projectName,
      userId,
      version: submission.value.projectVersion,
      onboardingData: Object.keys(onboardingData).length > 0 ? onboardingData : undefined,
    });

    if (code && configurationId) {
      const environment = await prisma.runtimeEnvironment.findFirst({
        where: {
          projectId: project.id,
          slug: "prod",
          archivedAt: null,
        },
      });

      if (!environment) {
        return redirectWithErrorMessage(
          newProjectPath({ slug: organizationSlug }),
          request,
          "Failed to find project environment."
        );
      }

      const state = await generateVercelOAuthState({
        organizationId: project.organization.id,
        projectId: project.id,
        environmentSlug: environment.slug,
        organizationSlug: project.organization.slug,
        projectSlug: project.slug,
      });

      const params = new URLSearchParams({
        state,
        code,
        configurationId,
        origin: "marketplace",
      });
      if (next) {
        params.set("next", next);
      }
      return redirect(`/vercel/connect?${params.toString()}`);
    }

    return redirectWithSuccessMessage(
      v3ProjectPath(project.organization, project),
      request,
      `${submission.value.projectName} created`
    );
  } catch (error) {
    if (error instanceof ExceededProjectLimitError) {
      return redirectWithErrorMessage(
        newProjectPath({ slug: organizationSlug }),
        request,
        error.message,
        {
          title: "Failed to create project",
          action: {
            label: "Request more projects",
            variant: "secondary/small",
            action: { type: "help", feedbackType: "help" },
          },
        }
      );
    }

    return redirectWithErrorMessage(
      newProjectPath({ slug: organizationSlug }),
      request,
      error instanceof Error ? error.message : "Something went wrong",
      { ephemeral: false }
    );
  }
};

export default function Page() {
  const { organization, message } = useTypedLoaderData<typeof loader>();
  const lastSubmission = useActionData();

  const canCreateV3Projects = organization.isActivated;

  const [form, { projectName, projectVersion }] = useForm({
    id: "create-project",
    lastResult: lastSubmission as any,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema });
    },
  });

  const navigation = useNavigation();
  const isLoading = navigation.state === "submitting" || navigation.state === "loading";

  const [selectedWorkingOn, setSelectedWorkingOn] = useState<string[]>([]);
  const [workingOnOther, setWorkingOnOther] = useState("");
  const [selectedTechnologies, setSelectedTechnologies] = useState<string[]>([]);
  const [customTechnologies, setCustomTechnologies] = useState<string[]>([]);
  const [selectedGoals, setSelectedGoals] = useState<string[]>([]);
  const [goalsOther, setGoalsOther] = useState("");

  const [shuffledWorkingOn, setShuffledWorkingOn] = useState<string[]>([...workingOnOptions]);
  const [shuffledGoals, setShuffledGoals] = useState<string[]>([...goalOptions]);

  useEffect(() => {
    const nonOther = workingOnOptions.filter((o) => o !== WORKING_ON_OTHER);
    setShuffledWorkingOn([...shuffleArray(nonOther), WORKING_ON_OTHER]);

    const nonOtherGoals = goalOptions.filter((o) => o !== GOALS_OTHER);
    setShuffledGoals([...shuffleArray(nonOtherGoals), GOALS_OTHER]);
  }, []);

  const showWorkingOnOther = selectedWorkingOn.includes(WORKING_ON_OTHER);
  const showGoalsOther = selectedGoals.includes(GOALS_OTHER);

  return (
    <AppContainer className="bg-background-deep">
      <BackgroundWrapper>
        <MainCenteredContainer
          variant="onboarding"
          className="max-w-116 rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg"
        >
          <div>
            <FormTitle
              LeadingIcon={<FolderIcon className="size-7 text-indigo-500" />}
              title="Create a new project"
              description={`This will create a new project in your "${organization.title}" organization.`}
            />
            <Form method="post" {...getFormProps(form)}>
              {message && (
                <Callout variant="success" className="mb-4">
                  {message}
                </Callout>
              )}
              <Fieldset>
                <InputGroup>
                  <Label htmlFor={projectName.id}>
                    Project name <span className="text-text-bright">*</span>
                  </Label>
                  <Input
                    {...getInputProps(projectName, { type: "text" })}
                    placeholder="Your project name"
                    icon={FolderIcon}
                    autoFocus
                  />
                  <FormError id={projectName.errorId}>{projectName.errors}</FormError>
                </InputGroup>
                {canCreateV3Projects ? (
                  <input
                    {...getInputProps(projectVersion, { type: "hidden" })}
                    defaultValue={"v3"}
                  />
                ) : (
                  <input
                    {...getInputProps(projectVersion, { type: "hidden" })}
                    defaultValue={"v2"}
                  />
                )}

                <div className="border-t border-grid-bright" />
                <InputGroup>
                  <Label>What are you working on?</Label>
                  <input type="hidden" name="workingOn" value={JSON.stringify(selectedWorkingOn)} />
                  <input
                    type="hidden"
                    name="workingOnPositions"
                    value={JSON.stringify(
                      selectedWorkingOn.map((v) => shuffledWorkingOn.indexOf(v) + 1)
                    )}
                  />
                  <MultiSelectField
                    value={selectedWorkingOn}
                    setValue={setSelectedWorkingOn}
                    items={shuffledWorkingOn}
                    icon={<CommandLineIcon className="mr-1 size-4 text-text-dimmed" />}
                  />
                  {showWorkingOnOther && (
                    <>
                      <input type="hidden" name="workingOnOther" value={workingOnOther} />
                      <Input
                        type="text"
                        variant="small"
                        value={workingOnOther}
                        onChange={(e) => setWorkingOnOther(e.target.value)}
                        placeholder="Tell us what you're working on"
                        spellCheck={false}
                        containerClassName="h-8"
                      />
                    </>
                  )}
                </InputGroup>

                <InputGroup>
                  <Label>What technologies are you using?</Label>
                  <input
                    type="hidden"
                    name="technologies"
                    value={JSON.stringify(selectedTechnologies)}
                  />
                  <input
                    type="hidden"
                    name="technologiesOther"
                    value={JSON.stringify(customTechnologies)}
                  />
                  <TechnologyPicker
                    value={selectedTechnologies}
                    onChange={setSelectedTechnologies}
                    customValues={customTechnologies}
                    onCustomValuesChange={setCustomTechnologies}
                  />
                </InputGroup>

                <InputGroup>
                  <Label>What are you trying to do with Trigger.dev?</Label>
                  <input type="hidden" name="goals" value={JSON.stringify(selectedGoals)} />
                  <input
                    type="hidden"
                    name="goalsPositions"
                    value={JSON.stringify(selectedGoals.map((v) => shuffledGoals.indexOf(v) + 1))}
                  />
                  <MultiSelectField
                    value={selectedGoals}
                    setValue={setSelectedGoals}
                    items={shuffledGoals}
                    icon={<CommandLineIcon className="mr-1 size-4 text-text-dimmed" />}
                  />
                  {showGoalsOther && (
                    <>
                      <input type="hidden" name="goalsOther" value={goalsOther} />
                      <Input
                        type="text"
                        variant="small"
                        value={goalsOther}
                        onChange={(e) => setGoalsOther(e.target.value)}
                        placeholder="Tell us what you're trying to do with Trigger.dev"
                        spellCheck={false}
                        containerClassName="h-8"
                      />
                    </>
                  )}
                </InputGroup>

                <FormButtons
                  confirmButton={
                    <Button type="submit" variant={"primary/small"} isLoading={isLoading}>
                      Create
                    </Button>
                  }
                  cancelButton={
                    organization.projectsCount > 0 ? (
                      <LinkButton to={organizationPath(organization)} variant={"secondary/small"}>
                        Cancel
                      </LinkButton>
                    ) : undefined
                  }
                />
              </Fieldset>
            </Form>
          </div>
          <Feedback button={<></>} />
        </MainCenteredContainer>
      </BackgroundWrapper>
    </AppContainer>
  );
}
