"use client";

/**
 * Lazy-expanding file tree for a Gitea repo path.
 *
 * Wraps `GET /api/projects/[projectId]/file?path=…` which returns a
 * directory listing. Directories expand inline on click and lazy-load
 * their children; files render as leaves and (for now) link out to
 * Gitea's web UI on click.
 */

import { useEffect, useState, useCallback } from "react";
import { ChevronRight, ChevronDown, Folder, FileText, Loader2, AlertCircle } from "lucide-react";

interface TreeItem {
  name: string;
  path: string;
  type: "file" | "dir" | "symlink";
  size?: number;
}

interface ApiOk {
  type: "dir";
  items: TreeItem[];
}

interface GiteaFileTreeProps {
  projectId: string;
  /** Repo path to root the tree at, e.g. "apps/web" */
  rootPath: string;
  /** Fires when the user clicks a file row. When omitted, files
   *  are not interactive. */
  onSelectFile?: (path: string) => void;
  /** Path of the currently-selected file, used to highlight the row. */
  selectedPath?: string;
}

export function GiteaFileTree({
  projectId,
  rootPath,
  onSelectFile,
  selectedPath,
}: GiteaFileTreeProps) {
  const [rootItems, setRootItems] = useState<TreeItem[] | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [expanded, setExpanded] = useState<Set<string>>(new Set());
  const [childrenByPath, setChildrenByPath] = useState<Record<string, TreeItem[]>>({});
  const [loadingPaths, setLoadingPaths] = useState<Set<string>>(new Set());

  const fetchPath = useCallback(
    async (path: string): Promise<TreeItem[]> => {
      const res = await fetch(
        `/api/projects/${projectId}/file?path=${encodeURIComponent(path)}`,
        { credentials: "include" }
      );
      if (!res.ok) {
        const body = await res.json().catch(() => ({}));
        throw new Error(body.error || `HTTP ${res.status}`);
      }
      const data = (await res.json()) as ApiOk;
      return data.items ?? [];
    },
    [projectId]
  );

  // Load root whenever projectId or rootPath changes
  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    setError(null);
    setRootItems(null);
    setExpanded(new Set());
    setChildrenByPath({});

    fetchPath(rootPath)
      .then(items => {
        if (!cancelled) setRootItems(items);
      })
      .catch(err => {
        if (!cancelled) setError(err.message || "Failed to load");
      })
      .finally(() => {
        if (!cancelled) setLoading(false);
      });

    return () => {
      cancelled = true;
    };
  }, [projectId, rootPath, fetchPath]);

  const toggleDir = useCallback(
    async (path: string) => {
      const isOpen = expanded.has(path);
      const next = new Set(expanded);
      if (isOpen) {
        next.delete(path);
        setExpanded(next);
        return;
      }
      next.add(path);
      setExpanded(next);
      if (childrenByPath[path]) return;

      setLoadingPaths(prev => new Set(prev).add(path));
      try {
        const items = await fetchPath(path);
        setChildrenByPath(prev => ({ ...prev, [path]: items }));
      } catch (err) {
        console.warn(`[gitea-file-tree] failed to load ${path}:`, err);
      } finally {
        setLoadingPaths(prev => {
          const n = new Set(prev);
          n.delete(path);
          return n;
        });
      }
    },
    [expanded, childrenByPath, fetchPath]
  );

  if (loading) {
    return (
      <div style={msgWrap}>
        <Loader2 size={14} className="animate-spin" style={{ color: INK.muted }} />
        <span style={msgText}>Loading…</span>
      </div>
    );
  }

  if (error) {
    const isMissingRepo = /no gitea repo/i.test(error);
    return (
      <div style={msgWrap}>
        <AlertCircle size={14} style={{ color: INK.muted }} />
        <span style={msgText}>
          {isMissingRepo ? "No Gitea repo connected to this project." : error}
        </span>
      </div>
    );
  }

  if (!rootItems || rootItems.length === 0) {
    return (
      <div style={msgWrap}>
        <span style={msgText}>
          <code style={inlineCode}>{rootPath}</code> is empty (or doesn't exist yet).
        </span>
      </div>
    );
  }

  return (
    <div style={treeWrap}>
      {rootItems.map(item => (
        <Node
          key={item.path}
          item={item}
          depth={0}
          expanded={expanded}
          loadingPaths={loadingPaths}
          childrenByPath={childrenByPath}
          onToggle={toggleDir}
          onSelectFile={onSelectFile}
          selectedPath={selectedPath}
        />
      ))}
    </div>
  );
}

interface NodeProps {
  item: TreeItem;
  depth: number;
  expanded: Set<string>;
  loadingPaths: Set<string>;
  childrenByPath: Record<string, TreeItem[]>;
  onToggle: (path: string) => void;
  onSelectFile?: (path: string) => void;
  selectedPath?: string;
}

function Node({
  item,
  depth,
  expanded,
  loadingPaths,
  childrenByPath,
  onToggle,
  onSelectFile,
  selectedPath,
}: NodeProps) {
  const isDir = item.type === "dir";
  const isOpen = expanded.has(item.path);
  const isLoading = loadingPaths.has(item.path);
  const children = childrenByPath[item.path];
  const isSelected = !isDir && selectedPath === item.path;
  const fileClickable = !isDir && typeof onSelectFile === "function";

  const Icon = isDir
    ? isOpen
      ? ChevronDown
      : ChevronRight
    : null;

  const indent = depth * 14;

  const interactive = isDir || fileClickable;
  const handleClick = () => {
    if (isDir) onToggle(item.path);
    else if (fileClickable) onSelectFile!(item.path);
  };

  return (
    <>
      <div
        style={{
          ...rowStyle,
          paddingLeft: 6 + indent,
          cursor: interactive ? "pointer" : "default",
          background: isSelected ? "rgba(26,26,26,0.06)" : "transparent",
          color: isSelected ? INK.ink : undefined,
          fontWeight: isSelected ? 600 : 400,
        }}
        onClick={interactive ? handleClick : undefined}
        role={interactive ? "button" : undefined}
        aria-expanded={isDir ? isOpen : undefined}
        aria-current={isSelected ? "true" : undefined}
      >
        <span style={chevronCell}>
          {Icon && <Icon size={12} style={{ color: INK.mid }} />}
        </span>
        {isDir ? (
          <Folder size={13} style={{ color: INK.stone, flexShrink: 0 }} />
        ) : (
          <FileText size={13} style={{ color: isSelected ? INK.ink : INK.muted, flexShrink: 0 }} />
        )}
        <span style={nameStyle}>{item.name}</span>
        {isLoading && (
          <Loader2 size={11} className="animate-spin" style={{ color: INK.muted, marginLeft: "auto" }} />
        )}
      </div>
      {isDir && isOpen && children?.map(child => (
        <Node
          key={child.path}
          item={child}
          depth={depth + 1}
          expanded={expanded}
          loadingPaths={loadingPaths}
          childrenByPath={childrenByPath}
          onToggle={onToggle}
          onSelectFile={onSelectFile}
          selectedPath={selectedPath}
        />
      ))}
    </>
  );
}

const INK = {
  ink: "#1a1a1a",
  mid: "#5f5e5a",
  muted: "#a09a90",
  stone: "#b5b0a6",
  border: "#e8e4dc",
} as const;

const treeWrap: React.CSSProperties = {
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
  fontSize: "0.78rem",
  color: INK.ink,
  flex: 1,
  minHeight: 0,
  overflowY: "auto",
  margin: "-4px -8px",
};

const rowStyle: React.CSSProperties = {
  display: "flex",
  alignItems: "center",
  gap: 6,
  padding: "3px 8px",
  lineHeight: 1.4,
  borderRadius: 4,
  userSelect: "none",
};

const chevronCell: React.CSSProperties = {
  width: 12,
  display: "inline-flex",
  alignItems: "center",
  justifyContent: "center",
  flexShrink: 0,
};

const nameStyle: React.CSSProperties = {
  whiteSpace: "nowrap",
  overflow: "hidden",
  textOverflow: "ellipsis",
  minWidth: 0,
};

const msgWrap: React.CSSProperties = {
  display: "flex",
  alignItems: "center",
  gap: 8,
  padding: "16px 4px",
};

const msgText: React.CSSProperties = {
  fontSize: "0.82rem",
  color: INK.mid,
  lineHeight: 1.5,
};

const inlineCode: React.CSSProperties = {
  fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
  fontSize: "0.78rem",
  background: "rgba(0,0,0,0.04)",
  padding: "1px 6px",
  borderRadius: 4,
};
