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

  interface Props {
    cwd: string;
    projectId: string;
  }

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

  // Read file state from project state tree
  function getFiles() { return appState.project.getState(projectId).files; }

  // 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';
  }

  function getSelectedType(): FileType {
    const f = getFiles().selectedFile;
    return f ? detectFileType(f) : 'text';
  }
  function getSelectedName(): string {
    const f = getFiles().selectedFile;
    return f ? f.split('/').pop() ?? '' : '';
  }

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

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

  /** Select and load a file. Fix #6: uses request token to discard stale responses. */
  async function selectFile(filePath: string) {
    const f = getFiles();
    if (f.selectedFile === filePath) return;
    appState.project.files.setMulti(projectId, {
      selectedFile: filePath, isDirty: false, fileContent: null,
      fileError: null, fileLoading: true,
    });
    const token = appState.project.files.nextRequestToken(projectId);
    const type = detectFileType(filePath);

    if (type === 'pdf') {
      appState.project.files.setState(projectId, 'fileLoading', false);
      return;
    }

    if (type === 'image') {
      try {
        const result = await appRpc.request["files.read"]({ path: filePath });
        if (token !== appState.project.files.getRequestToken(projectId)) return;
        if (result.error) {
          appState.project.files.setState(projectId, 'fileError', result.error);
          return;
        }
        appState.project.files.setMulti(projectId, {
          fileContent: result.content ?? '', fileEncoding: result.encoding, fileSize: result.size,
        });
      } catch (err) {
        if (token !== appState.project.files.getRequestToken(projectId)) return;
        appState.project.files.setState(projectId, 'fileError', err instanceof Error ? err.message : String(err));
      } finally {
        if (token === appState.project.files.getRequestToken(projectId)) {
          appState.project.files.setState(projectId, 'fileLoading', false);
        }
      }
      return;
    }

    try {
      const result = await appRpc.request["files.read"]({ path: filePath });
      if (token !== appState.project.files.getRequestToken(projectId)) return;
      if (result.error) {
        appState.project.files.setState(projectId, 'fileError', result.error);
        return;
      }
      appState.project.files.setMulti(projectId, {
        fileContent: result.content ?? '', fileEncoding: result.encoding,
        fileSize: result.size, editorContent: result.content ?? '',
      });
      try {
        const stat = await appRpc.request["files.stat"]({ path: filePath });
        if (token === appState.project.files.getRequestToken(projectId) && !stat.error) {
          appState.project.files.setState(projectId, 'readMtimeMs', stat.mtimeMs);
        }
      } catch { /* non-critical */ }
    } catch (err) {
      if (token !== appState.project.files.getRequestToken(projectId)) return;
      appState.project.files.setState(projectId, 'fileError', err instanceof Error ? err.message : String(err));
    } finally {
      if (token === appState.project.files.getRequestToken(projectId)) {
        appState.project.files.setState(projectId, 'fileLoading', false);
      }
    }
  }

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

  /** Force-save, bypassing conflict check. */
  async function doSave() {
    const f = getFiles();
    if (!f.selectedFile) return;
    try {
      const result = await appRpc.request["files.write"]({
        path: f.selectedFile, content: f.editorContent,
      });
      if (result.ok) {
        appState.project.files.setMulti(projectId, {
          isDirty: false, fileContent: f.editorContent, showConflictDialog: false,
        });
        const stat = await appRpc.request["files.stat"]({ path: f.selectedFile });
        if (!stat.error) appState.project.files.setState(projectId, '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() {
    appState.project.files.setState(projectId, 'showConflictDialog', false);
    const f = getFiles();
    if (f.selectedFile) {
      appState.project.files.setState(projectId, 'isDirty', false);
      const saved = f.selectedFile;
      appState.project.files.setState(projectId, 'selectedFile', null);
      await selectFile(saved);
    }
  }

  function onEditorChange(newContent: string) {
    const f = getFiles();
    appState.project.files.setMulti(projectId, {
      editorContent: newContent, isDirty: newContent !== f.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 — use onMount, NOT $effect
  import { onMount } from 'svelte';
  onMount(() => {
    if (cwd) {
      loadDir(cwd);
      appState.project.files.setState(projectId, 'openDirs', new Set([cwd]));
    }
  });
</script>

<div class="file-browser">
  <!-- Tree panel -->
  <div class="fb-tree">
    {#snippet renderEntries(dirPath: string, depth: number)}
      {#if getFiles().childrenCache.has(dirPath)}
        {#each getFiles().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={getFiles().openDirs.has(fullPath)}
            >
              <span class="fb-chevron" class:open={getFiles().openDirs.has(fullPath)}>
                {#if getFiles().loadingDirs.has(fullPath)}...{:else}>{/if}
              </span>
              <span class="fb-name">{entry.name}</span>
            </button>
            {#if getFiles().openDirs.has(fullPath)}
              {@render renderEntries(fullPath, depth + 1)}
            {/if}
          {:else}
            <button
              class="fb-row fb-file"
              class:selected={getFiles().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 getFiles().selectedFile === fullPath && getFiles().isDirty}
                <span class="dirty-dot" title="Unsaved changes"></span>
              {/if}
            </button>
          {/if}
        {/each}
      {:else if getFiles().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 getFiles().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={() => appState.project.files.setState(projectId, 'showConflictDialog', false)}>Cancel</button>
        </div>
      </div>
    </div>
  {/if}

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