import { getFormProps, getInputProps, useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { EnvelopeIcon, NoSymbolIcon, UserPlusIcon } from "@heroicons/react/20/solid";
import { DialogClose } from "@radix-ui/react-dialog";
import {
  Form,
  type MetaFunction,
  useActionData,
  useFetcher,
  useNavigation,
} from "@remix-run/react";
import { json } from "@remix-run/server-runtime";
import { tryCatch } from "@trigger.dev/core/utils";
import { cloneElement, useEffect, useRef, useState } from "react";
import { type UseDataFunctionReturn, typedjson, useTypedLoaderData } from "remix-typedjson";
import invariant from "tiny-invariant";
import { z } from "zod";
import { Feedback } from "~/components/Feedback";
import { UserAvatar } from "~/components/UserProfilePhoto";
import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import {
  Alert,
  AlertCancel,
  AlertContent,
  AlertDescription,
  AlertFooter,
  AlertHeader,
  AlertTitle,
  AlertTrigger,
} from "~/components/primitives/Alert";
import { Button, ButtonContent, LinkButton } from "~/components/primitives/Buttons";
import { PermissionButton } from "~/components/primitives/PermissionButton";
import { DateTime } from "~/components/primitives/DateTime";
import { Dialog, DialogContent, DialogHeader, DialogTrigger } from "~/components/primitives/Dialog";
import { Fieldset } from "~/components/primitives/Fieldset";
import { FormButtons } from "~/components/primitives/FormButtons";
import { FormError } from "~/components/primitives/FormError";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { InputGroup } from "~/components/primitives/InputGroup";
import { InputNumberStepper } from "~/components/primitives/InputNumberStepper";
import { Label } from "~/components/primitives/Label";
import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader";
import { Paragraph } from "~/components/primitives/Paragraph";
import * as Property from "~/components/primitives/PropertyTable";
import { Select, SelectItem, SelectLinkItem } from "~/components/primitives/Select";
import { SpinnerWhite } from "~/components/primitives/Spinner";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { $replica, prisma } from "~/db.server";
import { useShowSelfServe } from "~/hooks/useShowSelfServe";
import { useOrganization } from "~/hooks/useOrganizations";
import { useUser } from "~/hooks/useUser";
import { removeTeamMember } from "~/models/removeTeamMember.server";
import { redirectWithSuccessMessage } from "~/models/message.server";
import { resolveOrgIdFromSlug } from "~/models/organization.server";
import { TeamPresenter } from "~/presenters/TeamPresenter.server";
import { getCurrentPlan, getSelfServePurchaseBlockReason } from "~/services/platform.v3.server";
import { rbac } from "~/services/rbac.server";
import { ssoController } from "~/services/sso.server";
import { dashboardAction, dashboardLoader } from "~/services/routeBuilders/dashboardBuilder";
import { cn } from "~/utils/cn";
import { formatCurrency, formatNumber } from "~/utils/numberFormatter";
import {
  inviteTeamMemberPath,
  organizationRolesPath,
  organizationTeamPath,
  resendInvitePath,
  revokeInvitePath,
  v3BillingPath,
} from "~/utils/pathBuilder";
import { SetSeatsAddOnService } from "~/v3/services/setSeatsAddOn.server";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";

export const meta: MetaFunction = () => {
  return [
    {
      title: `Team | Trigger.dev`,
    },
  ];
};

const Params = z.object({
  organizationSlug: z.string(),
});

export const loader = dashboardLoader(
  {
    params: Params,
    context: async (params) => {
      const orgId = await resolveOrgIdFromSlug(params.organizationSlug);
      return orgId ? { organizationId: orgId } : {};
    },
    authorization: { action: "read", resource: { type: "members" } },
  },
  async ({ user, ability, context }) => {
    const orgId = context.organizationId;
    if (!orgId) {
      throw new Response("Not Found", { status: 404 });
    }

    const presenter = new TeamPresenter();
    const result = await presenter.call({
      userId: user.id,
      organizationId: orgId,
    });

    if (!result) {
      throw new Response("Not Found", { status: 404 });
    }

    // Pre-compute manage authority server-side so the UI gating matches
    // the action gating (the action enforces it independently). Seat
    // purchases are a billing operation, so they gate on manage:billing.
    const canManageMembers = ability.can("manage", { type: "members" });
    const canManageBilling = ability.can("manage", { type: "billing" });

    // When Directory Sync is the authority (allowManualMembership off + a
    // directory active), manual invite/remove/leave are disabled. Fail-open to
    // "allowed" so a plugin hiccup never strands the team page.
    const policy = await ssoController.getMembershipPolicy(orgId);
    const manualMembershipAllowed = policy.isOk() ? policy.value.manualMembershipAllowed : true;

    return typedjson({ ...result, canManageMembers, canManageBilling, manualMembershipAllowed });
  }
);

const schema = z.object({
  memberId: z.string(),
});

const PurchaseSchema = z.discriminatedUnion("action", [
  z.object({
    action: z.literal("purchase"),
    amount: z.coerce.number().int("Must be a whole number").min(0, "Amount must be 0 or more"),
  }),
  z.object({
    action: z.literal("quota-increase"),
    amount: z.coerce.number().int("Must be a whole number").min(1, "Amount must be greater than 0"),
  }),
]);

const SetRoleSchema = z.object({
  userId: z.string(),
  roleId: z.string(),
});

export const action = dashboardAction(
  {
    params: Params,
    context: async (params) => {
      const orgId = await resolveOrgIdFromSlug(params.organizationSlug);
      return orgId ? { organizationId: orgId } : {};
    },
    // No top-level authorization — different intents have different
    // requirements. Each branch inside checks the right ability:
    //   set-role        → manage:members
    //   purchase-seats  → manage:billing
    //   remove-member   → manage:members (skipped for self-leave)
    // Don't rely on the model-layer (removeTeamMember /
    // SetSeatsAddOnService) for enforcement — those are defense in
    // depth; the route layer is where the ability gate belongs.
  },
  async ({ user, ability, request, params, context }) => {
    const userId = user.id;
    const { organizationSlug } = params;
    invariant(organizationSlug, "organizationSlug not found");

    const formData = await request.formData();
    const formType = formData.get("_formType");

    if (formType === "set-role") {
      if (!ability.can("manage", { type: "members" })) {
        return json({ ok: false, error: "Unauthorized" } as const, { status: 403 });
      }
      const orgId = context.organizationId;
      if (!orgId) {
        return json({ ok: false, error: "Organization not found" } as const, { status: 404 });
      }
      const submission = parseWithZod(formData, { schema: SetRoleSchema });
      if (submission.status !== "success") {
        return json(submission.reply());
      }
      const result = await rbac.setUserRole({
        userId: submission.value.userId,
        organizationId: orgId,
        roleId: submission.value.roleId,
      });
      if (!result.ok) {
        return json({ ok: false, error: result.error } as const, { status: 400 });
      }
      return json({ ok: true } as const);
    }

    if (formType === "purchase-seats") {
      // Adjusting seat count is a billing operation. Pre-RBAC the team
      // page's loader gated the entire route on Owner/Admin, so reaching
      // this action implied authority. Post-RBAC the loader requires
      // `read:members` (broader audience), so gate the seat purchase
      // explicitly here against the right ability rather than relying
      // on the SetSeatsAddOnService for enforcement at the model layer.
      if (!ability.can("manage", { type: "billing" })) {
        return json({ ok: false, error: "Unauthorized" } as const, { status: 403 });
      }
      // Reuse the orgId the dashboardBuilder already resolved in the
      // context callback (single slug → orgId lookup per request,
      // regardless of whether the OSS fallback or cloud plugin
      // services the auth — the plugin takes `organizationId` as
      // input and doesn't re-resolve from a slug).
      const orgId = context.organizationId;
      if (!orgId) {
        return json({ ok: false, error: "Organization not found" } as const);
      }

      const currentPlan = await getCurrentPlan(orgId);
      const purchaseBlockReason = getSelfServePurchaseBlockReason(currentPlan);
      if (purchaseBlockReason === "plan_unavailable") {
        return json(
          { ok: false, error: "Unable to verify billing status. Please try again." } as const,
          { status: 503 }
        );
      }
      if (purchaseBlockReason === "managed_billing") {
        return json({ ok: false, error: "Contact us to request more seats." } as const, {
          status: 403,
        });
      }

      const submission = parseWithZod(formData, { schema: PurchaseSchema });

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

      const service = new SetSeatsAddOnService();
      const [error, result] = await tryCatch(
        service.call({
          userId,
          organizationId: orgId,
          action: submission.value.action,
          amount: submission.value.amount,
        })
      );

      if (error) {
        return json(
          submission.reply({
            fieldErrors: { amount: [error instanceof Error ? error.message : "Unknown error"] },
          })
        );
      }

      if (!result.success) {
        return json(submission.reply({ fieldErrors: { amount: [result.error] } }));
      }

      return json({ ok: true } as const);
    }

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

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

    // Default intent: remove a member or leave the org, with the target scoped
    // to the actor's organization. Self-leave is always allowed; removing
    // someone else requires manage:members.
    const orgId = context.organizationId;
    if (!orgId) {
      return json({ ok: false, error: "Organization not found" } as const, { status: 404 });
    }
    const targetMember = await $replica.orgMember.findFirst({
      where: { id: submission.value.memberId, organizationId: orgId },
      select: { userId: true },
    });
    if (!targetMember) {
      return json({ ok: false, error: "Member not found" } as const, { status: 404 });
    }
    const isSelfLeave = targetMember.userId === userId;
    if (!isSelfLeave && !ability.can("manage", { type: "members" })) {
      return json({ ok: false, error: "Unauthorized" } as const, { status: 403 });
    }

    // Directory-managed membership: manual removal + self-leave are disabled
    // (membership is driven by the directory). Enforced here, not just in the
    // UI. Fail-open on a plugin error.
    const removePolicy = await ssoController.getMembershipPolicy(orgId);
    if (removePolicy.isOk() && !removePolicy.value.manualMembershipAllowed) {
      return json({ ok: false, error: "Membership is managed by Directory Sync" } as const, {
        status: 403,
      });
    }

    try {
      const deletedMember = await removeTeamMember(
        {
          userId,
          memberId: submission.value.memberId,
          slug: organizationSlug,
        },
        prisma
      );

      // Sticky removal: record a tombstone so passive SSO-JIT won't re-add
      // them on next login (best-effort; no-op without the SSO plugin).
      await ssoController
        .recordMembershipRemoval({
          organizationId: orgId,
          userId: deletedMember.userId,
          reason: isSelfLeave ? "self_leave" : "manual_removal",
        })
        .unwrapOr(undefined);

      if (deletedMember.userId === userId) {
        return redirectWithSuccessMessage("/", request, `You left the organization`);
      }

      return redirectWithSuccessMessage(
        organizationTeamPath(deletedMember.organization),
        request,
        `Removed ${deletedMember.user.name ?? "member"} from team`
      );
    } catch (error: any) {
      return json({ errors: { body: error.message } }, { status: 400 });
    }
  }
);

type Member = UseDataFunctionReturn<typeof loader>["members"][number];
type Invite = UseDataFunctionReturn<typeof loader>["invites"][number];
type Role = UseDataFunctionReturn<typeof loader>["roles"][number];

export default function Page() {
  const {
    members,
    invites,
    limits,
    canPurchaseSeats,
    extraSeats,
    seatPricing,
    maxSeatQuota,
    planSeatLimit,
    roles,
    assignableRoleIds,
    memberRoles,
    canManageMembers,
    canManageBilling,
    manualMembershipAllowed,
  } = useTypedLoaderData<typeof loader>();
  // Build a userId → roleId map so the dropdown's defaultValue matches
  // each member's current assignment without re-querying.
  const memberRoleByUserId = new Map<string, string>(
    memberRoles.flatMap((m) => (m.role ? [[m.userId, m.role.id]] : []))
  );
  const user = useUser();
  const organization = useOrganization();

  const plan = useCurrentPlan();
  const showSelfServe = useShowSelfServe();
  const requiresUpgrade = limits.used >= limits.limit;
  const usageRatio = limits.limit > 0 ? Math.min(limits.used / limits.limit, 1) : 0;
  const canUpgrade =
    plan?.v3Subscription?.plan && !plan.v3Subscription.plan.limits.teamMembers.canExceed;

  return (
    <PageContainer>
      <NavBar>
        <PageTitle title="Team" />

        <PageAccessories>
          <AdminDebugTooltip>
            <Property.Table>
              <Property.Item>
                <Property.Label>Org ID</Property.Label>
                <Property.Value>{organization.id}</Property.Value>
              </Property.Item>

              {members.map((member) => (
                <Property.Item key={member.id}>
                  <Property.Label>{member.user.name}</Property.Label>
                  <Property.Value>
                    <div className="flex items-center gap-2">
                      <Paragraph variant="extra-small/bright/mono">
                        {member.user.email} - {member.user.id}
                      </Paragraph>
                    </div>
                  </Property.Value>
                </Property.Item>
              ))}
            </Property.Table>
          </AdminDebugTooltip>
          {!manualMembershipAllowed ? (
            // Directory Sync is the membership authority — manual invites are
            // disabled. The invite action + acceptInvite enforce this too.
            <SimpleTooltip
              button={
                <ButtonContent
                  variant="primary/small"
                  LeadingIcon={UserPlusIcon}
                  className="cursor-not-allowed opacity-50"
                >
                  Invite a team member
                </ButtonContent>
              }
              content="Membership is managed by Directory Sync"
              disableHoverableContent
            />
          ) : !canManageMembers ? (
            // Gate the invite affordance on manage:members. The action
            // route enforces this independently — hiding it here just
            // avoids dead UI for non-managers.
            <SimpleTooltip
              button={
                <ButtonContent
                  variant="primary/small"
                  LeadingIcon={UserPlusIcon}
                  className="cursor-not-allowed opacity-50"
                >
                  Invite a team member
                </ButtonContent>
              }
              content="You don't have permission to invite team members"
              disableHoverableContent
            />
          ) : requiresUpgrade ? (
            <SimpleTooltip
              button={
                <ButtonContent
                  variant="primary/small"
                  LeadingIcon={UserPlusIcon}
                  className="cursor-not-allowed opacity-50"
                >
                  Invite a team member
                </ButtonContent>
              }
              content="Purchase more seats to invite more team members"
              disableHoverableContent
            />
          ) : (
            <LinkButton
              to={inviteTeamMemberPath(organization)}
              variant="primary/small"
              LeadingIcon={UserPlusIcon}
            >
              Invite a team member
            </LinkButton>
          )}
        </PageAccessories>
      </NavBar>
      <PageBody scrollable={false}>
        <div className="grid max-h-full min-h-full grid-rows-[1fr_auto]">
          <div className="overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control">
            <div className="mx-auto max-w-3xl px-4 pb-4 pt-20">
              {invites.length > 0 && (
                <>
                  <Header2 className="mb-3 mt-4">Pending invites</Header2>
                  <ul className="divide-ui-border mb-6 flex w-full flex-col divide-y border-y">
                    {invites.map((invite) => (
                      <li key={invite.id} className="flex items-center gap-4 py-4">
                        <div className="rounded-md border border-grid-dimmed bg-background-bright p-1.5">
                          <EnvelopeIcon className="size-7 text-text-dimmed" />
                        </div>
                        <div className="flex flex-col gap-0.5">
                          <Header3>{invite.email}</Header3>
                          <Paragraph variant="small">
                            Invite sent {<DateTime date={invite.updatedAt} />}
                          </Paragraph>
                        </div>
                        <div className="flex grow items-center justify-end gap-x-2">
                          <ResendButton invite={invite} canManageMembers={canManageMembers} />
                          <RevokeButton invite={invite} canManageMembers={canManageMembers} />
                        </div>
                      </li>
                    ))}
                  </ul>
                </>
              )}
              <div className="mt-4 flex items-baseline justify-between">
                <Header2>Active team members</Header2>
                {roles.length > 0 ? (
                  <a
                    className="text-xs text-text-link hover:underline"
                    href={organizationRolesPath(organization)}
                  >
                    View all role permissions →
                  </a>
                ) : null}
              </div>
              <div className="mb-8 mt-3 grid w-full grid-cols-[1fr_auto_auto] items-center gap-x-2 border-y border-grid-bright">
                {members.map((member) => (
                  <div
                    key={member.user.id}
                    className="col-span-3 grid grid-cols-subgrid items-center gap-x-2 border-b border-grid-bright py-2 last:border-b-0"
                  >
                    <div className="flex items-center gap-x-2">
                      <UserAvatar
                        avatarUrl={member.user.avatarUrl}
                        name={member.user.name}
                        className="size-10"
                      />
                      <div className="flex flex-col gap-0.5">
                        <Header3>
                          {member.user.name}{" "}
                          {member.user.id === user.id && (
                            <span className="text-text-dimmed">(You)</span>
                          )}
                        </Header3>
                        <Paragraph variant="small">{member.user.email}</Paragraph>
                      </div>
                    </div>
                    <RolePicker
                      memberUserId={member.user.id}
                      currentRoleId={memberRoleByUserId.get(member.user.id) ?? null}
                      roles={roles}
                      assignableRoleIds={assignableRoleIds}
                      canManageMembers={canManageMembers}
                    />
                    <div className="justify-self-end">
                      <LeaveRemoveButton
                        userId={user.id}
                        member={member}
                        memberCount={members.length}
                        canManageMembers={canManageMembers}
                        manualMembershipAllowed={manualMembershipAllowed}
                      />
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </div>

          <div className="flex h-fit w-full items-center gap-3 border-t border-grid-bright bg-background-bright p-[0.86rem] pl-4">
            <SimpleTooltip
              button={
                <div className="size-6">
                  <svg className="h-full w-full -rotate-90 overflow-visible">
                    <circle
                      className="fill-none stroke-grid-bright"
                      strokeWidth="4"
                      r="10"
                      cx="12"
                      cy="12"
                    />
                    <circle
                      className={`fill-none ${requiresUpgrade ? "stroke-error" : "stroke-success"}`}
                      strokeWidth="4"
                      r="10"
                      cx="12"
                      cy="12"
                      strokeDasharray={`${usageRatio * 62.8} 62.8`}
                      strokeDashoffset="0"
                      strokeLinecap="round"
                    />
                  </svg>
                </div>
              }
              content={`${Math.round(usageRatio * 100)}%`}
            />
            <div className="flex w-full items-center justify-between gap-6">
              {requiresUpgrade ? (
                <Header3 className="text-error">
                  You've used all {limits.limit} of your seats.{" "}
                  {canPurchaseSeats
                    ? "Purchase more seats to invite more team members."
                    : "Upgrade your plan to invite more team members."}
                </Header3>
              ) : (
                <Header3>
                  You've used {limits.used}/{limits.limit} of your seats
                </Header3>
              )}
              {canPurchaseSeats && seatPricing ? (
                <PurchaseSeatsModal
                  seatPricing={seatPricing}
                  extraSeats={extraSeats}
                  usedSeats={limits.used}
                  maxQuota={maxSeatQuota}
                  planSeatLimit={planSeatLimit}
                  canManageBilling={canManageBilling}
                />
              ) : canUpgrade ? (
                showSelfServe ? (
                  <LinkButton to={v3BillingPath(organization)} variant="primary/small">
                    Upgrade
                  </LinkButton>
                ) : (
                  <Feedback
                    defaultValue="enterprise"
                    button={<Button variant="secondary/small">Request more</Button>}
                  />
                )
              ) : null}
            </div>
          </div>
        </div>
      </PageBody>
    </PageContainer>
  );
}

function LeaveRemoveButton({
  userId,
  member,
  memberCount,
  canManageMembers,
  manualMembershipAllowed,
}: {
  userId: string;
  member: Member;
  memberCount: number;
  canManageMembers: boolean;
  manualMembershipAllowed: boolean;
}) {
  const organization = useOrganization();

  // Directory-managed membership: neither removing others nor leaving is
  // allowed — the directory drives membership. Enforced server-side too.
  if (!manualMembershipAllowed) {
    const isSelf = userId === member.user.id;
    return (
      <SimpleTooltip
        button={
          <ButtonContent
            variant={isSelf ? "minimal/small" : "secondary/small"}
            className="cursor-not-allowed opacity-50"
          >
            {isSelf ? "Leave team" : "Remove from team"}
          </ButtonContent>
        }
        disableHoverableContent
        content="Membership is managed by Directory Sync"
      />
    );
  }

  if (userId === member.user.id) {
    if (memberCount === 1) {
      return (
        <SimpleTooltip
          button={
            <ButtonContent variant="minimal/small" className="cursor-not-allowed">
              Leave team
            </ButtonContent>
          }
          disableHoverableContent
          content="An organization requires at least 1 team member"
        />
      );
    }

    //you leave the team — leaving is always permitted regardless of
    //manage:members; non-managers can still leave on their own.
    return (
      <LeaveTeamModal
        member={member}
        buttonText="Leave team"
        title="Are you sure you want to leave the team?"
        description={`You will no longer have access to ${organization.title}. To regain access, you will need to be invited again.`}
        actionText="Leave team"
      />
    );
  }

  //you remove another member — requires manage:members
  if (!canManageMembers) {
    return (
      <SimpleTooltip
        button={
          <ButtonContent variant="secondary/small" className="cursor-not-allowed opacity-50">
            Remove from team
          </ButtonContent>
        }
        disableHoverableContent
        content="You don't have permission to remove team members"
      />
    );
  }
  return (
    <LeaveTeamModal
      member={member}
      buttonText="Remove from team"
      title={`Are you sure you want to remove ${member.user.name ?? "them"} from the team?`}
      description={`They will no longer have access to ${organization.title}. To regain access, you will need to invite them again.`}
      actionText="Remove from team"
    />
  );
}

// Inline role picker — submits a `_formType=set-role` form via fetcher
// so the change persists without a full page reload. Disabled options
// (and the picker itself) reflect plan gating + manage:members; the
// server's setUserRole enforces both checks again as the source of
// truth, so this is a UI-affordance layer only.
function RolePicker({
  memberUserId,
  currentRoleId,
  roles,
  assignableRoleIds,
  canManageMembers,
}: {
  memberUserId: string;
  currentRoleId: string | null;
  roles: Role[];
  assignableRoleIds: string[];
  canManageMembers: boolean;
}) {
  const organization = useOrganization();
  const fetcher = useFetcher<{ ok: boolean; error?: string } | { ok: true }>();
  const assignable = new Set(assignableRoleIds);
  // With no RBAC plugin installed, the loader returns no roles —
  // render nothing rather than an empty dropdown.
  if (roles.length === 0) return null;

  const isSubmitting = fetcher.state === "submitting";
  const error =
    fetcher.data && "error" in fetcher.data && fetcher.data.error ? fetcher.data.error : null;

  const picker = (
    <Select
      // Controlled — keeps the dropdown in sync with the persisted
      // role even after a failed set-role fetcher submit (the server
      // kept the old role; without `value` the UI would show the
      // attempted change).
      value={currentRoleId ?? ""}
      items={roles}
      variant="tertiary/small"
      disabled={!canManageMembers || isSubmitting}
      dropdownIcon
      text={(v) => roles.find((r) => r.id === v)?.name ?? "No role"}
      setValue={(next) => {
        if (typeof next !== "string" || next === (currentRoleId ?? "")) return;
        // Upgrade-link rows have a value too (Ariakit needs one to
        // make the row interactive — without it the Link inside
        // doesn't even register the click), but they shouldn't
        // submit the role-change form. The Link navigates the user
        // to the plan-selection page; we just bail here.
        if (!assignable.has(next)) return;
        fetcher.submit(
          { _formType: "set-role", userId: memberUserId, roleId: next },
          { method: "post" }
        );
      }}
    >
      {(items) =>
        items.map((role) => {
          const isAssignable = assignable.has(role.id);
          return isAssignable ? (
            <SelectItem key={role.id} value={role.id}>
              {role.name}
            </SelectItem>
          ) : (
            <SelectLinkItem key={role.id} value={role.id} to={v3BillingPath(organization)}>
              {role.name} (upgrade)
            </SelectLinkItem>
          );
        })
      }
    </Select>
  );

  return (
    <div className="flex flex-col items-end gap-1">
      {canManageMembers ? (
        picker
      ) : (
        // Disabled <Select> swallows hover events on its own, so wrap it
        // in a div the tooltip can attach to.
        <SimpleTooltip
          button={<div className="cursor-not-allowed">{picker}</div>}
          content="You don't have permission to change roles"
          disableHoverableContent
        />
      )}
      {error ? (
        <span className="text-xs text-error" role="alert">
          {error}
        </span>
      ) : null}
    </div>
  );
}

function LeaveTeamModal({
  member,
  buttonText,
  title,
  description,
  actionText,
}: {
  member: Member;
  buttonText: string;
  title: string;
  description: string;
  actionText: string;
}) {
  const [open, setOpen] = useState(false);
  const lastSubmission = useActionData();

  const [form, _fields] = useForm({
    id: "remove-member",
    // TODO: type this
    lastResult: lastSubmission as any,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema });
    },
  });

  return (
    <Alert open={open} onOpenChange={(o) => setOpen(o)}>
      <AlertTrigger asChild>
        <Button variant="secondary/small">{buttonText}</Button>
      </AlertTrigger>
      <AlertContent>
        <AlertHeader>
          <AlertTitle>{title}</AlertTitle>
          <AlertDescription>{description}</AlertDescription>
        </AlertHeader>
        <AlertFooter>
          <AlertCancel asChild>
            <Button variant="secondary/small">Cancel</Button>
          </AlertCancel>
          <Form method="post" {...getFormProps(form)} onSubmit={() => setOpen(false)}>
            <input type="hidden" value={member.id} name="memberId" />
            <Button type="submit" variant="danger/small" form={form.id}>
              {actionText}
            </Button>
          </Form>
        </AlertFooter>
      </AlertContent>
    </Alert>
  );
}

const RESEND_COOLDOWN_SECONDS = 30;

function initialCooldown(updatedAt: Date | string): number {
  const elapsed = Math.floor((Date.now() - new Date(updatedAt).getTime()) / 1000);
  const remaining = RESEND_COOLDOWN_SECONDS - elapsed;
  return remaining > 0 ? remaining : 0;
}

function ResendButton({ invite, canManageMembers }: { invite: Invite; canManageMembers: boolean }) {
  const navigation = useNavigation();
  const isSubmitting =
    navigation.state === "submitting" &&
    navigation.formAction === resendInvitePath() &&
    navigation.formData?.get("inviteId") === invite.id;
  const prevSubmitting = useRef(false);
  const [cooldown, setCooldown] = useState(() => initialCooldown(invite.updatedAt));
  const intervalRef = useRef<ReturnType<typeof setInterval>>();

  useEffect(() => {
    if (prevSubmitting.current && !isSubmitting) {
      setCooldown(RESEND_COOLDOWN_SECONDS);
    }
    prevSubmitting.current = isSubmitting;
  }, [isSubmitting]);

  const cooldownActive = cooldown > 0;
  useEffect(() => {
    if (!cooldownActive) return;

    intervalRef.current = setInterval(() => {
      setCooldown((c) => {
        if (c <= 1) {
          clearInterval(intervalRef.current);
          return 0;
        }
        return c - 1;
      });
    }, 1000);

    return () => clearInterval(intervalRef.current);
  }, [cooldownActive]);

  const isDisabled = isSubmitting || cooldown > 0 || !canManageMembers;

  return (
    <Form method="post" action={resendInvitePath()} className="flex">
      <input type="hidden" value={invite.id} name="inviteId" />
      <Button
        type="submit"
        variant="secondary/small"
        disabled={isDisabled}
        tooltip={canManageMembers ? undefined : "You don't have permission to manage team members"}
      >
        {isSubmitting ? (
          "Sending…"
        ) : cooldown > 0 ? (
          <span className="tabular-nums">{`Sent – resend in ${cooldown}s`}</span>
        ) : (
          "Resend invite"
        )}
      </Button>
    </Form>
  );
}

function RevokeButton({ invite, canManageMembers }: { invite: Invite; canManageMembers: boolean }) {
  const organization = useOrganization();

  return (
    <Form method="post" action={revokeInvitePath()} className="flex">
      <input type="hidden" value={invite.id} name="inviteId" />
      <input type="hidden" value={organization.slug} name="slug" />
      <SimpleTooltip
        button={
          <Button
            type="submit"
            variant="danger/small"
            LeadingIcon={NoSymbolIcon}
            leadingIconClassName="text-white"
            aria-label="Revoke invite"
            disabled={!canManageMembers}
          />
        }
        content={
          canManageMembers ? "Revoke invite" : "You don't have permission to manage team members"
        }
        disableHoverableContent
        asChild
      />
    </Form>
  );
}

export function PurchaseSeatsModal({
  seatPricing,
  extraSeats,
  usedSeats,
  maxQuota,
  planSeatLimit,
  triggerButton,
  canManageBilling = true,
}: {
  seatPricing: {
    stepSize: number;
    centsPerStep: number;
  };
  extraSeats: number;
  usedSeats: number;
  maxQuota: number;
  planSeatLimit: number;
  triggerButton?: React.ReactElement;
  canManageBilling?: boolean;
}) {
  const showSelfServe = useShowSelfServe();
  const fetcher = useFetcher();
  const organization = useOrganization();
  const lastSubmission =
    fetcher.data && typeof fetcher.data === "object" && "status" in fetcher.data
      ? fetcher.data
      : undefined;
  const [form, fields] = useForm({
    id: "purchase-seats",
    lastResult: lastSubmission as any,
    onValidate({ formData }) {
      return parseWithZod(formData, { schema: PurchaseSchema });
    },
    shouldRevalidate: "onSubmit",
  });
  const amount = fields.amount;

  const [amountValue, setAmountValue] = useState(extraSeats);
  useEffect(() => {
    setAmountValue(extraSeats);
  }, [extraSeats]);
  const isLoading = fetcher.state !== "idle";

  const [open, setOpen] = useState(false);
  useEffect(() => {
    const data = fetcher.data;
    if (
      fetcher.state === "idle" &&
      data !== null &&
      typeof data === "object" &&
      "ok" in data &&
      data.ok
    ) {
      setOpen(false);
    }
  }, [fetcher.state, fetcher.data]);

  const state = updateSeatState({
    value: amountValue,
    existingValue: extraSeats,
    quota: maxQuota,
    usedSeats,
    planSeatLimit,
  });
  const changeClassName =
    state === "decrease" ? "text-error" : state === "increase" ? "text-success" : undefined;

  const pricePerSeat = seatPricing.centsPerStep / seatPricing.stepSize / 100;
  const title = extraSeats === 0 ? "Purchase extra seats…" : "Add/remove extra seats…";

  if (!showSelfServe) {
    return (
      <Feedback
        defaultValue="enterprise"
        button={<Button variant="secondary/small">Request more</Button>}
      />
    );
  }

  // Buying seats is a billing action — disable the trigger (and explain why)
  // when the role can't manage billing. The action enforces it independently.
  const noBillingTooltip = "You don't have permission to manage billing";
  const trigger = canManageBilling ? (
    (triggerButton ?? (
      <Button variant="primary/small" onClick={() => setOpen(true)}>
        {title}
      </Button>
    ))
  ) : triggerButton ? (
    cloneElement(triggerButton, { disabled: true, tooltip: noBillingTooltip })
  ) : (
    <PermissionButton
      variant="primary/small"
      hasPermission={false}
      noPermissionTooltip={noBillingTooltip}
    >
      {title}
    </PermissionButton>
  );

  return (
    <Dialog open={open} onOpenChange={setOpen}>
      <DialogTrigger asChild>{trigger}</DialogTrigger>
      <DialogContent>
        <DialogHeader>{title}</DialogHeader>
        <fetcher.Form
          method="post"
          action={organizationTeamPath(organization)}
          {...getFormProps(form)}
        >
          <input type="hidden" name="_formType" value="purchase-seats" />
          <div className="flex flex-col gap-4 pt-2">
            <div className="flex flex-col gap-1">
              <Paragraph variant="small/bright">
                Purchase extra seats at {formatCurrency(pricePerSeat, true)}/month per seat.
                Reducing seats will take effect at the start of your next billing cycle (on the 1st
                of the month).
              </Paragraph>
            </div>
            <Fieldset>
              <InputGroup fullWidth>
                <Label htmlFor="amount" className="text-text-dimmed">
                  Total extra seats
                </Label>
                <InputNumberStepper
                  {...getInputProps(amount, { type: "number" })}
                  step={seatPricing.stepSize}
                  min={0}
                  max={undefined}
                  value={amountValue}
                  onChange={(e) => setAmountValue(Number(e.target.value))}
                  disabled={isLoading}
                />
                <FormError id={amount.errorId}>{amount.errors}</FormError>
                <FormError>{form.errors}</FormError>
              </InputGroup>
            </Fieldset>
            {state === "need_to_remove_members" ? (
              <div className="flex flex-col pb-3">
                <Paragraph variant="small" className="text-warning" spacing>
                  You need to remove {formatNumber(usedSeats - (planSeatLimit + amountValue))}{" "}
                  {usedSeats - (planSeatLimit + amountValue) === 1
                    ? "team member or pending invite"
                    : "team members or pending invites"}{" "}
                  before you can reduce to this level.
                </Paragraph>
              </div>
            ) : state === "above_quota" ? (
              <div className="flex flex-col pb-3">
                <Paragraph variant="small" className="text-warning" spacing>
                  Currently you can only have up to {maxQuota} extra seats. Send a request below to
                  lift your current limit. We'll get back to you soon.
                </Paragraph>
              </div>
            ) : (
              <div className="flex flex-col pb-3 tabular-nums">
                <div className="grid grid-cols-2 border-b border-grid-dimmed pb-1">
                  <Header3 className="font-normal text-text-dimmed">Summary</Header3>
                  <Header3 className="justify-self-end font-normal text-text-dimmed">Total</Header3>
                </div>
                <div className="grid grid-cols-2 pt-2">
                  <Header3 className="pb-0 font-normal text-text-dimmed">
                    <span className="text-text-bright">{formatNumber(extraSeats)}</span> current
                    extra
                  </Header3>
                  <Header3 className="justify-self-end font-normal text-text-bright">
                    {formatCurrency(extraSeats * pricePerSeat, true)}
                  </Header3>
                </div>
                <div className="grid grid-cols-2 text-xs">
                  <span className="text-text-dimmed">
                    ({extraSeats} {extraSeats === 1 ? "seat" : "seats"})
                  </span>
                  <span className="justify-self-end text-text-dimmed">/mth</span>
                </div>
                <div className="grid grid-cols-2 pt-2">
                  <Header3 className={cn("pb-0 font-normal", changeClassName)}>
                    {state === "increase" ? "+" : null}
                    {formatNumber(amountValue - extraSeats)}
                  </Header3>
                  <Header3 className={cn("justify-self-end font-normal", changeClassName)}>
                    {state === "increase" ? "+" : null}
                    {formatCurrency((amountValue - extraSeats) * pricePerSeat, true)}
                  </Header3>
                </div>
                <div className="grid grid-cols-2 text-xs">
                  <span className="text-text-dimmed">
                    ({Math.abs(amountValue - extraSeats)}{" "}
                    {Math.abs(amountValue - extraSeats) === 1 ? "seat" : "seats"} @{" "}
                    {formatCurrency(pricePerSeat, true)}/mth)
                  </span>
                  <span className="justify-self-end text-text-dimmed">/mth</span>
                </div>
                <div className="grid grid-cols-2 pt-2">
                  <Header3 className="pb-0 font-normal text-text-dimmed">
                    <span className="text-text-bright">{formatNumber(amountValue)}</span> new total
                  </Header3>
                  <Header3 className="justify-self-end font-normal text-text-bright">
                    {formatCurrency(amountValue * pricePerSeat, true)}
                  </Header3>
                </div>
                <div className="grid grid-cols-2 text-xs">
                  <span className="text-text-dimmed">
                    ({amountValue} {amountValue === 1 ? "seat" : "seats"})
                  </span>
                  <span className="justify-self-end text-text-dimmed">/mth</span>
                </div>
              </div>
            )}
          </div>
          <FormButtons
            confirmButton={
              state === "above_quota" ? (
                <>
                  <input type="hidden" name="action" value="quota-increase" />
                  <Button
                    LeadingIcon={isLoading ? SpinnerWhite : EnvelopeIcon}
                    variant="primary/medium"
                    type="submit"
                    disabled={isLoading}
                  >
                    <span className="tabular-nums text-text-bright">{`Send request for ${formatNumber(
                      amountValue
                    )}`}</span>
                  </Button>
                </>
              ) : state === "decrease" || state === "need_to_remove_members" ? (
                <>
                  <input type="hidden" name="action" value="purchase" />
                  <Button
                    variant="danger/medium"
                    type="submit"
                    disabled={isLoading || state === "need_to_remove_members"}
                    LeadingIcon={isLoading ? SpinnerWhite : undefined}
                  >
                    <span className="tabular-nums text-text-bright">{`Remove ${formatNumber(
                      extraSeats - amountValue
                    )} ${extraSeats - amountValue === 1 ? "seat" : "seats"}`}</span>
                  </Button>
                </>
              ) : (
                <>
                  <input type="hidden" name="action" value="purchase" />
                  <Button
                    variant="primary/medium"
                    type="submit"
                    disabled={isLoading || state === "no_change"}
                    LeadingIcon={isLoading ? SpinnerWhite : undefined}
                  >
                    <span className="tabular-nums text-text-bright">{`Purchase ${formatNumber(
                      amountValue - extraSeats
                    )} ${amountValue - extraSeats === 1 ? "seat" : "seats"}`}</span>
                  </Button>
                </>
              )
            }
            cancelButton={
              <DialogClose asChild>
                <Button variant="secondary/medium" disabled={isLoading}>
                  Cancel
                </Button>
              </DialogClose>
            }
          />
        </fetcher.Form>
      </DialogContent>
    </Dialog>
  );
}

function updateSeatState({
  value,
  existingValue,
  quota,
  usedSeats,
  planSeatLimit,
}: {
  value: number;
  existingValue: number;
  quota: number;
  usedSeats: number;
  planSeatLimit: number;
}): "no_change" | "increase" | "decrease" | "above_quota" | "need_to_remove_members" {
  if (value === existingValue) return "no_change";
  if (value < existingValue) {
    const newTotalLimit = planSeatLimit + value;
    if (usedSeats > newTotalLimit) {
      return "need_to_remove_members";
    }
    return "decrease";
  }
  if (value > quota) return "above_quota";
  return "increase";
}
