import { GitCompareArrows } from "lucide-react";
import { Suspense, type JSX } from "react";
import { useMemo, useRef, useState, memo } from "react";
import type { CapturedLog } from "../../proxy/schemas";
import { stripClaudeCodeBillingHeader } from "../../proxy/claudeCodeStrip";
import { Button } from "../ui/button";
import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "../ui/tooltip";
import { useJsonBulkExpansion } from "../ui/json-viewer-bulk";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { useCopyFeedback } from "./useCopyFeedback";
import type { HeaderTabActions } from "./LogEntryHeader";
import {
  LazyJsonViewer,
  LazyJsonViewerFromString,
  LazyReplayDialog,
  LazyRequestAnatomy,
  LazyResponseView,
  LazyStreamingChunkSequence,
} from "./lazy";
import type { AnatomySegment } from "./anatomy/types";
import { useAnatomyJump } from "./anatomy/useAnatomyJump";
import { computeHeadersDiff, computeRequestDiff, DiffView } from "./diff";
import { LogEntryHeader } from "./LogEntryHeader";
import type { CacheTrendEntry } from "./cacheTrend";
import { getLogFormatAdapter, resolveLogFormat } from "./log-formats";
import {
  shouldShowHeadersDiffButton,
  shouldShowRawRequestTab,
  shouldShowRequestDiffButton,
} from "./logEntryVisibility";

/**
 * Lightweight fallback for lazy-loaded tabs. Renders an empty 1px-tall row so
 * the layout doesn't jump while the chunk is fetching.
 */
function TabFallback(): JSX.Element {
  return <div className="h-1" aria-hidden="true" />;
}

export type LogEntryProps = {
  log: CapturedLog;
  viewMode?: "simple" | "full";
  /** Live "strip Claude Code billing header" flag, sourced once at the viewer
   * container. Hoisted out of `LogEntry` so a single SWR subscription serves
   * the whole virtualized list (N logs == N subscriptions is the previous
   * cost).
   */
  strip: boolean;
  /** Slow-response threshold in seconds. `0` disables the warning indicator. */
  slowResponseThresholdSeconds: number;
  /**
   * Per-log cache token trend, looked up in the viewer-level trend map.
   * `null` (or absent) means the header should render with no arrows.
   */
  cacheTrend?: CacheTrendEntry | null;
  /** Callback to open CompareDrawer with this log and its immediate predecessor. */
  onCompareWithPrevious?: (log: CapturedLog) => void;
};

function DiffToggleButton({
  active,
  onClick,
}: {
  active: boolean;
  onClick: (e: React.MouseEvent) => void;
}): JSX.Element {
  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <Button
          variant={active ? "default" : "outline"}
          size="sm"
          className="h-8 text-xs"
          onClick={onClick}
          aria-pressed={active}
        >
          <GitCompareArrows className="size-3.5 mr-1" />
          {active ? "Showing diff" : "Diff with Raw"}
        </Button>
      </TooltipTrigger>
      <TooltipContent>
        {active ? "Hide diff view" : "Compare proxy output against the original raw version"}
      </TooltipContent>
    </Tooltip>
  );
}

const RequestDiffContent = memo(function ({
  rawBody,
  displayedBody,
  emptyLabel,
}: {
  rawBody: string | null;
  displayedBody: string | null;
  emptyLabel: string;
}): JSX.Element {
  const result = useMemo(
    () => computeRequestDiff(rawBody, displayedBody),
    [rawBody, displayedBody],
  );
  return <DiffView result={result} emptyLabel={emptyLabel} />;
});

const HeadersDiffContent = memo(function ({
  rawHeaders,
  headers,
  emptyLabel,
}: {
  rawHeaders: Record<string, string> | undefined;
  headers: Record<string, string> | undefined;
  emptyLabel: string;
}): JSX.Element {
  const result = useMemo(() => computeHeadersDiff(rawHeaders, headers), [rawHeaders, headers]);
  return <DiffView result={result} emptyLabel={emptyLabel} />;
});

export const LogEntry = memo(function ({
  log,
  viewMode = "simple",
  strip,
  slowResponseThresholdSeconds,
  cacheTrend = null,
  onCompareWithPrevious,
}: LogEntryProps): JSX.Element {
  const [expanded, setExpanded] = useState<boolean>(false);
  const [replayOpen, setReplayOpen] = useState<boolean>(false);
  const [headersDiff, setHeadersDiff] = useState<boolean>(false);
  const [requestDiff, setRequestDiff] = useState<boolean>(false);
  const [activeTab, setActiveTab] = useState("request");
  const [expandToPath, setExpandToPath] = useState<string | null>(null);
  const requestJsonRef = useRef<HTMLDivElement | null>(null);
  const resolvedFormat = resolveLogFormat(log);
  const adapter = getLogFormatAdapter(resolvedFormat);
  const requestAnalysis = useMemo(
    () => adapter.analyzeRequest(log.rawRequestBody),
    [adapter, log.rawRequestBody],
  );
  const responseAnalysis = useMemo(
    () => adapter.analyzeResponse(log.responseText),
    [adapter, log.responseText],
  );
  const strippedRequestBody = useMemo(() => {
    if (!strip || resolvedFormat !== "anthropic" || log.rawRequestBody === null) {
      return null;
    }
    return stripClaudeCodeBillingHeader(log.rawRequestBody).body;
  }, [log.rawRequestBody, resolvedFormat, strip]);
  const displayedRequestBody = strippedRequestBody ?? log.rawRequestBody;
  const requestExpansion = useJsonBulkExpansion(displayedRequestBody);
  const rawRequestExpansion = useJsonBulkExpansion(log.rawRequestBody);
  const responseExpansion = useJsonBulkExpansion(log.responseText);

  // Headers are rendered as a flat list, so we copy them as pretty-printed JSON.
  // Only build the string when there's at least one entry — empty headers would
  // otherwise copy "{}", which is misleading.
  const headersText = useMemo(
    () =>
      log.headers && Object.keys(log.headers).length > 0
        ? JSON.stringify(log.headers, null, 2)
        : null,
    [log.headers],
  );
  const rawHeadersText = useMemo(
    () =>
      log.rawHeaders && Object.keys(log.rawHeaders).length > 0
        ? JSON.stringify(log.rawHeaders, null, 2)
        : null,
    [log.rawHeaders],
  );

  // One copy-feedback hook per JSON-bearing tab, so each tab can surface its
  // own Copy button in the header with independent "Copied!" feedback.
  const requestCopy = useCopyFeedback(displayedRequestBody);
  const rawRequestCopy = useCopyFeedback(log.rawRequestBody);
  const headersCopy = useCopyFeedback(headersText);
  const rawHeadersCopy = useCopyFeedback(rawHeadersText);
  const responseCopy = useCopyFeedback(log.responseText);

  // Per-tab action bundles consumed by the header. The header renders the
  // entry whose key matches `activeTab`. Tabs without an entry (Anatomy,
  // Parsed Response) render no header buttons.
  const tabActions: HeaderTabActions = useMemo(
    () => ({
      request: {
        copyLabel: "Copy request body",
        copyText: displayedRequestBody,
        copyCopied: requestCopy.copied,
        onCopy: requestCopy.copy,
        expansion: {
          isExpanded: requestExpansion.isExpanded,
          isPending: requestExpansion.isPending,
          onToggle: requestExpansion.toggle,
        },
        // "Diff with Raw" only makes sense when there's a raw body to compare
        // against (Anthropic + strip pipeline produces one). "Diff with
        // Previous" only exists when the parent wired up the compare drawer.
        diffWithRaw: shouldShowRequestDiffButton(
          resolvedFormat,
          viewMode,
          strip,
          log.rawRequestBody !== null,
        )
          ? { active: requestDiff, onToggle: () => setRequestDiff(!requestDiff) }
          : undefined,
        diffWithPrevious:
          onCompareWithPrevious === undefined
            ? undefined
            : () => {
                onCompareWithPrevious(log);
              },
      },
      "raw-request": {
        copyLabel: "Copy raw request",
        copyText: log.rawRequestBody,
        copyCopied: rawRequestCopy.copied,
        onCopy: rawRequestCopy.copy,
        expansion: {
          isExpanded: rawRequestExpansion.isExpanded,
          isPending: rawRequestExpansion.isPending,
          onToggle: rawRequestExpansion.toggle,
        },
      },
      headers: {
        copyLabel: "Copy headers",
        copyText: headersText,
        copyCopied: headersCopy.copied,
        onCopy: headersCopy.copy,
        // Headers are a flat dict, no JSON tree to expand.
        expansion: null,
      },
      "raw-headers": {
        copyLabel: "Copy raw headers",
        copyText: rawHeadersText,
        copyCopied: rawHeadersCopy.copied,
        onCopy: rawHeadersCopy.copy,
        expansion: null,
      },
      raw: {
        copyLabel: "Copy response",
        copyText: log.responseText,
        copyCopied: responseCopy.copied,
        onCopy: responseCopy.copy,
        expansion: {
          isExpanded: responseExpansion.isExpanded,
          isPending: responseExpansion.isPending,
          onToggle: responseExpansion.toggle,
        },
      },
    }),
    [
      displayedRequestBody,
      requestCopy,
      requestExpansion,
      requestDiff,
      log.rawRequestBody,
      rawRequestCopy,
      rawRequestExpansion,
      headersText,
      headersCopy,
      rawHeadersText,
      rawHeadersCopy,
      log.responseText,
      responseCopy,
      responseExpansion,
      resolvedFormat,
      viewMode,
      strip,
      onCompareWithPrevious,
    ],
  );
  const anatomySegments = useMemo(
    () =>
      requestExpansion.parsedData !== null
        ? adapter.anatomySegments(requestExpansion.parsedData)
        : null,
    [adapter, requestExpansion.parsedData],
  );
  const anatomyPaths = useMemo(() => {
    if (anatomySegments === null) return undefined;
    return new Set(anatomySegments.map((s) => s.path));
  }, [anatomySegments]);
  const jumpToAnatomySegment = useAnatomyJump({
    containerRef: requestJsonRef,
    setExpandToPath,
    ensureTabActive: () => {
      if (activeTab !== "request") setActiveTab("request");
    },
  });

  return (
    <TooltipProvider>
      <div className="border border-border rounded-lg mb-1 overflow-hidden">
        <LogEntryHeader
          log={log}
          messageCount={requestAnalysis.messageCount}
          toolCount={requestAnalysis.toolCount}
          expanded={expanded}
          onToggle={() => setExpanded(!expanded)}
          responseToolNames={responseAnalysis.toolNames}
          cacheTrend={cacheTrend}
          slowResponseThresholdSeconds={slowResponseThresholdSeconds}
          activeTab={activeTab}
          tabActions={tabActions}
          onReplay={
            onCompareWithPrevious === undefined
              ? undefined
              : () => {
                  setReplayOpen(true);
                }
          }
        />

        {expanded && (
          <div onClick={(e) => e.stopPropagation()} onKeyDown={(e) => e.stopPropagation()}>
            <Tabs value={activeTab} onValueChange={setActiveTab}>
              <TabsList className="mx-4 mt-2">
                {viewMode === "full" && <TabsTrigger value="raw-headers">Raw Headers</TabsTrigger>}
                {viewMode === "full" && <TabsTrigger value="headers">Headers</TabsTrigger>}
                {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
                  <TabsTrigger value="raw-request">Raw Request</TabsTrigger>
                )}
                <TabsTrigger value="request">Request</TabsTrigger>
                {anatomySegments !== null && <TabsTrigger value="anatomy">Anatomy</TabsTrigger>}
                {viewMode === "full" && <TabsTrigger value="raw">Raw Response</TabsTrigger>}
                <TabsTrigger value="parsed">Response</TabsTrigger>
              </TabsList>

              {shouldShowRawRequestTab(resolvedFormat, viewMode, strip) && (
                <TabsContent value="raw-request">
                  {activeTab === "raw-request" && (
                    <div className="px-4 pt-1 pb-3">
                      {log.rawRequestBody === null ? (
                        <p className="text-xs text-muted-foreground italic">No request body</p>
                      ) : rawRequestExpansion.parsedData !== null ? (
                        <Suspense fallback={<TabFallback />}>
                          <LazyJsonViewer
                            data={rawRequestExpansion.parsedData}
                            bulkDepth={rawRequestExpansion.bulkDepth}
                            bulkRevision={rawRequestExpansion.bulkRevision}
                          />
                        </Suspense>
                      ) : (
                        <pre className="font-mono text-xs whitespace-pre-wrap break-words">
                          {log.rawRequestBody}
                        </pre>
                      )}
                    </div>
                  )}
                </TabsContent>
              )}

              <TabsContent value="request">
                {activeTab === "request" && (
                  <div className="px-4 pt-1 pb-3">
                    {/* All tab actions (Copy, Expand, Diff with Raw, Diff with
                        Previous) live in the log header. The body is just
                        the diff or JSON view, depending on toggle state. */}
                    {requestDiff ? (
                      <RequestDiffContent
                        rawBody={log.rawRequestBody}
                        displayedBody={displayedRequestBody}
                        emptyLabel="No transformation applied — raw and sent request bodies are identical."
                      />
                    ) : (
                      <div ref={requestJsonRef}>
                        {displayedRequestBody === null ? (
                          <p className="text-xs text-muted-foreground italic">No request body</p>
                        ) : requestExpansion.parsedData !== null ? (
                          <Suspense fallback={<TabFallback />}>
                            <LazyJsonViewer
                              data={requestExpansion.parsedData}
                              bulkDepth={requestExpansion.bulkDepth}
                              bulkRevision={requestExpansion.bulkRevision}
                              anatomyPaths={anatomyPaths}
                              expandToPath={expandToPath}
                            />
                          </Suspense>
                        ) : (
                          <pre className="font-mono text-xs whitespace-pre-wrap break-words">
                            {displayedRequestBody}
                          </pre>
                        )}
                      </div>
                    )}
                  </div>
                )}
              </TabsContent>

              {anatomySegments !== null && (
                <TabsContent value="anatomy">
                  {activeTab === "anatomy" && (
                    <Suspense fallback={<TabFallback />}>
                      <LazyRequestAnatomy
                        parsed={null}
                        inputTokens={log.inputTokens ?? null}
                        segments={anatomySegments}
                        onSegmentActivate={jumpToAnatomySegment}
                      />
                    </Suspense>
                  )}
                </TabsContent>
              )}

              {viewMode === "full" && (
                <TabsContent value="headers">
                  {activeTab === "headers" && (
                    <div className="px-4 pt-1 pb-3">
                      {/* Copy lives in the log header. */}
                      <div className="flex justify-end gap-2 mb-2">
                        {shouldShowHeadersDiffButton(
                          viewMode,
                          log.rawHeaders !== undefined && Object.keys(log.rawHeaders).length > 0,
                        ) && (
                          <DiffToggleButton
                            active={headersDiff}
                            onClick={(e) => {
                              e.stopPropagation();
                              setHeadersDiff(!headersDiff);
                            }}
                          />
                        )}
                      </div>
                      {headersDiff ? (
                        <HeadersDiffContent
                          rawHeaders={log.rawHeaders}
                          headers={log.headers}
                          emptyLabel="No transformation applied — raw and processed headers are identical."
                        />
                      ) : log.headers && Object.keys(log.headers).length > 0 ? (
                        <div className="space-y-1 font-mono text-xs">
                          {Object.entries(log.headers)
                            .sort(([a], [b]) => a.localeCompare(b))
                            .map(([key, value]) => (
                              <div key={key} className="flex gap-2">
                                <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
                                  {key}:
                                </span>
                                <span className="text-muted-foreground truncate" title={value}>
                                  {value}
                                </span>
                              </div>
                            ))}
                        </div>
                      ) : (
                        <p className="text-xs text-muted-foreground italic">No headers captured</p>
                      )}
                    </div>
                  )}
                </TabsContent>
              )}

              {viewMode === "full" && (
                <TabsContent value="raw-headers">
                  {activeTab === "raw-headers" && (
                    <div className="px-4 pt-1 pb-3">
                      {/* Copy lives in the log header. */}
                      {log.rawHeaders && Object.keys(log.rawHeaders).length > 0 ? (
                        <div className="space-y-1 font-mono text-xs">
                          {Object.entries(log.rawHeaders)
                            .sort(([a], [b]) => a.localeCompare(b))
                            .map(([key, value]) => (
                              <div key={key} className="flex gap-2">
                                <span className="text-blue-600 dark:text-blue-400 font-semibold shrink-0">
                                  {key}:
                                </span>
                                <span className="text-muted-foreground truncate" title={value}>
                                  {value}
                                </span>
                              </div>
                            ))}
                        </div>
                      ) : (
                        <p className="text-xs text-muted-foreground italic">
                          No raw headers captured
                        </p>
                      )}
                    </div>
                  )}
                </TabsContent>
              )}

              <TabsContent value="raw">
                {activeTab === "raw" && (
                  <div className="px-4 pt-1 pb-3 space-y-3">
                    {log.error !== undefined && log.error !== null && (
                      <div className="rounded border border-destructive/50 bg-destructive/10 p-3 text-xs">
                        <div className="font-semibold text-destructive mb-1">SSE Error</div>
                        <div className="text-muted-foreground font-mono">{log.error}</div>
                      </div>
                    )}
                    {log.responseText !== null ? (
                      <Suspense fallback={<TabFallback />}>
                        <LazyJsonViewerFromString
                          text={log.responseText}
                          defaultExpandDepth={0}
                          bulkDepth={responseExpansion.bulkDepth}
                          bulkRevision={responseExpansion.bulkRevision}
                        />
                      </Suspense>
                    ) : (
                      <p className="text-xs text-muted-foreground italic">No response</p>
                    )}
                    {log.streaming === true && (
                      <Suspense fallback={<TabFallback />}>
                        <LazyStreamingChunkSequence
                          logId={log.id}
                          truncated={log.streamingChunksPath !== null}
                        />
                      </Suspense>
                    )}
                  </div>
                )}
              </TabsContent>

              <TabsContent value="parsed">
                {activeTab === "parsed" && (
                  <div className="px-4 pt-1 pb-3">
                    <Suspense fallback={<TabFallback />}>
                      <LazyResponseView
                        responseText={log.responseText}
                        responseStatus={log.responseStatus}
                        streaming={log.streaming}
                        inputTokens={log.inputTokens}
                        outputTokens={log.outputTokens}
                        cacheCreationInputTokens={log.cacheCreationInputTokens}
                        cacheReadInputTokens={log.cacheReadInputTokens}
                        apiFormat={resolvedFormat}
                        error={log.error}
                      />
                    </Suspense>
                  </div>
                )}
              </TabsContent>
            </Tabs>
          </div>
        )}
      </div>
      <Suspense fallback={null}>
        <LazyReplayDialog log={log} open={replayOpen} onOpenChange={setReplayOpen} />
      </Suspense>
    </TooltipProvider>
  );
});
