import { Check, ChevronDown, ChevronRight, ChevronsDown, Copy } from "lucide-react";
import { type JSX, memo, useEffect, useMemo, useState } from "react";
import ReactMarkdown from "react-markdown";
import { cn } from "../../lib/utils";
import { Button } from "./button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
import type { JsonValue } from "./json-viewer-bulk";
import { parseJsonText, safeJsonValue } from "./json-viewer-bulk";

type DataType = "string" | "number" | "boolean" | "null" | "array" | "object";

function classifyValue(value: JsonValue): DataType {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  switch (typeof value) {
    case "string":
      return "string";
    case "number":
      return "number";
    case "boolean":
      return "boolean";
    case "object":
      return "object";
    case "bigint":
    case "symbol":
    case "undefined":
    case "function":
      return "object";
  }
}

function isExpandable(value: JsonValue): boolean {
  return value !== null && (Array.isArray(value) || typeof value === "object");
}

function getEntries(value: JsonValue): ReadonlyArray<readonly [string, JsonValue]> {
  if (Array.isArray(value)) {
    return value.map((item, index): readonly [string, JsonValue] => [String(index), item]);
  }
  if (typeof value === "object" && value !== null) {
    return Object.entries(value);
  }
  return [];
}

const STRING_TRUNCATE_LIMIT = 120;

function StringValue({ text }: { text: string }): JSX.Element {
  const [expanded, setExpanded] = useState(false);
  const isLong = text.length > STRING_TRUNCATE_LIMIT;

  if (!isLong) {
    return (
      <span className="text-emerald-400 break-all">
        &quot;
        <span className="prose prose-sm dark:prose-invert inline max-w-none [&_p]:inline [&_p]:my-0 [&_code]:text-emerald-300 [&_a]:text-emerald-300">
          <ReactMarkdown>{text}</ReactMarkdown>
        </span>
        &quot;
      </span>
    );
  }

  return (
    <span className="text-emerald-400 break-all">
      &quot;
      {expanded ? (
        <span
          className="cursor-pointer prose prose-sm dark:prose-invert inline max-w-none [&_p]:inline [&_p]:my-0 [&_code]:text-emerald-300 [&_a]:text-emerald-300"
          onClick={(e) => {
            e.stopPropagation();
            setExpanded(false);
          }}
          onKeyDown={(e) => {
            if (e.key === "Enter" || e.key === " ") {
              e.stopPropagation();
              setExpanded(false);
            }
          }}
          role="button"
          tabIndex={0}
        >
          <ReactMarkdown>{text}</ReactMarkdown>
        </span>
      ) : (
        <Tooltip delayDuration={300}>
          <TooltipTrigger
            onClick={(e) => {
              e.stopPropagation();
              setExpanded(true);
            }}
            className="text-left cursor-pointer"
          >
            <span>{text.slice(0, STRING_TRUNCATE_LIMIT)}</span>
            <span className="text-emerald-400/50">…</span>
          </TooltipTrigger>
          <TooltipContent
            side="bottom"
            className="max-w-md text-xs p-2 break-words whitespace-pre-wrap"
          >
            {text.slice(0, 500)}
            {text.length > 500 ? "…" : ""}
          </TooltipContent>
        </Tooltip>
      )}
      &quot;
    </span>
  );
}

function PrimitiveValue({ value }: { value: JsonValue }): JSX.Element {
  if (value === null) {
    return <span className="text-rose-400 italic">null</span>;
  }

  switch (typeof value) {
    case "string":
      return <StringValue text={value} />;
    case "number":
      return <span className="text-amber-400">{value}</span>;
    case "boolean":
      return <span className="text-blue-400">{value ? "true" : "false"}</span>;
    case "object":
    case "bigint":
    case "symbol":
    case "undefined":
    case "function":
      return <span className="text-muted-foreground">{JSON.stringify(value)}</span>;
  }
}

function CopyValueButton({ value }: { value: JsonValue }): JSX.Element {
  const [copied, setCopied] = useState(false);

  function handleCopy(e: React.MouseEvent): void {
    e.stopPropagation();
    void window.navigator.clipboard.writeText(JSON.stringify(value, null, 2)).then(() => {
      setCopied(true);
      setTimeout(() => {
        setCopied(false);
      }, 2000);
    });
  }

  return (
    <button
      type="button"
      onClick={handleCopy}
      className="inline-flex items-center gap-1 text-[10px] text-muted-foreground hover:text-foreground transition-colors"
      title="Copy JSON"
    >
      {copied ? <Check className="size-3 text-green-500" /> : <Copy className="size-3" />}
    </button>
  );
}

function CopyButton({ value }: { value: JsonValue }): JSX.Element {
  const [copied, setCopied] = useState(false);

  function handleCopy(e: React.MouseEvent): void {
    e.stopPropagation();
    void window.navigator.clipboard.writeText(JSON.stringify(value, null, 2)).then(() => {
      setCopied(true);
      setTimeout(() => {
        setCopied(false);
      }, 2000);
    });
  }

  return (
    <button
      type="button"
      onClick={handleCopy}
      className="opacity-0 group-hover/row:opacity-100 hover:bg-muted p-0.5 rounded transition-opacity shrink-0"
      title="Copy to clipboard"
    >
      {copied ? (
        <Check className="size-3 text-green-500" />
      ) : (
        <Copy className="size-3 text-muted-foreground" />
      )}
    </button>
  );
}

type JsonNodeProps = {
  name: string;
  value: JsonValue;
  level: number;
  defaultExpandDepth: number;
  isArrayItem: boolean;
  /** Full JSON-pointer-style path to this node (e.g. "/messages/3"). */
  path: string;
  /** When set, ancestors of this path auto-expand for one render. */
  expandTargetPath: string | null;
  /** Set of paths that should expose `data-anatomy-path` on their row. */
  anatomyPaths: Set<string> | null;
};

function ExpandCollapseButton({
  onClick,
}: {
  onClick: (e: React.MouseEvent) => void;
}): JSX.Element {
  return (
    <button
      type="button"
      onClick={onClick}
      className="opacity-0 group-hover/row:opacity-100 hover:bg-muted p-0.5 rounded transition-opacity shrink-0"
      title="Expand all descendants"
    >
      <ChevronsDown className="size-3.5 text-muted-foreground" />
    </button>
  );
}

const JsonNode = memo(function JsonNode({
  name,
  value,
  level,
  defaultExpandDepth,
  isArrayItem,
  path,
  expandTargetPath,
  anatomyPaths,
}: JsonNodeProps): JSX.Element {
  // A node is an ancestor of the target if the target starts with this
  // node's path followed by a path separator. The root path is "" and
  // is an ancestor of every non-empty path.
  const isAncestorOfTarget = useMemo(() => {
    if (expandTargetPath === null) return false;
    if (path === "") return expandTargetPath.length > 0;
    return expandTargetPath === path || expandTargetPath.startsWith(`${path}/`);
  }, [expandTargetPath, path]);

  const initialExpanded = level < defaultExpandDepth || (isAncestorOfTarget && isExpandable(value));

  const [expanded, setExpanded] = useState(initialExpanded);
  // Re-evaluate on every render so a freshly-changed expandTargetPath
  // immediately auto-expands a previously-collapsed ancestor.
  useEffect(() => {
    if (isAncestorOfTarget && isExpandable(value) && !expanded) {
      setExpanded(true);
    }
    // We deliberately only react to expandTargetPath changes here.
  }, [expandTargetPath]);

  const [childResetKey, setChildResetKey] = useState(0);
  const [childDepthOverride, setChildDepthOverride] = useState<number | null>(null);
  const expandable = isExpandable(value);
  const fullyExpanded = childDepthOverride === Number.POSITIVE_INFINITY;
  const entries = useMemo(() => getEntries(value), [value]);
  const hasExpandableChild = useMemo(
    () => entries.some(([, child]) => isExpandable(child)),
    [entries],
  );

  const dataType = classifyValue(value);
  const openBracket = dataType === "array" ? "[" : "{";
  const closeBracket = dataType === "array" ? "]" : "}";

  const hasAnatomyPath = anatomyPaths !== null && anatomyPaths.has(path);

  function expandAllDeep(): void {
    setExpanded(true);
    setChildDepthOverride(Number.POSITIVE_INFINITY);
    setChildResetKey((k) => k + 1);
  }

  function collapseToDefault(): void {
    setExpanded(false);
    setChildDepthOverride(0);
    setChildResetKey((k) => k + 1);
  }

  function handleRowToggle(): void {
    if (fullyExpanded) {
      collapseToDefault();
    } else {
      setExpanded(!expanded);
    }
  }

  function handleExpandAll(e: React.MouseEvent): void {
    e.stopPropagation();
    expandAllDeep();
  }

  const effectiveChildDepth = childDepthOverride ?? defaultExpandDepth;

  return (
    <div className={cn(level > 0 && "border-l border-border/50 ml-2")}>
      <div
        className={cn(
          "flex items-start gap-1 py-0.5 px-1 -ml-1 rounded-sm group/row",
          expandable && "cursor-pointer hover:bg-muted/50",
        )}
        data-anatomy-path={hasAnatomyPath ? path : undefined}
        onClick={expandable ? handleRowToggle : undefined}
        onKeyDown={
          expandable
            ? (e) => {
                if (e.key === "Enter" || e.key === " ") {
                  e.preventDefault();
                  handleRowToggle();
                }
              }
            : undefined
        }
        onDoubleClick={expandable && hasExpandableChild ? () => expandAllDeep() : undefined}
        role={expandable ? "button" : undefined}
        tabIndex={expandable ? 0 : undefined}
      >
        {expandable ? (
          <span className="w-4 h-5 flex items-center justify-center shrink-0">
            {expanded ? (
              <ChevronDown className="size-3 text-muted-foreground" />
            ) : (
              <ChevronRight className="size-3 text-muted-foreground" />
            )}
          </span>
        ) : (
          <span className="w-4 shrink-0" />
        )}

        <span className={cn("shrink-0", isArrayItem ? "text-muted-foreground" : "text-cyan-400")}>
          {isArrayItem ? name : `"${name}"`}
        </span>

        <span className="text-muted-foreground shrink-0">{expandable ? "" : ":"}</span>

        {expandable ? (
          <span className="text-muted-foreground">
            {openBracket}
            <span className="text-muted-foreground/60 text-xs">
              {" "}
              {entries.length} {entries.length === 1 ? "item" : "items"} {closeBracket}
            </span>
          </span>
        ) : (
          <span className="min-w-0">
            <PrimitiveValue value={value} />
          </span>
        )}

        {expandable && hasExpandableChild && !fullyExpanded && (
          <ExpandCollapseButton onClick={handleExpandAll} />
        )}
        <CopyButton value={value} />
      </div>

      {expandable && expanded && (
        <div className="pl-4" key={childResetKey}>
          {entries.map(([key, childValue]) => (
            <JsonNode
              key={key}
              name={key}
              value={childValue}
              level={level + 1}
              defaultExpandDepth={effectiveChildDepth}
              isArrayItem={dataType === "array"}
              path={path === "" ? `/${key}` : `${path}/${key}`}
              expandTargetPath={expandTargetPath}
              anatomyPaths={anatomyPaths}
            />
          ))}
          <div className="text-muted-foreground py-0.5 px-1">{closeBracket}</div>
        </div>
      )}
    </div>
  );
});

export type JsonViewerProps = {
  data: JsonValue;
  defaultExpandDepth?: number;
  className?: string;
  showCopy?: boolean;
  bulkDepth?: number;
  bulkRevision?: number;
  /**
   * Set of JSON-pointer-style paths whose row should expose a
   * `data-anatomy-path` attribute. Used by the Anatomy view to
   * locate rows for click-to-jump. When `null` or `undefined`,
   * no `data-anatomy-path` attribute is emitted (zero cost).
   */
  anatomyPaths?: Set<string> | null;
  /**
   * When set, every ancestor of this path in the tree auto-expands
   * for one render so the target row becomes visible. The hook
   * flips this back to `null` after the scroll lands.
   */
  expandToPath?: string | null;
};

export function JsonViewer({
  data,
  defaultExpandDepth = 0,
  className,
  showCopy = false,
  bulkDepth: controlledBulkDepth,
  bulkRevision: controlledBulkRevision,
  anatomyPaths = null,
  expandToPath = null,
}: JsonViewerProps): JSX.Element {
  const expandable = isExpandable(data);
  const entries = useMemo(() => getEntries(data), [data]);

  const bulkDepth = controlledBulkDepth ?? defaultExpandDepth;
  const bulkRevision = controlledBulkRevision ?? 0;

  if (!expandable) {
    return (
      <TooltipProvider>
        <div className={cn("font-mono text-xs leading-relaxed", className)}>
          <div className="flex items-center gap-1">
            <PrimitiveValue value={data} />
            {showCopy && <CopyValueButton value={data} />}
          </div>
        </div>
      </TooltipProvider>
    );
  }

  const dataType = classifyValue(data);
  const isArray = dataType === "array";

  return (
    <TooltipProvider>
      <div className={cn("font-mono text-xs leading-relaxed", className)}>
        {showCopy && (
          <div className="mb-2 flex items-center justify-end gap-2">
            <CopyValueButton value={data} />
          </div>
        )}
        <div key={bulkRevision}>
          {entries.map(([key, childValue]) => (
            <JsonNode
              key={key}
              name={key}
              value={childValue}
              level={0}
              defaultExpandDepth={bulkDepth}
              isArrayItem={isArray}
              path={`/${key}`}
              expandTargetPath={expandToPath}
              anatomyPaths={anatomyPaths}
            />
          ))}
        </div>
      </div>
    </TooltipProvider>
  );
}

export type JsonViewerFromStringProps = {
  text: string;
  defaultExpandDepth?: number;
  className?: string;
};

export const JsonViewerFromString = memo(function JsonViewerFromString({
  text,
  defaultExpandDepth = 0,
  className,
}: JsonViewerFromStringProps): JSX.Element {
  const parsed = useMemo(() => parseJsonText(text), [text]);

  if (parsed.kind === "json") {
    return (
      <JsonViewer
        data={parsed.data}
        defaultExpandDepth={defaultExpandDepth}
        className={className}
      />
    );
  }

  return (
    <pre className={cn("font-mono text-xs whitespace-pre-wrap break-words", className)}>{text}</pre>
  );
});
