import { Outlet, type ShouldRevalidateFunction, type UIMatch } from "@remix-run/react";
import type { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson } from "remix-typedjson";
import { z } from "zod";
import { RouteErrorDisplay } from "~/components/ErrorDisplay";
import { prisma } from "~/db.server";
import { useOptionalOrganization } from "~/hooks/useOrganizations";
import { useTypedMatchesData } from "~/hooks/useTypedMatchData";
import { OrganizationsPresenter } from "~/presenters/OrganizationsPresenter.server";
import { RegionsPresenter, type Region } from "~/presenters/v3/RegionsPresenter.server";
import { getImpersonationId } from "~/services/impersonation.server";
import { getCachedUsage, getBillingLimit, getCurrentPlan } from "~/services/platform.v3.server";
import { rbac } from "~/services/rbac.server";
import { canManageBillingLimits } from "~/services/routeBuilders/permissions.server";
import { requireUser } from "~/services/session.server";
import { telemetry } from "~/services/telemetry.server";
import { organizationPath } from "~/utils/pathBuilder";
import { isEnvironmentPauseResumeFormSubmission } from "../_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route";
import { isBillingLimitSettingsFormSubmission } from "../_app.orgs.$organizationSlug.settings.billing-limits/billingLimitsRevalidation";

const ParamsSchema = z.object({
  organizationSlug: z.string(),
  projectParam: z.string().optional(),
  envParam: z.string().optional(),
});

export function useCurrentPlan(matches?: UIMatch[]) {
  const data = useTypedMatchesData<typeof loader>({
    id: "routes/_app.orgs.$organizationSlug",
    matches,
  });

  return data?.currentPlan;
}

export const shouldRevalidate: ShouldRevalidateFunction = (params) => {
  const { currentParams, nextParams } = params;

  const current = ParamsSchema.safeParse(currentParams);
  const next = ParamsSchema.safeParse(nextParams);

  if (current.success && next.success) {
    if (current.data.organizationSlug !== next.data.organizationSlug) {
      return true;
    }
    if (current.data.projectParam !== next.data.projectParam) {
      return true;
    }
    if (current.data.envParam !== next.data.envParam) {
      return true;
    }
  }

  // Invalidate if the environment has been paused or resumed
  if (isEnvironmentPauseResumeFormSubmission(params.formMethod, params.formData)) {
    return true;
  }

  if (isBillingLimitSettingsFormSubmission(params.formMethod, params.formData)) {
    return true;
  }

  // This prevents revalidation when there are search params changes
  // IMPORTANT: If the loader function depends on search params, this should be updated
  return params.currentUrl.pathname !== params.nextUrl.pathname;
};

// IMPORTANT: Make sure to update shouldRevalidate if this loader depends on search params
export const loader = async ({ request, params }: LoaderFunctionArgs) => {
  const user = await requireUser(request);
  const impersonationId = await getImpersonationId(request);

  const { organizationSlug, projectParam, envParam } = ParamsSchema.parse(params);

  const orgsPresenter = new OrganizationsPresenter();
  const { organizations, organization, project, environment } = await orgsPresenter.call({
    user,
    request,
    organizationSlug,
    projectSlug: projectParam,
    environmentSlug: envParam,
  });

  telemetry.organization.identify({ organization });
  telemetry.project.identify({ project });

  //1st day of the month
  const firstDayOfMonth = new Date();
  firstDayOfMonth.setUTCDate(1);
  firstDayOfMonth.setUTCHours(0, 0, 0, 0);

  // Using the 1st day of next month means we get the usage for the current month
  // and the cache key for getCachedUsage is stable over the month
  const firstDayOfNextMonth = new Date();
  firstDayOfNextMonth.setUTCDate(1);
  firstDayOfNextMonth.setUTCHours(0, 0, 0, 0);
  firstDayOfNextMonth.setUTCMonth(firstDayOfNextMonth.getUTCMonth() + 1);

  const shouldLoadRegions = !!projectParam && !!environment && environment.type !== "DEVELOPMENT";

  const [sessionAuth, plan, usage, billingLimit, customDashboards, regions] = await Promise.all([
    rbac
      .authenticateSession(request, {
        userId: user.id,
        organizationId: organization.id,
      })
      .catch(() => ({ ok: false as const, reason: "unauthorized" as const })),
    getCurrentPlan(organization.id),
    getCachedUsage(organization.id, { from: firstDayOfMonth, to: firstDayOfNextMonth }),
    getBillingLimit(organization.id),
    prisma.metricsDashboard.findMany({
      where: { organizationId: organization.id },
      select: {
        friendlyId: true,
        title: true,
        layout: true,
      },
      orderBy: { createdAt: "desc" },
    }),
    shouldLoadRegions
      ? new RegionsPresenter()
          .call({ userId: user.id, projectSlug: projectParam! })
          .then(({ regions }) => regions)
          .catch(() => [] as Region[])
      : Promise.resolve([] as Region[]),
  ]);
  const userCanManageBillingLimits = sessionAuth.ok
    ? canManageBillingLimits(sessionAuth.ability)
    : false;

  let hasExceededFreeTier = false;
  let usagePercentage = 0;
  if (plan?.v3Subscription && !plan.v3Subscription.isPaying && plan.v3Subscription.plan && usage) {
    hasExceededFreeTier = usage.cents > plan.v3Subscription.plan.limits.includedUsage;
    usagePercentage = usage.cents / plan.v3Subscription.plan.limits.includedUsage;
  }

  // Derive metric dashboard limit from plan, fallback to 3
  const metricDashboardsLimitValue = plan?.v3Subscription?.plan?.limits?.metricDashboards;
  const dashboardLimit =
    typeof metricDashboardsLimitValue === "number"
      ? metricDashboardsLimitValue
      : (metricDashboardsLimitValue?.number ?? 3);

  // Derive widget-per-dashboard limit from plan, fallback to 16
  const metricWidgetsLimitValue = plan?.v3Subscription?.plan?.limits?.metricWidgetsPerDashboard;
  const widgetLimitPerDashboard =
    typeof metricWidgetsLimitValue === "number"
      ? metricWidgetsLimitValue
      : (metricWidgetsLimitValue?.number ?? 16);

  // Compute widget counts per dashboard from layout JSON
  const customDashboardsWithWidgetCount = customDashboards.map((d) => {
    let widgetCount = 0;
    try {
      const layout = JSON.parse(String(d.layout)) as Record<string, unknown>;
      const widgets = layout.widgets;
      if (widgets && typeof widgets === "object") {
        widgetCount = Object.keys(widgets as Record<string, unknown>).length;
      }
    } catch {
      // ignore parse errors
    }
    return {
      friendlyId: d.friendlyId,
      title: d.title,
      widgetCount,
    };
  });

  return typedjson({
    organizations,
    organization,
    project,
    environment,
    regions,
    isImpersonating: !!impersonationId,
    currentPlan: { ...plan, v3Usage: { ...usage, hasExceededFreeTier, usagePercentage } },
    billingLimit,
    customDashboards: customDashboardsWithWidgetCount,
    dashboardLimits: {
      used: customDashboards.length,
      limit: dashboardLimit,
    },
    widgetLimitPerDashboard,
    canManageBillingLimits: userCanManageBillingLimits,
  });
};

export default function Organization() {
  return <Outlet />;
}

export function ErrorBoundary() {
  const org = useOptionalOrganization();
  return org ? (
    <RouteErrorDisplay button={{ title: org.title, to: organizationPath(org) }} />
  ) : (
    <RouteErrorDisplay button={{ title: "Go to homepage", to: "/" }} />
  );
}
