import { type ActionFunctionArgs } from "@remix-run/node";
import { nanoid } from "nanoid";
import { z } from "zod";
import { typedjson } from "remix-typedjson";
import { prisma } from "~/db.server";
import { QueryWidgetConfig } from "~/components/metrics/QueryWidget";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import {
  DashboardLayout,
  LayoutItem,
} from "~/presenters/v3/MetricDashboardPresenter.server";
import { getCurrentPlan } from "~/services/platform.v3.server";
import { requireUserId } from "~/services/session.server";
import { EnvironmentParamSchema } from "~/utils/pathBuilder";

// Schemas for each action type
const AddWidgetSchema = z.object({
  widgetId: z.string().min(1, "Widget ID is required").nullish(),
  title: z.string().min(1, "Title is required"),
  query: z.string().default(""),
  config: z.string().transform((str, ctx) => {
    try {
      const parsed = JSON.parse(str);
      const result = QueryWidgetConfig.safeParse(parsed);
      if (!result.success) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "Invalid widget config",
        });
        return z.NEVER;
      }
      return result.data;
    } catch {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid JSON",
      });
      return z.NEVER;
    }
  }),
});

const UpdateWidgetSchema = z.object({
  widgetId: z.string().min(1, "Widget ID is required"),
  title: z.string().min(1, "Title is required"),
  query: z.string().min(1, "Query is required"),
  config: z.string().transform((str, ctx) => {
    try {
      const parsed = JSON.parse(str);
      const result = QueryWidgetConfig.safeParse(parsed);
      if (!result.success) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "Invalid widget config",
        });
        return z.NEVER;
      }
      return result.data;
    } catch {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid JSON",
      });
      return z.NEVER;
    }
  }),
});

const RenameWidgetSchema = z.object({
  widgetId: z.string().min(1, "Widget ID is required"),
  title: z.string().min(1, "Title is required"),
});

const DeleteWidgetSchema = z.object({
  widgetId: z.string().min(1, "Widget ID is required"),
});

const DuplicateWidgetSchema = z.object({
  widgetId: z.string().min(1, "Widget ID is required"),
  newId: z.string().min(1, "New widget ID is required").nullish(),
});

const SaveLayoutSchema = z.object({
  layout: z.string().transform((str, ctx) => {
    try {
      const parsed = JSON.parse(str);
      const result = z.array(LayoutItem).safeParse(parsed);
      if (!result.success) {
        ctx.addIssue({
          code: z.ZodIssueCode.custom,
          message: "Invalid layout format",
        });
        return z.NEVER;
      }
      return result.data;
    } catch {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "Invalid JSON",
      });
      return z.NEVER;
    }
  }),
});

const ParamsSchema = EnvironmentParamSchema.extend({
  dashboardId: z.string(),
});

// Check widget limit for add/duplicate actions (title widgets don't count)
async function checkWidgetLimit(
  existingLayout: z.infer<typeof DashboardLayout>,
  organizationId: string
) {
  const currentWidgetCount = Object.values(existingLayout.widgets).filter(
    (w) => w.display.type !== "title"
  ).length;
  const plan = await getCurrentPlan(organizationId);
  const metricWidgetsLimitValue = (plan?.v3Subscription?.plan?.limits as any)
    ?.metricWidgetsPerDashboard;
  const widgetLimit =
    typeof metricWidgetsLimitValue === "number"
      ? metricWidgetsLimitValue
      : (metricWidgetsLimitValue?.number ?? 16);

  if (currentWidgetCount >= widgetLimit) {
    throw new Response("Widget limit reached", { status: 403 });
  }
}

// Optimistic concurrency save: uses updateMany so we can include updatedAt
// in the where clause. If another request modified the dashboard between our
// read and this write, updatedAt won't match and count will be 0.
async function saveDashboardLayout(
  dashboardId: string,
  expectedUpdatedAt: Date,
  updatedLayout: z.infer<typeof DashboardLayout>
) {
  const result = await prisma.metricsDashboard.updateMany({
    where: { id: dashboardId, updatedAt: expectedUpdatedAt },
    data: {
      layout: JSON.stringify(updatedLayout),
    },
  });

  if (result.count === 0) {
    throw new Response(
      "Dashboard was modified by another request. Please refresh and try again.",
      { status: 409 }
    );
  }
}

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

  const project = await findProjectBySlug(organizationSlug, projectParam, userId);
  if (!project) {
    throw new Response("Project not found", { status: 404 });
  }

  const environment = await findEnvironmentBySlug(project.id, envParam, userId);
  if (!environment) {
    throw new Response("Environment not found", { status: 404 });
  }

  // Load the dashboard
  const dashboard = await prisma.metricsDashboard.findFirst({
    where: {
      friendlyId: dashboardId,
      organizationId: project.organizationId,
    },
  });

  if (!dashboard) {
    throw new Response("Dashboard not found", { status: 404 });
  }

  const formData = await request.formData();
  const action = formData.get("action");

  // Parse existing layout (shared across all actions)
  let existingLayout: z.infer<typeof DashboardLayout>;
  try {
    const parsed = JSON.parse(dashboard.layout);
    const layoutResult = DashboardLayout.safeParse(parsed);
    if (!layoutResult.success) {
      // For add action, we can start with empty layout
      if (action === "add") {
        existingLayout = {
          version: "1",
          layout: [],
          widgets: {},
        };
      } else {
        throw new Response("Invalid dashboard layout", { status: 500 });
      }
    } else {
      existingLayout = layoutResult.data;
    }
  } catch (e) {
    if (e instanceof Response) throw e;
    if (action === "add") {
      existingLayout = {
        version: "1",
        layout: [],
        widgets: {},
      };
    } else {
      throw new Response("Failed to parse dashboard layout", { status: 500 });
    }
  }

  switch (action) {
    case "add": {
      const rawData = {
        widgetId: formData.get("widgetId"),
        title: formData.get("title"),
        query: formData.get("query"),
        config: formData.get("config"),
      };

      const result = AddWidgetSchema.safeParse(rawData);
      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      const { title, query, config } = result.data;

      // Validate that non-title widgets have a query
      if (config.type !== "title" && !query) {
        throw new Response("Query is required for chart widgets", { status: 400 });
      }

      // Title widgets don't count against the limit
      if (config.type !== "title") {
        await checkWidgetLimit(existingLayout, project.organizationId);
      }

      // Use client-provided widget ID if available, otherwise generate one
      // Using the client's ID ensures optimistic UI state stays in sync with the server
      const widgetId = result.data.widgetId || nanoid(8);

      // Calculate position at the bottom
      let maxBottom = 0;
      for (const item of existingLayout.layout) {
        const itemBottom = item.y + item.h;
        if (itemBottom > maxBottom) {
          maxBottom = itemBottom;
        }
      }

      // Add new layout item (full width, height depends on widget type)
      const newLayoutItem = {
        i: widgetId,
        x: 0,
        y: maxBottom,
        w: 12,
        h: config.type === "title" ? 2 : 15,
      };

      // Add new widget
      const newWidget = {
        title,
        query,
        display: config,
      };

      // Update the layout
      const updatedLayout = {
        ...existingLayout,
        layout: [...existingLayout.layout, newLayoutItem],
        widgets: {
          ...existingLayout.widgets,
          [widgetId]: newWidget,
        },
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true, widgetId });
    }

    case "update": {
      const rawData = {
        widgetId: formData.get("widgetId"),
        title: formData.get("title"),
        query: formData.get("query"),
        config: formData.get("config"),
      };

      const result = UpdateWidgetSchema.safeParse(rawData);
      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      const { widgetId, title, query, config } = result.data;

      // Check if widget exists
      if (!existingLayout.widgets[widgetId]) {
        throw new Response("Widget not found", { status: 404 });
      }

      // Update the widget
      const updatedWidget = {
        title,
        query,
        display: config,
      };

      // Update the layout
      const updatedLayout = {
        ...existingLayout,
        widgets: {
          ...existingLayout.widgets,
          [widgetId]: updatedWidget,
        },
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true, updatedTitle: title });
    }

    case "rename": {
      const rawData = {
        widgetId: formData.get("widgetId"),
        title: formData.get("title"),
      };

      const result = RenameWidgetSchema.safeParse(rawData);
      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      const { widgetId, title } = result.data;

      // Check if widget exists
      if (!existingLayout.widgets[widgetId]) {
        throw new Response("Widget not found", { status: 404 });
      }

      // Update just the title
      const updatedLayout = {
        ...existingLayout,
        widgets: {
          ...existingLayout.widgets,
          [widgetId]: {
            ...existingLayout.widgets[widgetId],
            title,
          },
        },
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true, renamedTitle: title });
    }

    case "delete": {
      const rawData = {
        widgetId: formData.get("widgetId"),
      };

      const result = DeleteWidgetSchema.safeParse(rawData);
      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      const { widgetId } = result.data;

      // Get widget title before deleting (for the success message)
      const widget = existingLayout.widgets[widgetId];
      const widgetTitle = widget?.title ?? "Widget";

      // Remove widget from layout and widgets
      const updatedLayout = {
        ...existingLayout,
        layout: existingLayout.layout.filter((item) => item.i !== widgetId),
        widgets: Object.fromEntries(
          Object.entries(existingLayout.widgets).filter(([key]) => key !== widgetId)
        ),
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true, deletedTitle: widgetTitle });
    }

    case "duplicate": {
      await checkWidgetLimit(existingLayout, project.organizationId);

      const rawData = {
        widgetId: formData.get("widgetId"),
        newId: formData.get("newId"),
      };

      const result = DuplicateWidgetSchema.safeParse(rawData);
      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      const { widgetId } = result.data;

      // Find the original widget
      const originalWidget = existingLayout.widgets[widgetId];
      if (!originalWidget) {
        throw new Response("Widget not found", { status: 404 });
      }

      // Find the original layout item
      const originalLayoutItem = existingLayout.layout.find((item) => item.i === widgetId);
      if (!originalLayoutItem) {
        throw new Response("Widget layout not found", { status: 404 });
      }

      // Use client-provided ID if available, otherwise generate one
      // Using the client's ID ensures optimistic UI state stays in sync with the server
      const newWidgetId = result.data.newId || nanoid(8);

      // Calculate position at the bottom
      let maxBottom = 0;
      for (const item of existingLayout.layout) {
        const itemBottom = item.y + item.h;
        if (itemBottom > maxBottom) {
          maxBottom = itemBottom;
        }
      }

      // Create new layout item with same dimensions but at the bottom
      const newLayoutItem = {
        i: newWidgetId,
        x: 0,
        y: maxBottom,
        w: originalLayoutItem.w,
        h: originalLayoutItem.h,
      };

      // Create new widget with "(Copy)" suffix
      const newWidget = {
        ...originalWidget,
        title: `${originalWidget.title} (Copy)`,
      };

      // Update the layout
      const updatedLayout = {
        ...existingLayout,
        layout: [...existingLayout.layout, newLayoutItem],
        widgets: {
          ...existingLayout.widgets,
          [newWidgetId]: newWidget,
        },
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true, duplicatedTitle: originalWidget.title });
    }

    case "layout": {
      const result = SaveLayoutSchema.safeParse({
        layout: formData.get("layout"),
      });

      if (!result.success) {
        throw new Response("Invalid form data: " + result.error.message, { status: 400 });
      }

      // Update layout positions while preserving widgets
      const updatedLayout = {
        ...existingLayout,
        layout: result.data.layout,
      };

      // Save to database (with optimistic concurrency check)
      await saveDashboardLayout(dashboard.id, dashboard.updatedAt, updatedLayout);

      return typedjson({ success: true });
    }

    default: {
      throw new Response("Invalid action", { status: 400 });
    }
  }
};
