'use client';

import type {
  FileResponse,
  FileTreeNode,
} from '@/components/agent-workspace/types';
import { useCallback, useEffect, useState } from 'react';
import { CodeEditor } from '@/components/agent-workspace/code-editor';
import { FileTabs } from '@/components/agent-workspace/file-tabs';
import { FileTree } from '@/components/agent-workspace/file-tree';
import { XtermSession } from '@/components/agent-workspace/xterm-session';
import { useQuery } from 'convex/react';
import { FolderTree, SquareTerminal } from 'lucide-react';
import { toast } from 'sonner';

import { api } from '@spoon/backend/convex/_generated/api.js';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@spoon/ui';

import type { BoxStatus, LifecycleAction } from './box-status-card';
import { BoxStatusCard } from './box-status-card';
import { BoxUserCard } from './box-user-card';

const STATUS_POLL_MS = 10_000;

type OpenFileState = {
  path: string;
  content: string;
  savedContent: string;
  loading: boolean;
  saving: boolean;
};

export const MachineShell = () => {
  const environment = useQuery(api.userEnvironment.getMine, {});
  const username = environment?.username ?? '';
  const firstName = environment?.firstName;

  const [status, setStatus] = useState<BoxStatus | null | undefined>(undefined);
  const [pendingAction, setPendingAction] = useState<LifecycleAction | null>(
    null,
  );
  const [tree, setTree] = useState<FileTreeNode | null>(null);
  const [files, setFiles] = useState<Record<string, OpenFileState>>({});
  const [openFilePaths, setOpenFilePaths] = useState<string[]>([]);
  const [activeFilePath, setActiveFilePath] = useState<string>();
  const [expandedDirectoryPaths, setExpandedDirectoryPaths] = useState<
    string[]
  >([]);
  const [vimEnabled, setVimEnabled] = useState(false);
  const [activeTab, setActiveTab] = useState<'files' | 'terminal'>('files');

  const refreshStatus = useCallback(async () => {
    try {
      const response = await fetch('/api/box/status');
      if (!response.ok) throw new Error(await response.text());
      const data = (await response.json()) as { status: BoxStatus };
      setStatus(data.status);
    } catch (error) {
      console.error(error);
      setStatus((current) => current ?? null);
    }
  }, []);

  const loadTree = useCallback(async () => {
    try {
      const response = await fetch('/api/box/tree');
      if (!response.ok) throw new Error(await response.text());
      const data = (await response.json()) as { tree: FileTreeNode | null };
      setTree(data.tree);
    } catch (error) {
      console.error(error);
    }
  }, []);

  useEffect(() => {
    void refreshStatus();
    void loadTree();
    const interval = window.setInterval(() => {
      void refreshStatus();
    }, STATUS_POLL_MS);
    return () => window.clearInterval(interval);
  }, [refreshStatus, loadTree]);

  const runLifecycle = useCallback(
    (action: LifecycleAction) => {
      if (pendingAction) return;
      setPendingAction(action);
      void (async () => {
        try {
          const response = await fetch('/api/box/lifecycle', {
            method: 'POST',
            body: JSON.stringify({ action }),
          });
          if (!response.ok) throw new Error(await response.text());
          const data = (await response.json()) as { status: BoxStatus };
          setStatus(data.status);
          toast.success(
            action === 'stop'
              ? 'Machine stopped.'
              : action === 'restart'
                ? 'Machine restarted.'
                : 'Machine started.',
          );
          void loadTree();
        } catch (error) {
          console.error(error);
          toast.error(`Could not ${action} your machine.`);
          void refreshStatus();
        } finally {
          setPendingAction(null);
        }
      })();
    },
    [pendingAction, loadTree, refreshStatus],
  );

  const loadFile = useCallback(async (path: string) => {
    setFiles((current) => ({
      ...current,
      [path]: current[path] ?? {
        path,
        content: '',
        savedContent: '',
        loading: true,
        saving: false,
      },
    }));
    const response = await fetch(
      `/api/box/file?path=${encodeURIComponent(path)}`,
    );
    if (!response.ok) throw new Error(await response.text());
    const data = (await response.json()) as FileResponse;
    setFiles((current) => ({
      ...current,
      [data.path]: {
        path: data.path,
        content: data.content,
        savedContent: data.content,
        loading: false,
        saving: false,
      },
    }));
  }, []);

  const openFile = useCallback(
    (path: string) => {
      setOpenFilePaths((current) =>
        current.includes(path) ? current : [...current, path],
      );
      setActiveFilePath(path);
      if (!files[path]) {
        void loadFile(path).catch((error) => {
          console.error(error);
          setFiles((current) => {
            const next = { ...current };
            delete next[path];
            return next;
          });
          setOpenFilePaths((current) =>
            current.filter((filePath) => filePath !== path),
          );
          toast.error('Could not load file.');
        });
      }
    },
    [files, loadFile],
  );

  const writeFileContent = async (path: string, content: string) => {
    setFiles((current) => ({
      ...current,
      [path]: {
        ...(current[path] ?? { path, savedContent: '', loading: false }),
        content,
        saving: true,
      },
    }));
    const response = await fetch('/api/box/file', {
      method: 'PUT',
      body: JSON.stringify({ path, content }),
    });
    if (!response.ok) {
      setFiles((current) => ({
        ...current,
        [path]: {
          ...(current[path] ?? {
            path,
            content,
            savedContent: '',
            loading: false,
          }),
          saving: false,
        },
      }));
      toast.error('Could not save file.');
      throw new Error(await response.text());
    }
    setFiles((current) => ({
      ...current,
      [path]: {
        ...(current[path] ?? { path, loading: false }),
        content,
        savedContent: content,
        saving: false,
      },
    }));
    toast.success('File saved.');
  };

  const saveFile = async (content: string) => {
    if (!activeFilePath) return;
    await writeFileContent(activeFilePath, content);
  };

  const closeFile = (path: string) => {
    const index = openFilePaths.indexOf(path);
    const nextOpen = openFilePaths.filter((filePath) => filePath !== path);
    setOpenFilePaths(nextOpen);
    setFiles((current) => {
      const next = { ...current };
      delete next[path];
      return next;
    });
    if (activeFilePath === path) {
      setActiveFilePath(nextOpen[index - 1] ?? nextOpen[index] ?? undefined);
    }
  };

  const toggleDirectory = (path: string) => {
    setExpandedDirectoryPaths((current) =>
      current.includes(path)
        ? current.filter((directoryPath) => directoryPath !== path)
        : [...current, path],
    );
  };

  const activeFile = activeFilePath ? files[activeFilePath] : undefined;
  const running = Boolean(status?.running);
  const starting = pendingAction === 'start' || pendingAction === 'restart';
  // The terminal only shows this while the box is not running. Distinguish a box
  // that is starting/restarting from one that is simply stopped so the label is
  // accurate instead of always claiming "Starting…".
  const terminalWaitingLabel = starting
    ? 'Starting your machine…'
    : 'Machine is stopped — start it to open a terminal.';

  return (
    <main className='space-y-6'>
      <div>
        <h1 className='text-3xl font-semibold tracking-normal'>
          {firstName ? `${firstName}'s Machine` : 'My Machine'}
        </h1>
        <p className='text-muted-foreground mt-2'>
          Your always-available dev box — a persistent home, a terminal, and a
          file browser rooted at <code className='font-mono'>~</code>.
        </p>
      </div>

      <BoxStatusCard
        status={status}
        pendingAction={pendingAction}
        onAction={runLifecycle}
      />

      <BoxUserCard username={username} />

      <Tabs
        value={activeTab}
        onValueChange={(value) => setActiveTab(value as 'files' | 'terminal')}
        className='border-border bg-muted/20 flex h-[calc(100vh-8.5rem)] min-h-[600px] flex-col overflow-hidden rounded-md border'
      >
        <TabsList className='border-border bg-muted/30 h-12 flex-none justify-start rounded-none border-b px-3'>
          <TabsTrigger
            value='files'
            className='data-active:bg-background data-active:text-foreground data-active:shadow-sm'
          >
            <FolderTree className='size-4' />
            Files
          </TabsTrigger>
          <TabsTrigger
            value='terminal'
            className='data-active:bg-background data-active:text-foreground data-active:shadow-sm'
          >
            <SquareTerminal className='size-4' />
            Terminal
          </TabsTrigger>
        </TabsList>
        <TabsContent
          value='files'
          className='m-0 grid min-h-0 flex-1 grid-cols-1 lg:grid-cols-[280px_minmax(0,1fr)]'
        >
          <aside className='border-border bg-background min-h-0 border-r'>
            <div className='border-border border-b p-3'>
              <h2 className='text-sm font-semibold'>Home</h2>
              <p className='text-muted-foreground text-xs'>
                Files in your persistent home
              </p>
            </div>
            <FileTree
              tree={tree}
              selectedPath={activeFilePath}
              expandedPaths={expandedDirectoryPaths}
              onSelect={openFile}
              onToggleDirectory={toggleDirectory}
            />
          </aside>
          <section className='bg-background flex min-w-0 flex-col overflow-hidden'>
            <FileTabs
              tabs={openFilePaths.map((path) => ({
                path,
                dirty: files[path]
                  ? files[path].content !== files[path].savedContent
                  : false,
              }))}
              activePath={activeFilePath}
              onActivate={setActiveFilePath}
              onClose={closeFile}
            />
            <CodeEditor
              path={activeFilePath}
              content={activeFile?.content ?? ''}
              savedContent={activeFile?.savedContent ?? ''}
              readOnly={false}
              vimEnabled={vimEnabled}
              onSave={saveFile}
              onVimEnabledChange={setVimEnabled}
              onChange={(content) => {
                if (!activeFilePath) return;
                setFiles((current) => ({
                  ...current,
                  [activeFilePath]: {
                    ...(current[activeFilePath] ?? {
                      path: activeFilePath,
                      savedContent: '',
                      loading: false,
                      saving: false,
                    }),
                    content,
                  },
                }));
              }}
            />
          </section>
        </TabsContent>
        <TabsContent
          value='terminal'
          forceMount
          className='m-0 min-h-0 flex-1 overflow-hidden data-[state=inactive]:hidden'
        >
          <XtermSession
            active={running}
            visible={activeTab === 'terminal'}
            tokenUrl='/api/box/terminal-token'
            sessionKey={username}
            waitingLabel={terminalWaitingLabel}
            label='your machine'
          />
        </TabsContent>
      </Tabs>
    </main>
  );
};
