import { type LoaderFunctionArgs, redirect } from "@remix-run/server-runtime";
import { type MetaFunction, useFetcher, useNavigation, useLocation, Form } from "@remix-run/react";
import { XMarkIcon } from "@heroicons/react/20/solid";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import {
  TypedAwait,
  typeddefer,
  type UseDataFunctionReturn,
  useTypedLoaderData,
} from "remix-typedjson";
import { requireUser } from "~/services/session.server";
import { getCurrentPlan } from "~/services/platform.v3.server";
import { EnvironmentParamSchema } from "~/utils/pathBuilder";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { LogsListPresenter, LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import type { LogLevel } from "~/utils/logUtils";
import { $replica, prisma } from "~/db.server";
import { clickhouseFactory } from "~/services/clickhouse/clickhouseFactoryInstance.server";
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
import { PageBody, PageContainer } from "~/components/layout/AppLayout";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react";
import { useOptimisticLocation } from "~/hooks/useOptimisticLocation";
import { Spinner } from "~/components/primitives/Spinner";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Callout } from "~/components/primitives/Callout";
import { LogsTable } from "~/components/logs/LogsTable";
import { LogDetailView } from "~/components/logs/LogDetailView";
import { SearchInput } from "~/components/primitives/SearchInput";
import { LogsLevelFilter } from "~/components/logs/LogsLevelFilter";
import { LogsTaskFilter } from "~/components/logs/LogsTaskFilter";
import { LogsRunIdFilter } from "~/components/logs/LogsRunIdFilter";
import { TimeFilter } from "~/components/runs/v3/SharedFilters";
import {
  RESIZABLE_PANEL_ANIMATION,
  ResizableHandle,
  ResizablePanel,
  ResizablePanelGroup,
  collapsibleHandleClassName,
  useFrozenValue,
} from "~/components/primitives/Resizable";
import { Button } from "~/components/primitives/Buttons";
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags";

// Valid log levels for filtering
const validLevels: LogLevel[] = ["TRACE", "DEBUG", "INFO", "WARN", "ERROR"];

function parseLevelsFromUrl(url: URL): LogLevel[] | undefined {
  const levelParams = url.searchParams.getAll("levels").filter((v) => v.length > 0);
  if (levelParams.length === 0) return undefined;
  return levelParams.filter((l): l is LogLevel => validLevels.includes(l as LogLevel));
}

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

// TODO: Move this to a more appropriate shared location
async function hasLogsPageAccess(
  userId: string,
  isAdmin: boolean,
  isImpersonating: boolean,
  organizationSlug: string
): Promise<boolean> {
  if (isAdmin || isImpersonating) {
    return true;
  }

  // Check organization feature flags
  const organization = await prisma.organization.findFirst({
    where: {
      slug: organizationSlug,
      members: { some: { userId } },
    },
    select: {
      featureFlags: true,
    },
  });

  if (!organization?.featureFlags) {
    return false;
  }

  const flags = organization.featureFlags as Record<string, unknown>;
  const hasLogsPageAccessResult = validateFeatureFlagValue(
    FEATURE_FLAG.hasLogsPageAccess,
    flags.hasLogsPageAccess
  );

  return hasLogsPageAccessResult.success && hasLogsPageAccessResult.data === true;
}

export const loader = async ({ request, params }: LoaderFunctionArgs) => {
  const user = await requireUser(request);
  const userId = user.id;

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

  const canAccess = await hasLogsPageAccess(
    userId,
    user.admin,
    user.isImpersonating,
    organizationSlug
  );

  if (!canAccess) {
    throw redirect("/");
  }

  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 });
  }

  // Get filters from query params
  const url = new URL(request.url);
  const tasks = url.searchParams.getAll("tasks").filter((t) => t.length > 0);
  const runId = url.searchParams.get("runId") ?? undefined;
  const search = url.searchParams.get("search") ?? undefined;
  const levels = parseLevelsFromUrl(url);
  const period = url.searchParams.get("period") ?? undefined;
  const fromStr = url.searchParams.get("from");
  const toStr = url.searchParams.get("to");
  const from = fromStr ? parseInt(fromStr, 10) : undefined;
  const to = toStr ? parseInt(toStr, 10) : undefined;

  // Get the user's plan to determine log retention limit
  const plan = await getCurrentPlan(project.organizationId);
  const retentionLimitDays = plan?.v3Subscription?.plan?.limits.logRetentionDays.number ?? 30;

  const logsClickhouse = await clickhouseFactory.getClickhouseForOrganization(
    project.organizationId,
    "logs"
  );
  const presenter = new LogsListPresenter($replica, logsClickhouse);

  const listPromise = presenter
    .call(project.organizationId, environment.id, {
      userId,
      projectId: project.id,
      tasks: tasks.length > 0 ? tasks : undefined,
      runId,
      search,
      levels,
      period,
      from,
      to,
      defaultPeriod: "1h",
      retentionLimitDays,
    })
    .catch((error) => {
      if (error instanceof ServiceValidationError) {
        return { error: error.message };
      }
      throw error;
    });

  return typeddefer({
    data: listPromise,
    defaultPeriod: "1h",
    retentionLimitDays,
  });
};

export default function Page() {
  const { data, defaultPeriod, retentionLimitDays } = useTypedLoaderData<typeof loader>();

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

      <PageBody scrollable={false}>
        <Suspense
          fallback={
            <div className="grid h-full max-h-full grid-rows-[2.5rem_auto] overflow-hidden">
              <div className="border-b border-grid-bright" />
              <div className="my-2 flex items-center justify-center">
                <div className="mx-auto flex items-center gap-2">
                  <Spinner />
                  <Paragraph variant="small">Loading logs…</Paragraph>
                </div>
              </div>
            </div>
          }
        >
          <TypedAwait
            resolve={data}
            errorElement={
              <div className="grid h-full max-h-full grid-rows-[2.5rem_auto_1fr] overflow-hidden">
                <FiltersBar defaultPeriod={defaultPeriod} retentionLimitDays={retentionLimitDays} />
                <div className="flex items-center justify-center px-3 py-12">
                  <Callout variant="error" className="max-w-fit">
                    Unable to load your logs. Please refresh the page or try again in a moment.
                  </Callout>
                </div>
              </div>
            }
          >
            {(result) => {
              // Check if result contains an error
              if ("error" in result) {
                return (
                  <div className="grid h-full max-h-full grid-rows-[2.5rem_auto_1fr] overflow-hidden">
                    <FiltersBar
                      defaultPeriod={defaultPeriod}
                      retentionLimitDays={retentionLimitDays}
                    />
                    <div className="flex items-center justify-center px-3 py-12">
                      <Callout variant="error" className="max-w-fit">
                        {result.error}
                      </Callout>
                    </div>
                  </div>
                );
              }
              return (
                <div className="grid h-full max-h-full grid-rows-[2.5rem_1fr] overflow-hidden">
                  <FiltersBar
                    list={result}
                    defaultPeriod={defaultPeriod}
                    retentionLimitDays={retentionLimitDays}
                  />
                  <LogsList list={result} defaultPeriod={defaultPeriod} />
                </div>
              );
            }}
          </TypedAwait>
        </Suspense>
      </PageBody>
    </PageContainer>
  );
}

function FiltersBar({
  list,
  defaultPeriod,
  retentionLimitDays,
}: {
  list?: Exclude<Awaited<UseDataFunctionReturn<typeof loader>["data"]>, { error: string }>;
  defaultPeriod?: string;
  retentionLimitDays: number;
}) {
  const location = useOptimisticLocation();
  const searchParams = new URLSearchParams(location.search);
  const hasFilters =
    searchParams.has("tasks") ||
    searchParams.has("runId") ||
    searchParams.has("search") ||
    searchParams.has("levels") ||
    searchParams.has("period") ||
    searchParams.has("from") ||
    searchParams.has("to");

  return (
    <div className="flex items-start justify-between gap-x-2 border-b border-grid-bright p-2">
      <div className="flex flex-row flex-wrap items-center gap-1.5">
        {list ? (
          <>
            <SearchInput />
            <LogsTaskFilter possibleTasks={list.possibleTasks} />
            <LogsRunIdFilter />
            <TimeFilter defaultPeriod={defaultPeriod} maxPeriodDays={retentionLimitDays} />
            <LogsLevelFilter />
            {hasFilters && (
              <Form className="-ml-1 h-6">
                <Button
                  variant="minimal/small"
                  LeadingIcon={XMarkIcon}
                  tooltip="Clear all filters"
                  className="group-hover/button:bg-transparent"
                  leadingIconClassName="group-hover/button:text-text-bright"
                />
              </Form>
            )}
          </>
        ) : (
          <>
            <LogsTaskFilter possibleTasks={[]} />
            <LogsRunIdFilter />
            <TimeFilter defaultPeriod={defaultPeriod} maxPeriodDays={retentionLimitDays} />
            <LogsLevelFilter />
            <SearchInput />
            {hasFilters && (
              <Form className="-ml-1 h-6">
                <Button
                  variant="minimal/small"
                  LeadingIcon={XMarkIcon}
                  tooltip="Clear all filters"
                  className="group-hover/button:bg-transparent"
                  leadingIconClassName="group-hover/button:text-text-bright"
                />
              </Form>
            )}
          </>
        )}
      </div>
    </div>
  );
}

function LogsList({
  list,
}: {
  list: Exclude<Awaited<UseDataFunctionReturn<typeof loader>["data"]>, { error: string }>; //exclude error, it is handled
  defaultPeriod?: string;
}) {
  const navigation = useNavigation();
  const location = useLocation();
  const fetcher = useFetcher<{ logs: LogEntry[]; pagination: { next?: string } }>();
  const [, startTransition] = useTransition();
  const isLoading = navigation.state !== "idle";

  // Accumulated logs state
  const [accumulatedLogs, setAccumulatedLogs] = useState<LogEntry[]>(list.logs);
  const [nextCursor, setNextCursor] = useState<string | undefined>(list.pagination.next);

  // Selected log state - managed locally to avoid triggering navigation
  const [selectedLogId, setSelectedLogId] = useState<string | undefined>(() => {
    const params = new URLSearchParams(location.search);
    return params.get("log") ?? undefined;
  });

  // Track which filter state (search params) the current fetcher request corresponds to
  const fetcherFilterStateRef = useRef<string>(location.search);
  // Track whether the current fetch is a "check for new" request vs "load more"
  const isCheckingForNewRef = useRef<boolean>(false);

  // Clear accumulated logs immediately when filters change (for instant visual feedback)
  useEffect(() => {
    setAccumulatedLogs([]);
    setNextCursor(undefined);
    // Preserve log selection from URL param, clear if not present
    const params = new URLSearchParams(location.search);
    setSelectedLogId(params.get("log") ?? undefined);
  }, [location.search]);

  // Populate accumulated logs when new data arrives
  useEffect(() => {
    setAccumulatedLogs(list.logs);
    setNextCursor(list.pagination.next);
  }, [list.logs, list.pagination.next]);

  // Clear log parameter from URL when selectedLogId is cleared
  useEffect(() => {
    if (!selectedLogId) {
      const url = new URL(window.location.href);
      if (url.searchParams.has("log")) {
        url.searchParams.delete("log");
        window.history.replaceState(null, "", url.toString());
      }
    }
  }, [selectedLogId]);

  // Append/prepend new logs when fetcher completes (with deduplication)
  useEffect(() => {
    if (fetcher.data && fetcher.state === "idle") {
      // Ignore fetcher data if it was loaded for a different filter state
      if (fetcherFilterStateRef.current !== location.search) {
        return;
      }

      if (isCheckingForNewRef.current) {
        // "Check for new" - prepend new logs, don't update cursor
        setAccumulatedLogs((prev) => {
          const existingIds = new Set(prev.map((log) => log.id));
          const newLogs = fetcher.data!.logs.filter((log) => !existingIds.has(log.id));
          return newLogs.length > 0 ? [...newLogs, ...prev] : prev;
        });
        isCheckingForNewRef.current = false;
      } else {
        // "Load more" - append logs and update cursor
        setAccumulatedLogs((prev) => {
          const existingIds = new Set(prev.map((log) => log.id));
          const newLogs = fetcher.data!.logs.filter((log) => !existingIds.has(log.id));
          return newLogs.length > 0 ? [...prev, ...newLogs] : prev;
        });
        setNextCursor(fetcher.data.pagination.next);
      }
    }
  }, [fetcher.data, fetcher.state, location.search]);

  // Build resource URL for loading more
  const loadMoreUrl = useMemo(() => {
    if (!nextCursor) return null;
    const resourcePath = `/resources${location.pathname}`;
    const params = new URLSearchParams(location.search);
    params.set("cursor", nextCursor);
    params.delete("log");
    return `${resourcePath}?${params.toString()}`;
  }, [location.pathname, location.search, nextCursor]);

  const handleLoadMore = useCallback(() => {
    if (loadMoreUrl && fetcher.state === "idle") {
      // Store the current filter state before loading
      fetcherFilterStateRef.current = location.search;
      fetcher.load(loadMoreUrl);
    }
  }, [loadMoreUrl, fetcher, location.search]);

  const selectedLog = useMemo(() => {
    if (!selectedLogId) return undefined;
    return accumulatedLogs.find((log) => log.id === selectedLogId);
  }, [selectedLogId, accumulatedLogs]);

  const frozenLogId = useFrozenValue(selectedLogId);
  const frozenLog = useFrozenValue(selectedLog);
  const displayLogId = selectedLogId ?? frozenLogId;
  const displayLog = selectedLog ?? frozenLog ?? undefined;

  const updateUrlWithLog = useCallback((logId: string | undefined) => {
    const url = new URL(window.location.href);
    if (logId) {
      url.searchParams.set("log", logId);
    } else {
      url.searchParams.delete("log");
    }
    window.history.replaceState(null, "", url.toString());
  }, []);

  const handleLogSelect = useCallback(
    (logId: string) => {
      startTransition(() => {
        setSelectedLogId(logId);
      });
      updateUrlWithLog(logId);
    },
    [updateUrlWithLog, startTransition]
  );

  const handleClosePanel = useCallback(() => {
    startTransition(() => {
      setSelectedLogId(undefined);
    });
    updateUrlWithLog(undefined);
  }, [updateUrlWithLog, startTransition]);

  const handleCheckForMore = useCallback(() => {
    if (fetcher.state !== "idle") return;
    // Fetch without cursor to check for new logs
    const resourcePath = `/resources${location.pathname}`;
    const params = new URLSearchParams(location.search);
    params.delete("cursor");
    params.delete("log");
    fetcherFilterStateRef.current = location.search;
    isCheckingForNewRef.current = true;
    fetcher.load(`${resourcePath}?${params.toString()}`);
  }, [fetcher, location.pathname, location.search]);

  return (
    <ResizablePanelGroup orientation="horizontal" className="max-h-full">
      <ResizablePanel id="logs-main" min="200px">
        <LogsTable
          key={location.search}
          logs={accumulatedLogs}
          searchTerm={list.searchTerm}
          isLoading={isLoading}
          isLoadingMore={fetcher.state === "loading"}
          hasMore={!!nextCursor}
          onLoadMore={handleLoadMore}
          onCheckForMore={handleCheckForMore}
          selectedLogId={selectedLogId}
          onLogSelect={handleLogSelect}
        />
      </ResizablePanel>
      <ResizableHandle id="logs-handle" className={collapsibleHandleClassName(!!selectedLogId)} />
      <ResizablePanel
        id="log-detail"
        default="430px"
        min="430px"
        max="600px"
        className="overflow-hidden"
        collapsible
        collapsed={!selectedLogId}
        onCollapseChange={() => {}}
        collapsedSize="0px"
        collapseAnimation={RESIZABLE_PANEL_ANIMATION}
      >
        <div className="h-full" style={{ minWidth: 430 }}>
          {displayLogId && (
            <Suspense
              fallback={
                <div className="flex h-full items-center justify-center">
                  <Spinner />
                </div>
              }
            >
              <LogDetailView
                logId={displayLogId}
                initialLog={displayLog}
                onClose={handleClosePanel}
                searchTerm={list.searchTerm}
              />
            </Suspense>
          )}
        </div>
      </ResizablePanel>
    </ResizablePanelGroup>
  );
}
