import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/server-runtime";
import { json, redirect } from "@remix-run/server-runtime";
import { fromPromise } from "neverthrow";
import { useEffect, useState } from "react";
import { Form, useNavigation } from "@remix-run/react";
import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { z } from "zod";
import { BuildingOfficeIcon, FolderIcon } from "@heroicons/react/20/solid";
import { AppContainer, MainCenteredContainer } from "~/components/layout/AppLayout";
import { BackgroundWrapper } from "~/components/BackgroundWrapper";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormTitle } from "~/components/primitives/FormTitle";
import { InputGroup } from "~/components/primitives/InputGroup";
import { Label } from "~/components/primitives/Label";
import { Select, SelectItem } from "~/components/primitives/Select";
import { ButtonSpinner } from "~/components/primitives/Spinner";
import { prisma } from "~/db.server";
import { authenticator } from "~/services/auth.server";
import { logger } from "~/services/logger.server";
import { requireUserId } from "~/services/session.server";
import { ssoRedirectForEmail } from "~/services/ssoAutoDiscovery.server";
import { confirmBasicDetailsPath, newProjectPath } from "~/utils/pathBuilder";
import { redirectWithErrorMessage } from "~/models/message.server";
import { generateVercelOAuthState } from "~/v3/vercel/vercelOAuthState.server";

const LoaderParamsSchema = z.object({
  organizationId: z.string().optional().nullable(),
  code: z.string().optional().nullable(),
  configurationId: z.string().optional().nullable(),
  next: z.string().optional().nullable(),
  error: z.string().optional().nullable(),
});

const SelectOrgActionSchema = z.object({
  action: z.literal("select-org"),
  organizationId: z.string(),
  code: z.string(),
  configurationId: z.string().optional().nullable(),
  next: z.string().optional(),
});

const SelectProjectActionSchema = z.object({
  action: z.literal("select-project"),
  projectId: z.string(),
  organizationId: z.string(),
  code: z.string(),
  configurationId: z.string().optional().nullable(),
  next: z.string().optional().nullable(),
});

const ActionSchema = z.discriminatedUnion("action", [
  SelectOrgActionSchema,
  SelectProjectActionSchema,
]);

export async function loader({ request }: LoaderFunctionArgs) {
  const userId = await requireUserId(request);
  const url = new URL(request.url);

  const params = LoaderParamsSchema.safeParse({
    organizationId: url.searchParams.get("organizationId"),
    code: url.searchParams.get("code"),
    configurationId: url.searchParams.get("configurationId"),
    next: url.searchParams.get("next"),
    error: url.searchParams.get("error"),
  });

  if (!params.success) {
    logger.error("Invalid params for Vercel onboarding", { error: params.error });
    throw await redirectWithErrorMessage(
      "/",
      request,
      "Invalid installation parameters. Please try again from Vercel."
    );
  }

  const { error } = params.data;
  if (error === "expired") {
    return typedjson({
      step: "error" as const,
      error: "Your installation session has expired. Please start the installation again.",
      code: params.data.code ?? null,
      configurationId: params.data.configurationId ?? null,
      next: params.data.next ?? null,
    });
  }

  if (!params.data.code) {
    logger.error("Missing code parameter for Vercel onboarding");
    throw await redirectWithErrorMessage(
      "/",
      request,
      "Invalid installation parameters. Please try again from Vercel."
    );
  }

  const code = params.data.code;

  const organizations = await prisma.organization.findMany({
    where: {
      members: {
        some: { userId },
      },
      deletedAt: null,
    },
    select: {
      id: true,
      title: true,
      slug: true,
      projects: {
        where: {
          deletedAt: null,
        },
        select: {
          id: true,
          name: true,
          slug: true,
        },
        orderBy: {
          createdAt: "asc",
        },
      },
    },
    orderBy: {
      createdAt: "asc",
    },
  });

  // New user: no organizations
  if (organizations.length === 0) {
    const onboardingParams = new URLSearchParams();
    onboardingParams.set("code", code);
    if (params.data.configurationId) {
      onboardingParams.set("configurationId", params.data.configurationId);
    }
    onboardingParams.set("integration", "vercel");
    if (params.data.next) {
      onboardingParams.set("next", params.data.next);
    }
    throw redirect(`${confirmBasicDetailsPath()}?${onboardingParams.toString()}`);
  }

  // If organizationId is provided, show project selection
  if (params.data.organizationId) {
    const organization = organizations.find((org) => org.id === params.data.organizationId);

    if (!organization) {
      logger.error("Organization not found or access denied", {
        organizationId: params.data.organizationId,
        userId,
      });
      throw await redirectWithErrorMessage(
        "/",
        request,
        "Organization not found. Please try again."
      );
    }

    return typedjson({
      step: "project" as const,
      organization,
      organizations,
      code: code,
      configurationId: params.data.configurationId ?? null,
      next: params.data.next ?? null,
    });
  }

  return typedjson({
    step: "org" as const,
    organizations,
    code: code,
    configurationId: params.data.configurationId ?? null,
    next: params.data.next ?? null,
  });
}

export async function action({ request }: ActionFunctionArgs) {
  const userId = await requireUserId(request);
  const formData = await request.formData();

  const submission = ActionSchema.safeParse({
    action: formData.get("action"),
    organizationId: formData.get("organizationId"),
    projectId: formData.get("projectId"),
    code: formData.get("code"),
    configurationId: formData.get("configurationId"),
    next: formData.get("next"),
  });

  if (!submission.success) {
    return json({ error: "Invalid submission" }, { status: 400 });
  }

  // SSO auto-discovery: if the signed-in user's domain requires SSO, the
  // current session was established via a non-SSO method — block the
  // onboarding action and route them through the SSO flow instead.
  const sessionUser = await prisma.user.findFirst({
    where: { id: userId },
    select: { email: true },
  });
  if (sessionUser?.email) {
    // Preserve the in-progress Vercel install across the SSO handoff:
    // rebuild the onboarding URL (same shape the org-step redirect below
    // uses) and pass it as `redirectTo` so the single-use `code`,
    // `configurationId`, and `next` aren't lost when the user is bounced
    // to their identity provider.
    const resumeParams = new URLSearchParams();
    resumeParams.set("code", submission.data.code);
    if (submission.data.configurationId) {
      resumeParams.set("configurationId", submission.data.configurationId);
    }
    if (submission.data.next) {
      resumeParams.set("next", submission.data.next);
    }
    const resumeUrl = `/vercel/onboarding?${resumeParams.toString()}`;
    const ssoRedirect = await ssoRedirectForEmail(sessionUser.email, "oauth_blocked", resumeUrl);
    if (ssoRedirect) {
      // The user is already authenticated via a non-SSO method, so a plain
      // redirect to `/login/sso` would be bounced straight home by that
      // route's already-authenticated guard — silently dropping the install.
      // Destroy the current session first (mirroring the OAuth callbacks,
      // which never commit a session when SSO is required) so `/login/sso`
      // accepts them. `authenticator.logout` redirects to `ssoRedirect`
      // verbatim — unlike the `/logout` route it doesn't run the redirect
      // sanitizer, which would otherwise reject the non-navigable
      // `/login/sso` target. The resume URL rides along as `redirectTo` and
      // survives the SSO round-trip.
      return authenticator.logout(request, { redirectTo: ssoRedirect });
    }
  }

  const { code, configurationId, next } = submission.data;

  // Handle org selection
  if (submission.data.action === "select-org") {
    const { organizationId } = submission.data;

    const projectParams = new URLSearchParams();
    projectParams.set("organizationId", organizationId);
    projectParams.set("code", code);
    if (configurationId) {
      projectParams.set("configurationId", configurationId);
    }
    if (next) {
      projectParams.set("next", next);
    }

    return redirect(`/vercel/onboarding?${projectParams.toString()}`);
  }

  // Handle project selection
  const { projectId, organizationId } = submission.data;

  const project = await prisma.project.findFirst({
    where: {
      id: projectId,
      organizationId,
      deletedAt: null,
      organization: {
        members: { some: { userId } },
      },
    },
    include: {
      organization: true,
    },
  });

  if (!project) {
    logger.error("Project not found or access denied", { projectId, userId });
    return json({ error: "Project not found" }, { status: 404 });
  }

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

  if (!environment) {
    logger.error("Environment not found", { projectId: project.id });
    return json({ error: "Environment not found" }, { status: 404 });
  }

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

  if (stateResult.isErr()) {
    logger.error("Failed to generate Vercel OAuth state", { error: stateResult.error });
    return json({ error: "Failed to generate installation state" }, { status: 500 });
  }

  const params = new URLSearchParams();
  params.set("state", stateResult.value);
  params.set("code", code);
  if (configurationId) {
    params.set("configurationId", configurationId);
  }
  params.set("origin", "marketplace");
  if (next) {
    params.set("next", next);
  }

  return redirect(`/vercel/connect?${params.toString()}`, 303);
}

export default function VercelOnboardingPage() {
  const data = useTypedLoaderData<typeof loader>();
  const navigation = useNavigation();
  const isSubmitting = navigation.state === "submitting";
  const [isInstalling, setIsInstalling] = useState(false);

  // Reset isInstalling when navigation returns to idle (e.g. on error)
  useEffect(() => {
    if (navigation.state === "idle" && isInstalling) {
      setIsInstalling(false);
    }
  }, [navigation.state, isInstalling]);

  if (data.step === "error") {
    return (
      <AppContainer className="bg-background-deep">
        <BackgroundWrapper>
          <MainCenteredContainer
            variant="onboarding"
            className="max-w-104 rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg"
          >
            <FormTitle title="Installation Expired" description={data.error} />
            <Button variant="primary/medium" onClick={() => window.close()} className="w-full">
              Close
            </Button>
          </MainCenteredContainer>
        </BackgroundWrapper>
      </AppContainer>
    );
  }

  if (data.step === "org") {
    const newOrgUrl = (() => {
      const params = new URLSearchParams();
      params.set("code", data.code);
      if (data.configurationId) {
        params.set("configurationId", data.configurationId);
      }
      params.set("integration", "vercel");
      if (data.next) {
        params.set("next", data.next);
      }
      return `/orgs/new?${params.toString()}`;
    })();

    return (
      <AppContainer className="bg-background-deep">
        <BackgroundWrapper>
          <MainCenteredContainer
            variant="onboarding"
            className="max-w-104 rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg"
          >
            <FormTitle
              LeadingIcon={<BuildingOfficeIcon className="size-7 text-indigo-500" />}
              title="Select Organization"
              description="Choose which organization to install the Vercel integration into."
            />
            <Form method="post">
              <input type="hidden" name="action" value="select-org" />
              <input type="hidden" name="code" value={data.code} />
              {data.configurationId && (
                <input type="hidden" name="configurationId" value={data.configurationId} />
              )}
              {data.next && <input type="hidden" name="next" value={data.next} />}

              <Fieldset>
                <InputGroup>
                  <Label>Organization</Label>
                  <Select
                    name="organizationId"
                    placeholder="Choose an organization"
                    required
                    variant="tertiary/medium"
                    dropdownIcon
                    defaultValue={data.organizations[0]?.id}
                    text={(v) =>
                      typeof v === "string"
                        ? data.organizations.find((o) => o.id === v)?.title ||
                          "Choose an organization"
                        : "Choose an organization"
                    }
                  >
                    {data.organizations.map((org) => (
                      <SelectItem key={org.id} value={org.id}>
                        {org.title}
                      </SelectItem>
                    ))}
                  </Select>
                </InputGroup>
                <FormButtons
                  confirmButton={
                    <div className="flex items-center gap-2">
                      <LinkButton to={newOrgUrl} variant="tertiary/small">
                        + New Organization
                      </LinkButton>
                      <Button type="submit" variant="primary/small">
                        Continue
                      </Button>
                    </div>
                  }
                />
              </Fieldset>
            </Form>
          </MainCenteredContainer>
        </BackgroundWrapper>
      </AppContainer>
    );
  }

  const newProjectUrl = (() => {
    const params = new URLSearchParams();
    params.set("code", data.code);
    if (data.configurationId) {
      params.set("configurationId", data.configurationId);
    }
    params.set("integration", "vercel");
    params.set("organizationId", data.organization.id);
    if (data.next) {
      params.set("next", data.next);
    }
    return `${newProjectPath({ slug: data.organization.slug })}?${params.toString()}`;
  })();

  const isLoading = isSubmitting || isInstalling;

  return (
    <AppContainer className="bg-background-deep">
      <BackgroundWrapper>
        <MainCenteredContainer
          variant="onboarding"
          className="max-w-104 rounded-lg border border-grid-bright bg-background-dimmed p-5 shadow-lg"
        >
          <FormTitle
            LeadingIcon={<FolderIcon className="size-7 text-indigo-500" />}
            title="Select Project"
            description={`Choose which project in "${data.organization.title}" to install the Vercel integration into.`}
          />
          <Form method="post" onSubmit={() => setIsInstalling(true)}>
            <input type="hidden" name="action" value="select-project" />
            <input type="hidden" name="organizationId" value={data.organization.id} />
            <input type="hidden" name="code" value={data.code} />
            {data.configurationId && (
              <input type="hidden" name="configurationId" value={data.configurationId} />
            )}
            {data.next && <input type="hidden" name="next" value={data.next} />}

            <Fieldset>
              <InputGroup>
                <Label>Project</Label>
                <Select
                  name="projectId"
                  placeholder="Choose a project"
                  required
                  variant="tertiary/medium"
                  dropdownIcon
                  defaultValue={data.organization.projects[0]?.id}
                  text={(v) =>
                    typeof v === "string"
                      ? data.organization.projects.find((p) => p.id === v)?.name ||
                        "Choose a project"
                      : "Choose a project"
                  }
                >
                  {data.organization.projects.map((project) => (
                    <SelectItem key={project.id} value={project.id}>
                      {project.name}
                    </SelectItem>
                  ))}
                </Select>
              </InputGroup>
              <FormButtons
                confirmButton={
                  <div className="flex items-center gap-2">
                    <LinkButton to={newProjectUrl} variant="tertiary/small" disabled={isLoading}>
                      + New Project
                    </LinkButton>
                    <Button
                      type="submit"
                      variant="primary/small"
                      disabled={isLoading}
                      TrailingIcon={isLoading ? ButtonSpinner : undefined}
                    >
                      {isLoading ? "Continuing…" : "Continue"}
                    </Button>
                  </div>
                }
              />
            </Fieldset>
          </Form>
        </MainCenteredContainer>
      </BackgroundWrapper>
    </AppContainer>
  );
}
