'use client';

import type { FileTreeNode } from '@/components/agent-workspace/types';
import { useEffect, useMemo, useRef, useState } from 'react';
import { CodeEditor } from '@/components/agent-workspace/code-editor';
import { FileTree } from '@/components/agent-workspace/file-tree';
import { useAction, useMutation, useQuery } from 'convex/react';
import { FilePlus, FolderUp, Trash2, Upload } from 'lucide-react';
import { toast } from 'sonner';

import { api } from '@spoon/backend/convex/_generated/api.js';
import { Button, Card, Input, Label } from '@spoon/ui';

type DotfileMeta = {
  _id: string;
  path: string;
  size: number;
  isExecutable: boolean;
  updatedAt: number;
};

type UploadFile = { path: string; content: string; isExecutable?: boolean };

// Minimal typed surface of the drag-and-drop FileSystem entry API.
type FsEntry = {
  isFile: boolean;
  isDirectory: boolean;
  name: string;
  file?: (cb: (f: File) => void, err: (e: unknown) => void) => void;
  createReader?: () => {
    readEntries: (
      cb: (e: FsEntry[]) => void,
      err: (e: unknown) => void,
    ) => void;
  };
};

const buildTree = (files: DotfileMeta[], rootLabel: string): FileTreeNode => {
  const root: FileTreeNode = {
    name: rootLabel,
    path: '',
    type: 'directory',
    children: [],
  };
  for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
    const segments = file.path.split('/');
    let node = root;
    segments.forEach((segment, index) => {
      const isLeaf = index === segments.length - 1;
      const childPath = segments.slice(0, index + 1).join('/');
      node.children ??= [];
      let child = node.children.find((c) => c.path === childPath);
      if (!child) {
        child = {
          name: segment,
          path: childPath,
          type: isLeaf ? 'file' : 'directory',
          children: isLeaf ? undefined : [],
        };
        node.children.push(child);
      }
      node = child;
    });
  }
  return root;
};

const readAllEntries = (reader: {
  readEntries: (cb: (e: FsEntry[]) => void, err: (e: unknown) => void) => void;
}) =>
  new Promise<FsEntry[]>((resolve, reject) => {
    const all: FsEntry[] = [];
    const next = () =>
      reader.readEntries((batch) => {
        if (batch.length === 0) resolve(all);
        else {
          all.push(...batch);
          next();
        }
      }, reject);
    next();
  });

const collectEntry = async (
  entry: FsEntry,
  prefix: string,
  out: UploadFile[],
) => {
  if (entry.isFile && entry.file) {
    const file = await new Promise<File>((res, rej) => entry.file?.(res, rej));
    out.push({ path: `${prefix}${entry.name}`, content: await file.text() });
  } else if (entry.isDirectory && entry.createReader) {
    const entries = await readAllEntries(entry.createReader());
    for (const child of entries) {
      await collectEntry(child, `${prefix}${entry.name}/`, out);
    }
  }
};

export const DotfilesManager = () => {
  const settings = useQuery(api.userEnvironment.getMine);
  const filesQuery = useQuery(api.userDotfiles.listMine);
  const files = useMemo(
    () => (filesQuery ?? []) as DotfileMeta[],
    [filesQuery],
  );
  const getFileContent = useAction(api.userDotfilesNode.getFileContent);
  const putFile = useAction(api.userDotfilesNode.putFile);
  const importFiles = useAction(api.userDotfilesNode.importFiles);
  const removeFile = useMutation(api.userDotfiles.remove);
  const updateEnv = useMutation(api.userEnvironment.updateMine);

  const [selected, setSelected] = useState<DotfileMeta>();
  const [content, setContent] = useState('');
  const [savedContent, setSavedContent] = useState('');
  const [expandedOverride, setExpandedOverride] = useState<string[] | null>(
    null,
  );
  const [dragOver, setDragOver] = useState(false);
  const folderInputRef = useRef<HTMLInputElement>(null);
  const filesInputRef = useRef<HTMLInputElement>(null);

  const firstName = settings?.firstName ?? 'you';
  const tree = useMemo(
    () => buildTree(files, `home/${firstName}`),
    [files, firstName],
  );

  // Directories default to expanded; once the user toggles, their choice wins.
  const allDirs = useMemo(
    () =>
      files
        .flatMap((f) => {
          const segs = f.path.split('/');
          return segs
            .slice(0, -1)
            .map((_, i) => segs.slice(0, i + 1).join('/'));
        })
        .filter((v, i, a) => a.indexOf(v) === i),
    [files],
  );
  const expanded = expandedOverride ?? allDirs;

  const openFile = async (path: string) => {
    const file = files.find((f) => f.path === path);
    if (!file) return; // directory
    setSelected(file);
    setContent('');
    setSavedContent('');
    try {
      const { content: text } = await getFileContent({
        fileId: file._id as never,
      });
      setContent(text);
      setSavedContent(text);
    } catch {
      toast.error('Could not open file.');
    }
  };

  const saveSelected = async (next: string) => {
    if (!selected) return;
    await putFile({
      path: selected.path,
      content: next,
      isExecutable: selected.isExecutable,
    });
    setSavedContent(next);
    toast.success('Saved.');
  };

  const importAll = async (incoming: UploadFile[]) => {
    const valid = incoming.filter((f) => f.path.trim());
    if (valid.length === 0) return;
    try {
      await importFiles({ files: valid });
      toast.success(`Imported ${valid.length} file(s).`);
    } catch (error) {
      toast.error(error instanceof Error ? error.message : 'Import failed.');
    }
  };

  const onDrop = async (event: React.DragEvent) => {
    event.preventDefault();
    setDragOver(false);
    const out: UploadFile[] = [];
    const entries: FsEntry[] = [];
    for (const item of Array.from(event.dataTransfer.items)) {
      const entry = item.webkitGetAsEntry() as FsEntry | null;
      if (entry) entries.push(entry);
    }
    if (entries.length > 0) {
      for (const entry of entries) await collectEntry(entry, '', out);
    } else {
      for (const file of Array.from(event.dataTransfer.files)) {
        out.push({ path: file.name, content: await file.text() });
      }
    }
    await importAll(out);
  };

  const onPickFiles = async (
    event: React.ChangeEvent<HTMLInputElement>,
    stripFirstSegment: boolean,
  ) => {
    const picked = Array.from(event.target.files ?? []);
    const out: UploadFile[] = [];
    for (const file of picked) {
      const relative =
        (file as File & { webkitRelativePath?: string }).webkitRelativePath ||
        file.name;
      const path = stripFirstSegment
        ? relative.split('/').slice(1).join('/')
        : relative;
      out.push({ path, content: await file.text() });
    }
    event.target.value = '';
    await importAll(out);
  };

  const newFile = async () => {
    const path = window.prompt('New file path (relative to home):', '.bashrc');
    if (!path?.trim()) return;
    try {
      await putFile({ path: path.trim(), content: '' });
      toast.success('Created.');
    } catch (error) {
      toast.error(error instanceof Error ? error.message : 'Could not create.');
    }
  };

  const deleteSelected = async () => {
    if (!selected) return;
    await removeFile({ fileId: selected._id as never });
    setSelected(undefined);
    setContent('');
    setSavedContent('');
    toast.success('Deleted.');
  };

  return (
    <div className='space-y-4'>
      <Card className='gap-0 overflow-hidden p-0 shadow-none'>
        <div className='border-border flex flex-wrap items-center gap-2 border-b p-2'>
          <Button type='button' variant='outline' size='sm' onClick={newFile}>
            <FilePlus className='size-4' /> New file
          </Button>
          <Button
            type='button'
            variant='outline'
            size='sm'
            onClick={() => folderInputRef.current?.click()}
          >
            <FolderUp className='size-4' /> Upload folder
          </Button>
          <Button
            type='button'
            variant='outline'
            size='sm'
            onClick={() => filesInputRef.current?.click()}
          >
            <Upload className='size-4' /> Upload files
          </Button>
          {selected ? (
            <Button
              type='button'
              variant='outline'
              size='sm'
              className='text-destructive ml-auto'
              onClick={() => void deleteSelected()}
            >
              <Trash2 className='size-4' /> Delete
            </Button>
          ) : null}
          <input
            ref={folderInputRef}
            type='file'
            // @ts-expect-error non-standard but widely supported folder picker
            webkitdirectory=''
            multiple
            hidden
            onChange={(e) => void onPickFiles(e, true)}
          />
          <input
            ref={filesInputRef}
            type='file'
            multiple
            hidden
            onChange={(e) => void onPickFiles(e, false)}
          />
        </div>
        <div className='grid min-h-[28rem] grid-cols-1 md:grid-cols-[16rem_1fr]'>
          <div
            onDragOver={(e) => {
              e.preventDefault();
              setDragOver(true);
            }}
            onDragLeave={() => setDragOver(false)}
            onDrop={(e) => void onDrop(e)}
            className={`border-border min-h-0 overflow-auto border-b md:border-r md:border-b-0 ${
              dragOver ? 'bg-primary/10' : ''
            }`}
          >
            <FileTree
              tree={tree}
              selectedPath={selected?.path}
              expandedPaths={expanded}
              onSelect={(path) => void openFile(path)}
              onToggleDirectory={(path) =>
                setExpandedOverride(
                  expanded.includes(path)
                    ? expanded.filter((p) => p !== path)
                    : [...expanded, path],
                )
              }
            />
            {files.length === 0 ? (
              <p className='text-muted-foreground p-4 text-center text-xs'>
                Drag files or folders here, or use the buttons above. They land
                relative to your home directory.
              </p>
            ) : null}
          </div>
          <div className='min-h-0'>
            {selected ? (
              <CodeEditor
                path={selected.path}
                content={content}
                savedContent={savedContent}
                readOnly={false}
                vimEnabled={false}
                onSave={saveSelected}
                onChange={setContent}
                onVimEnabledChange={() => undefined}
              />
            ) : (
              <div className='text-muted-foreground flex h-full items-center justify-center p-6 text-sm'>
                Select a file to edit, or add files to get started.
              </div>
            )}
          </div>
        </div>
      </Card>

      <RepoPanel
        settings={settings}
        onSave={async (values) => {
          await updateEnv(values);
          toast.success('Saved.');
        }}
      />

      <p className='text-muted-foreground text-xs'>
        Dotfiles are encrypted at rest. For real API keys or tokens, use the
        Secrets feature on a Spoon instead — those are injected as environment
        variables.
      </p>
    </div>
  );
};

const RepoPanel = ({
  settings,
  onSave,
}: {
  settings:
    | {
        dotfilesRepoUrl?: string;
        dotfilesRepoRef?: string;
        setupCommand?: string;
      }
    | undefined;
  onSave: (values: {
    dotfilesRepoUrl?: string;
    dotfilesRepoRef?: string;
    setupCommand?: string;
  }) => Promise<void>;
}) => {
  const [repoUrl, setRepoUrl] = useState('');
  const [repoRef, setRepoRef] = useState('');
  const [setupCommand, setSetupCommand] = useState('');
  const [saving, setSaving] = useState(false);
  const [hydrated, setHydrated] = useState(false);

  useEffect(() => {
    if (!settings || hydrated) return;
    const timeout = window.setTimeout(() => {
      setRepoUrl(settings.dotfilesRepoUrl ?? '');
      setRepoRef(settings.dotfilesRepoRef ?? '');
      setSetupCommand(settings.setupCommand ?? '');
      setHydrated(true);
    }, 0);
    return () => window.clearTimeout(timeout);
  }, [settings, hydrated]);

  return (
    <Card className='space-y-3 p-4 shadow-none'>
      <div>
        <h3 className='font-medium'>Dotfiles repo (optional)</h3>
        <p className='text-muted-foreground text-xs'>
          A public git repo cloned to <code>~/.dotfiles</code> on start. The
          setup command runs in the container afterwards (e.g.{' '}
          <code>install</code> to symlink, like a dotfiles bootstrap). Your
          edited files above are applied on top.
        </p>
      </div>
      <div className='grid gap-3 sm:grid-cols-2'>
        <div className='space-y-1'>
          <Label htmlFor='repoUrl'>Public repo URL</Label>
          <Input
            id='repoUrl'
            placeholder='https://github.com/you/dotfiles'
            value={repoUrl}
            onChange={(e) => setRepoUrl(e.target.value)}
          />
        </div>
        <div className='space-y-1'>
          <Label htmlFor='repoRef'>Branch / ref (optional)</Label>
          <Input
            id='repoRef'
            placeholder='main'
            value={repoRef}
            onChange={(e) => setRepoRef(e.target.value)}
          />
        </div>
      </div>
      <div className='space-y-1'>
        <Label htmlFor='setupCommand'>Setup script path (optional)</Label>
        <Input
          id='setupCommand'
          placeholder='install.sh'
          value={setupCommand}
          onChange={(e) => setSetupCommand(e.target.value)}
        />
      </div>
      <Button
        type='button'
        size='sm'
        disabled={saving}
        onClick={() => {
          setSaving(true);
          void onSave({
            dotfilesRepoUrl: repoUrl,
            dotfilesRepoRef: repoRef,
            setupCommand,
          }).finally(() => setSaving(false));
        }}
      >
        {saving ? 'Saving…' : 'Save repo settings'}
      </Button>
    </Card>
  );
};
