import type { OutputColumnMetadata } from "@internal/clickhouse";
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { z } from "zod";
import { requireUserId } from "~/services/session.server";
import { hasAccessToEnvironment } from "~/models/runtimeEnvironment.server";
import { executeQuery } from "~/services/queryService.server";
import {
  QueryWidget,
  type QueryWidgetConfig,
  type QueryWidgetData,
} from "~/components/metrics/QueryWidget";
import { useElementVisibility } from "~/hooks/useElementVisibility";
import { useInterval } from "~/hooks/useInterval";
import { env } from "~/env.server";

const Scope = z.union([z.literal("environment"), z.literal("organization"), z.literal("project")]);

// Response type for the action
type MetricWidgetActionResponse =
  | { success: false; error: string }
  | {
      success: true;
      data: {
        rows: Record<string, unknown>[];
        columns: OutputColumnMetadata[];
        stats: { elapsed_ns: string } | null;
        hiddenColumns: string[] | null;
        reachedMaxRows: boolean;
        periodClipped: number | null;
        maxQueryPeriod: number | undefined;
        timeRange: { from: string; to: string };
      };
    };

const MetricWidgetQuery = z.object({
  query: z.string(),
  organizationId: z.string(),
  projectId: z.string(),
  environmentId: z.string(),
  scope: Scope,
  period: z.string().nullable(),
  from: z.string().nullable(),
  to: z.string().nullable(),
  taskIdentifiers: z.array(z.string()).optional(),
  queues: z.array(z.string()).optional(),
  responseModels: z.array(z.string()).optional(),
  promptSlugs: z.array(z.string()).optional(),
  promptVersions: z.array(z.number()).optional(),
  operations: z.array(z.string()).optional(),
  providers: z.array(z.string()).optional(),
  tags: z.array(z.string()).optional(),
});

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

  const data = await request.json();
  const submission = MetricWidgetQuery.safeParse(data);

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

  const {
    query,
    organizationId,
    projectId,
    environmentId,
    scope,
    period,
    from,
    to,
    taskIdentifiers,
    queues,
    responseModels,
    promptSlugs,
    promptVersions,
    operations,
    providers,
    tags,
  } = submission.data;

  // Check they should be able to access it
  const hasAccess = await hasAccessToEnvironment({
    environmentId,
    projectId,
    organizationId,
    userId,
  });

  if (!hasAccess) {
    return json(
      {
        success: false as const,
        error: "You don't have permission for this resource",
      },
      { status: 400 }
    );
  }

  const queryResult = await executeQuery({
    name: "query-page",
    query,
    scope,
    organizationId,
    projectId,
    environmentId,
    period,
    from,
    to,
    taskIdentifiers,
    queues,
    responseModels,
    promptSlugs,
    promptVersions,
    operations,
    providers,
    // Set higher concurrency if many widgets are on screen at once
    customOrgConcurrencyLimit: env.METRIC_WIDGET_DEFAULT_ORG_CONCURRENCY_LIMIT,
  });

  if (!queryResult.success) {
    return json(
      {
        success: false as const,
        error: queryResult.error.message,
      },
      { status: 400 }
    );
  }

  return json({
    success: true as const,
    data: {
      rows: queryResult.result.rows,
      columns: queryResult.result.columns,
      stats: queryResult.result.stats,
      hiddenColumns: queryResult.result.hiddenColumns ?? null,
      reachedMaxRows: queryResult.result.reachedMaxRows,
      periodClipped: queryResult.periodClipped,
      maxQueryPeriod: queryResult.maxQueryPeriod,
      timeRange: {
        from: queryResult.timeRange.from.toISOString(),
        to: queryResult.timeRange.to.toISOString(),
      },
    },
  });
};

type MetricWidgetProps = {
  /** Unique key for this widget - used to identify the fetcher */
  widgetKey: string;
  title: string;
  config: QueryWidgetConfig;
  refreshIntervalMs?: number;
  isResizing?: boolean;
  isDraggable?: boolean;
  /** Callback when edit button is clicked - receives current data */
  onEdit?: (data: QueryWidgetData) => void;
  /** Callback when rename is clicked - receives new title */
  onRename?: (newTitle: string) => void;
  /** Callback when delete is clicked */
  onDelete?: () => void;
  /** Callback when duplicate is clicked - receives current data */
  onDuplicate?: (data: QueryWidgetData) => void;
  /** Column names to hide from table display but keep in row data (useful for linking) */
  hiddenColumns?: string[];
} & z.infer<typeof MetricWidgetQuery>;

export function MetricWidget({
  widgetKey,
  title,
  config,
  refreshIntervalMs,
  isResizing,
  isDraggable,
  onEdit,
  onRename,
  onDelete,
  onDuplicate,
  hiddenColumns,
  ...props
}: MetricWidgetProps) {
  const [response, setResponse] = useState<MetricWidgetActionResponse | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const abortControllerRef = useRef<AbortController | null>(null);
  const isDirtyRef = useRef(false);

  // Track the latest props so the submit callback always uses fresh values
  // without needing to be recreated (which would cause useInterval to re-register listeners).
  const propsRef = useRef(props);
  propsRef.current = props;

  const submit = useCallback(() => {
    if (!isVisibleRef.current) {
      isDirtyRef.current = true;
      return;
    }
    isDirtyRef.current = false;

    // Abort any in-flight request for this widget
    abortControllerRef.current?.abort();

    const controller = new AbortController();
    abortControllerRef.current = controller;
    setIsLoading(true);

    fetch(`/resources/metric`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(propsRef.current),
      signal: controller.signal,
    })
      .then(async (res) => {
        try {
          return (await res.json()) as MetricWidgetActionResponse;
        } catch {
          throw new Error(`Request failed (${res.status})`);
        }
      })
      .then((data) => {
        if (!controller.signal.aborted) {
          setResponse(data);
          setIsLoading(false);
        }
      })
      .catch((err) => {
        if (err instanceof DOMException && err.name === "AbortError") return;
        if (!controller.signal.aborted) {
          // Only surface the error if there's no existing successful data to preserve
          setResponse((prev) =>
            prev?.success ? prev : { success: false, error: err.message || "Network error" }
          );
          setIsLoading(false);
        }
      });
  }, []);

  // Track visibility so we only fetch for on-screen widgets.
  // When a widget scrolls into view and has no data yet, trigger a load.
  const { ref: visibilityRef, isVisibleRef } = useElementVisibility({
    onVisibilityChange: (visible) => {
      if (visible && (!response || isDirtyRef.current)) {
        submit();
      }
    },
  });

  // Clean up on unmount
  useEffect(() => {
    return () => {
      abortControllerRef.current?.abort();
    };
  }, []);

  // Reload periodically and on focus (onLoad: false — the useEffect below handles initial load)
  useInterval({ interval: refreshIntervalMs, callback: submit, onLoad: false });

  // Reload on mount and when query, time period, or filters change
  useEffect(() => {
    submit();
  }, [
    submit,
    props.query,
    props.from,
    props.to,
    props.period,
    props.scope,
    JSON.stringify(props.taskIdentifiers),
    JSON.stringify(props.queues),
    JSON.stringify(props.responseModels),
    JSON.stringify(props.promptSlugs),
    JSON.stringify(props.promptVersions),
    JSON.stringify(props.operations),
    JSON.stringify(props.providers),
  ]);

  const data = response?.success
    ? { rows: response.data.rows, columns: response.data.columns }
    : { rows: [], columns: [] };

  const timeRange = response?.success ? response.data.timeRange : undefined;

  return (
    <div ref={visibilityRef} className="h-full">
      <QueryWidget
        title={title}
        titleString={title}
        query={props.query}
        config={config}
        isLoading={isLoading}
        data={data}
        timeRange={timeRange}
        error={response?.success === false ? response.error : undefined}
        isResizing={isResizing}
        isDraggable={isDraggable}
        onEdit={onEdit}
        onRename={onRename}
        onDelete={onDelete}
        onDuplicate={onDuplicate}
        hiddenColumns={hiddenColumns}
      />
    </div>
  );
}
