<script lang="ts">
  import { appRpc } from './rpc.ts';
  import CodeEditor from './CodeEditor.svelte';
  import PdfViewer from './PdfViewer.svelte';
  import CsvTable from './CsvTable.svelte';

  interface Props {
    cwd: string;
  }

  let { cwd }: Props = $props();

  interface DirEntry {
    name: string;
    type: 'file' | 'dir';
    size: number;
  }

  // Tree state: track loaded children and open/closed per path
  let childrenCache = $state<Map<string, DirEntry[]>>(new Map());
  let openDirs = $state<Set<string>>(new Set());
  let loadingDirs = $state<Set<string>>(new Set());
  let selectedFile = $state<string | null>(null);

  // File viewer state
  let fileContent = $state<string | null>(null);
  let fileEncoding = $state<'utf8' | 'base64'>('utf8');
  let fileSize = $state(0);
  let fileError = $state<string | null>(null);
  let fileLoading = $state(false);
  let isDirty = $state(false);
  let editorContent = $state('');
  // Fix #6: Request token to discard stale file load responses
  let fileRequestToken = 0;
  // Feature 2: Track mtime at read time for conflict detection
  let readMtimeMs = $state(0);
  let showConflictDialog = $state(false);

  // Extension-based type detection
  const CODE_EXTS = new Set([
    'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
    'py', 'rs', 'go', 'css', 'scss', 'less',
    'html', 'svelte', 'vue',
    'json', 'md', 'yaml', 'yml', 'toml', 'sh', 'bash',
    'xml', 'sql', 'c', 'cpp', 'h', 'java', 'php',
  ]);
  const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico', 'bmp']);
  const PDF_EXTS = new Set(['pdf']);
  const CSV_EXTS = new Set(['csv', 'tsv']);

  function getExt(name: string): string {
    const dot = name.lastIndexOf('.');
    return dot >= 0 ? name.slice(dot + 1).toLowerCase() : '';
  }

  type FileType = 'code' | 'image' | 'pdf' | 'csv' | 'text';

  function detectFileType(name: string): FileType {
    const ext = getExt(name);
    if (PDF_EXTS.has(ext)) return 'pdf';
    if (CSV_EXTS.has(ext)) return 'csv';
    if (IMAGE_EXTS.has(ext)) return 'image';
    if (CODE_EXTS.has(ext)) return 'code';
    return 'text';
  }

  /** Map extension to CodeMirror language name. */
  function extToLang(name: string): string {
    const ext = getExt(name);
    const map: Record<string, string> = {
      ts: 'typescript', tsx: 'tsx', js: 'javascript', jsx: 'jsx',
      mjs: 'javascript', cjs: 'javascript',
      py: 'python', rs: 'rust', go: 'go',
      css: 'css', scss: 'css', less: 'css',
      html: 'html', svelte: 'html', vue: 'html',
      json: 'json', md: 'markdown', yaml: 'yaml', yml: 'yaml',
      toml: 'toml', sh: 'bash', bash: 'bash',
      xml: 'xml', sql: 'sql', c: 'c', cpp: 'cpp', h: 'c',
      java: 'java', php: 'php',
    };
    return map[ext] ?? 'text';
  }

  let selectedType = $derived<FileType>(selectedFile ? detectFileType(selectedFile) : 'text');
  let selectedName = $derived(selectedFile ? selectedFile.split('/').pop() ?? '' : '');

  /** Load directory entries via RPC. */
  async function loadDir(dirPath: string) {
    if (childrenCache.has(dirPath)) return;
    const key = dirPath;
    loadingDirs = new Set([...loadingDirs, key]);
    try {
      const result = await appRpc.request["files.list"]({ path: dirPath });
      if (result.error) {
        console.error(`[files.list] ${dirPath}: ${result.error}`);
        return;
      }
      const next = new Map(childrenCache);
      next.set(dirPath, result.entries);
      childrenCache = next;
    } catch (err) {
      console.error('[files.list]', err);
    } finally {
      const s = new Set(loadingDirs);
      s.delete(key);
      loadingDirs = s;
    }
  }

  /** Toggle a directory open/closed. Lazy-loads on first open. */
  async function toggleDir(dirPath: string) {
    const s = new Set(openDirs);
    if (s.has(dirPath)) {
      s.delete(dirPath);
      openDirs = s;
    } else {
      s.add(dirPath);
      openDirs = s;
      await loadDir(dirPath);
    }
  }

  /** Select and load a file. Fix #6: uses request token to discard stale responses. */
  async function selectFile(filePath: string) {
    if (selectedFile === filePath) return;
    selectedFile = filePath;
    isDirty = false;
    fileContent = null;
    fileError = null;
    fileLoading = true;
    const token = ++fileRequestToken;

    const type = detectFileType(filePath);

    // PDF uses its own loader via PdfViewer
    if (type === 'pdf') {
      fileLoading = false;
      return;
    }

    // Images: read as base64 for display
    if (type === 'image') {
      try {
        const result = await appRpc.request["files.read"]({ path: filePath });
        if (token !== fileRequestToken) return;
        if (result.error) {
          fileError = result.error;
          return;
        }
        fileContent = result.content ?? '';
        fileEncoding = result.encoding;
        fileSize = result.size;
      } catch (err) {
        if (token !== fileRequestToken) return;
        fileError = err instanceof Error ? err.message : String(err);
      } finally {
        if (token === fileRequestToken) fileLoading = false;
      }
      return;
    }

    try {
      const result = await appRpc.request["files.read"]({ path: filePath });
      if (token !== fileRequestToken) return;
      if (result.error) {
        fileError = result.error;
        return;
      }
      fileContent = result.content ?? '';
      fileEncoding = result.encoding;
      fileSize = result.size;
      editorContent = fileContent;
      // Feature 2: Record mtime at read time
      try {
        const stat = await appRpc.request["files.stat"]({ path: filePath });
        if (token === fileRequestToken && !stat.error) readMtimeMs = stat.mtimeMs;
      } catch { /* non-critical */ }
    } catch (err) {
      if (token !== fileRequestToken) return;
      fileError = err instanceof Error ? err.message : String(err);
    } finally {
      if (token === fileRequestToken) fileLoading = false;
    }
  }

  /** Save current file. Feature 2: Check mtime before write for conflict detection. */
  async function saveFile() {
    if (!selectedFile || !isDirty) return;
    try {
      // Feature 2: Check if file was modified externally since we read it
      if (readMtimeMs > 0) {
        const stat = await appRpc.request["files.stat"]({ path: selectedFile });
        if (!stat.error && stat.mtimeMs > readMtimeMs) {
          showConflictDialog = true;
          return;
        }
      }
      await doSave();
    } catch (err) {
      console.error('[files.write]', err);
    }
  }

  /** Force-save, bypassing conflict check. */
  async function doSave() {
    if (!selectedFile) return;
    try {
      const result = await appRpc.request["files.write"]({
        path: selectedFile,
        content: editorContent,
      });
      if (result.ok) {
        isDirty = false;
        fileContent = editorContent;
        showConflictDialog = false;
        // Update mtime after successful save
        const stat = await appRpc.request["files.stat"]({ path: selectedFile });
        if (!stat.error) readMtimeMs = stat.mtimeMs;
      } else if (result.error) {
        console.error('[files.write]', result.error);
      }
    } catch (err) {
      console.error('[files.write]', err);
    }
  }

  /** Reload file from disk (discard local changes). */
  async function reloadFile() {
    showConflictDialog = false;
    if (selectedFile) {
      isDirty = false;
      const saved = selectedFile;
      selectedFile = null;
      await selectFile(saved);
    }
  }

  function onEditorChange(newContent: string) {
    editorContent = newContent;
    isDirty = newContent !== fileContent;
  }

  function fileIcon(name: string): string {
    const ext = getExt(name);
    if (['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'].includes(ext)) return '</>';
    if (ext === 'svelte' || ext === 'vue') return '~';
    if (ext === 'rs') return 'Rs';
    if (ext === 'py') return 'Py';
    if (ext === 'go') return 'Go';
    if (['json', 'toml', 'yaml', 'yml'].includes(ext)) return '{}';
    if (ext === 'md') return 'M';
    if (ext === 'css' || ext === 'scss') return '#';
    if (ext === 'html') return '<>';
    if (IMAGE_EXTS.has(ext)) return 'Im';
    if (ext === 'pdf') return 'Pd';
    if (CSV_EXTS.has(ext)) return 'Tb';
    return '..';
  }

  function formatSize(bytes: number): string {
    if (bytes < 1024) return `${bytes} B`;
    if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
    return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
  }

  // Load root directory on mount
  $effect(() => {
    if (cwd) {
      loadDir(cwd);
      const s = new Set(openDirs);
      s.add(cwd);
      openDirs = s;
    }
  });
</script>

<div class="file-browser">
  <!-- Tree panel -->
  <div class="fb-tree">
    {#snippet renderEntries(dirPath: string, depth: number)}
      {#if childrenCache.has(dirPath)}
        {#each childrenCache.get(dirPath) ?? [] as entry}
          {@const fullPath = `${dirPath}/${entry.name}`}
          {#if entry.type === 'dir'}
            <button
              class="fb-row fb-dir"
              style:padding-left="{0.5 + depth * 0.875}rem"
              onclick={() => toggleDir(fullPath)}
              aria-expanded={openDirs.has(fullPath)}
            >
              <span class="fb-chevron" class:open={openDirs.has(fullPath)}>
                {#if loadingDirs.has(fullPath)}...{:else}>{/if}
              </span>
              <span class="fb-name">{entry.name}</span>
            </button>
            {#if openDirs.has(fullPath)}
              {@render renderEntries(fullPath, depth + 1)}
            {/if}
          {:else}
            <button
              class="fb-row fb-file"
              class:selected={selectedFile === fullPath}
              style:padding-left="{0.5 + depth * 0.875}rem"
              onclick={() => selectFile(fullPath)}
              title={`${entry.name} (${formatSize(entry.size)})`}
            >
              <span class="fb-icon file-type">{fileIcon(entry.name)}</span>
              <span class="fb-name">{entry.name}</span>
              {#if selectedFile === fullPath && isDirty}
                <span class="dirty-dot" title="Unsaved changes"></span>
              {/if}
            </button>
          {/if}
        {/each}
      {:else if loadingDirs.has(dirPath)}
        <div class="fb-loading" style:padding-left="{0.5 + depth * 0.875}rem">Loading...</div>
      {/if}
    {/snippet}

    {@render renderEntries(cwd, 0)}
  </div>

  <!-- Feature 2: Conflict dialog -->
  {#if showConflictDialog}
    <div class="conflict-overlay">
      <div class="conflict-dialog">
        <p class="conflict-title">File modified externally</p>
        <p class="conflict-desc">This file was changed on disk since you opened it.</p>
        <div class="conflict-actions">
          <button class="conflict-btn overwrite" onclick={doSave}>Overwrite</button>
          <button class="conflict-btn reload" onclick={reloadFile}>Reload</button>
          <button class="conflict-btn cancel" onclick={() => showConflictDialog = false}>Cancel</button>
        </div>
      </div>
    </div>
  {/if}

  <!-- Viewer panel -->
  <div class="fb-viewer">
    {#if !selectedFile}
      <div class="fb-empty">Select a file to view</div>
    {:else if fileLoading}
      <div class="fb-empty">Loading...</div>
    {:else if fileError}
      <div class="fb-error">{fileError}</div>
    {:else if selectedType === 'pdf'}
      <PdfViewer filePath={selectedFile} />
    {:else if selectedType === 'csv' && fileContent != null}
      <CsvTable content={fileContent} filename={selectedName} />
    {:else if selectedType === 'image' && fileContent}
      {@const ext = getExt(selectedName)}
      {@const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'png' ? 'image/png' : ext === 'gif' ? 'image/gif' : ext === 'webp' ? 'image/webp' : 'image/jpeg'}
      <div class="fb-image-wrap">
        <div class="fb-image-label">{selectedName} ({formatSize(fileSize)})</div>
        <img
          class="fb-image"
          src="data:{mime};base64,{fileEncoding === 'base64' ? fileContent : btoa(fileContent)}"
          alt={selectedName}
        />
      </div>
    {:else if selectedType === 'code' && fileContent != null}
      <div class="fb-editor-header">
        <span class="fb-editor-path" title={selectedFile}>
          {selectedName}
          {#if isDirty}<span class="dirty-indicator"> (modified)</span>{/if}
        </span>
        <span class="fb-editor-size">{formatSize(fileSize)}</span>
      </div>
      <CodeEditor
        content={fileContent}
        lang={extToLang(selectedFile)}
        onsave={saveFile}
        onchange={onEditorChange}
        onblur={saveFile}
      />
    {:else if fileContent != null}
      <!-- Raw text fallback -->
      <div class="fb-editor-header">
        <span class="fb-editor-path" title={selectedFile}>
          {selectedName}
          {#if isDirty}<span class="dirty-indicator"> (modified)</span>{/if}
        </span>
        <span class="fb-editor-size">{formatSize(fileSize)}</span>
      </div>
      <CodeEditor
        content={fileContent}
        lang="text"
        onsave={saveFile}
        onchange={onEditorChange}
        onblur={saveFile}
      />
    {/if}
  </div>
</div>

<style>
  .file-browser {
    display: flex;
    height: 100%;
    overflow: hidden;
    font-size: 0.8125rem;
    position: relative;
  }

  /* ── Tree panel ── */
  .fb-tree {
    width: 14rem;
    min-width: 10rem;
    overflow-y: auto;
    padding: 0.25rem 0;
    border-right: 1px solid var(--ctp-surface0);
    background: var(--ctp-mantle);
    flex-shrink: 0;
  }

  .fb-tree::-webkit-scrollbar { width: 0.25rem; }
  .fb-tree::-webkit-scrollbar-track { background: transparent; }
  .fb-tree::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }

  .fb-row {
    display: flex;
    align-items: center;
    gap: 0.3rem;
    width: 100%;
    background: transparent;
    border: none;
    color: var(--ctp-text);
    font-family: var(--ui-font-family);
    font-size: 0.8125rem;
    padding-top: 0.2rem;
    padding-bottom: 0.2rem;
    padding-right: 0.5rem;
    cursor: pointer;
    text-align: left;
    white-space: nowrap;
    transition: background 0.08s;
  }

  .fb-row:hover { background: var(--ctp-surface0); }

  .fb-file.selected {
    background: color-mix(in srgb, var(--accent, var(--ctp-mauve)) 15%, transparent);
    color: var(--accent, var(--ctp-mauve));
  }

  .fb-chevron {
    display: inline-block;
    width: 0.875rem;
    font-size: 0.75rem;
    color: var(--ctp-overlay1);
    transition: transform 0.12s;
    transform: rotate(0deg);
    flex-shrink: 0;
    line-height: 1;
    font-family: var(--term-font-family, monospace);
  }

  .fb-chevron.open { transform: rotate(90deg); }

  .fb-icon { flex-shrink: 0; font-style: normal; }

  .file-type {
    font-size: 0.6rem;
    color: var(--ctp-overlay1);
    font-family: var(--term-font-family, monospace);
    width: 1.25rem;
    text-align: center;
  }

  .fb-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .fb-dir .fb-name { color: var(--ctp-subtext1); font-weight: 500; }

  .fb-loading {
    font-size: 0.7rem;
    color: var(--ctp-overlay0);
    font-style: italic;
    padding: 0.15rem 0;
  }

  .dirty-dot {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--ctp-peach);
    flex-shrink: 0;
    margin-left: auto;
  }

  /* ── Viewer panel ── */
  .fb-viewer {
    flex: 1;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: var(--ctp-base);
  }

  .fb-empty {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--ctp-overlay0);
    font-size: 0.8rem;
    font-style: italic;
  }

  .fb-error {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 100%;
    color: var(--ctp-red);
    font-size: 0.8rem;
    padding: 1rem;
  }

  .fb-editor-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 0.25rem 0.625rem;
    background: var(--ctp-mantle);
    border-bottom: 1px solid var(--ctp-surface0);
    flex-shrink: 0;
  }

  .fb-editor-path {
    font-size: 0.7rem;
    color: var(--ctp-subtext0);
    font-family: var(--term-font-family, monospace);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .dirty-indicator {
    color: var(--ctp-peach);
    font-style: italic;
  }

  .fb-editor-size {
    font-size: 0.65rem;
    color: var(--ctp-overlay0);
    font-variant-numeric: tabular-nums;
    flex-shrink: 0;
    margin-left: 0.5rem;
  }

  .fb-image-wrap {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    height: 100%;
    gap: 0.5rem;
  }

  .fb-image-label {
    font-size: 0.75rem;
    color: var(--ctp-subtext0);
    font-family: var(--term-font-family, monospace);
    flex-shrink: 0;
    padding: 0.5rem;
  }

  .fb-image {
    max-width: 100%;
    max-height: calc(100% - 2rem);
    object-fit: contain;
    border-radius: 0.25rem;
  }

  /* Feature 2: Conflict dialog */
  .conflict-overlay {
    position: absolute;
    inset: 0;
    z-index: 50;
    background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .conflict-dialog {
    background: var(--ctp-surface0);
    border: 1px solid var(--ctp-surface1);
    border-radius: 0.5rem;
    padding: 1rem 1.25rem;
    max-width: 20rem;
  }

  .conflict-title {
    margin: 0 0 0.25rem;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--ctp-peach);
  }

  .conflict-desc {
    margin: 0 0 0.75rem;
    font-size: 0.75rem;
    color: var(--ctp-subtext0);
  }

  .conflict-actions {
    display: flex;
    gap: 0.375rem;
  }

  .conflict-btn {
    flex: 1;
    padding: 0.25rem 0.5rem;
    border-radius: 0.25rem;
    border: 1px solid var(--ctp-surface1);
    background: var(--ctp-surface0);
    color: var(--ctp-text);
    font-family: var(--ui-font-family);
    font-size: 0.6875rem;
    font-weight: 600;
    cursor: pointer;
  }

  .conflict-btn.overwrite { border-color: var(--ctp-red); color: var(--ctp-red); }
  .conflict-btn.reload { border-color: var(--ctp-blue); color: var(--ctp-blue); }
  .conflict-btn:hover { background: var(--ctp-surface1); }
</style>
