<script lang="ts">
  import { onDestroy, onMount } from "svelte";
  import { open as openDialog } from "@tauri-apps/plugin-dialog";
  import { AlertCircle, Check, FolderOpen, GitBranch, GitMerge, LoaderCircle } from "@lucide/svelte";

  import TitleBar from "./lib/TitleBar.svelte";
  import BranchPanel from "./lib/components/BranchPanel.svelte";
  import CommitPanel from "./lib/components/CommitPanel.svelte";
  import CompareDialog from "./lib/components/CompareDialog.svelte";
  import ComparePanel from "./lib/components/ComparePanel.svelte";
  import CredentialDialog from "./lib/components/CredentialDialog.svelte";
  import ExplorerPanel from "./lib/components/ExplorerPanel.svelte";
  import FileHistoryPanel from "./lib/components/FileHistoryPanel.svelte";
  import GlobalSearchDialog from "./lib/components/GlobalSearchDialog.svelte";
  import HistoryPanel from "./lib/components/HistoryPanel.svelte";
  import ResolveDialog from "./lib/components/ResolveDialog.svelte";
  import StatusPanel from "./lib/components/StatusPanel.svelte";

  import {
    checkoutBranch,
    commit,
    compareCommits,
    cancelCodeSearch,
    diffFileAgainstWorkingTree,
    compareFileToHead,
    getStatus,
    listBranches,
    listCommits,
    listFileHistory,
    listRepositoryFiles,
    mergeBranch,
    openRepository,
    pull,
    push,
    readConflict,
    resolveConflict,
    resolveConflictSide,
    restoreFileFromCommit,
    restoreFiles,
    restoreToCommit,
    searchCodeIntroductions,
    stageFiles,
    unstageFiles,
  } from "./lib/git";

  import type {
    ConflictFile,
    ExplorerNode,
    ExplorerNodeKind,
    GitBranch as GitBranchInfo,
    GitCommit,
    GitCommitFile,
    GitCommitComparison,
    GitDiffFile,
    GitFileStatus,
    GitRepositoryFile,
    GitSearchHit,
    GitStatus,
    PreparedResolution,
  } from "./lib/types";

  // ── State ──────────────────────────────────────────────────────────────────

  let repoPath = "";
  let activeRepoPath = "";
  let status: GitStatus | null = null;
  let branches: GitBranchInfo[] = [];
  let commits: GitCommit[] = [];
  let repoFiles: GitRepositoryFile[] = [];
  let selectedExplorerPath = "";
  let selectedExplorerKind: ExplorerNodeKind = "file";
  let expandedExplorerPaths = new Set<string>();
  let expandedCommitHashes = new Set<string>();
  let fileHistory: GitCommit[] = [];
  let commitMessage = "";
  let errorMessage = "";
  let operation = "";
  let compareFrom = "";
  let compareTo = "";
  let comparison: GitCommitComparison | null = null;
  let compareDialogOpen = false;
  let selectedDiffPath = "";
  let pendingRestoreFile: { commit: GitCommit; file: GitCommitFile } | null = null;
  let globalSearchOpen = false;
  let globalSearchResults: GitSearchHit[] = [];
  let globalSearchBusy = false;
  let globalSearchError = "";
  let globalSearchId = "";
  let resolveDialogOpen = false;
  let conflictTarget = "";
  let conflict: ConflictFile | null = null;
  let preparedResolutions: Record<string, PreparedResolution> = {};
  let autoRefreshEnabled = true;
  let autoRefreshInFlight = false;
  let credDialogOpen = false;
  let credDialogAction: "push" | "pull" | null = null;
  let credDialogError = "";
  let sessionCredentials: { username: string; password: string } | null = null;
  let lastStatusFingerprint = "";
  const AUTO_REFRESH_INTERVAL = 4000;
  let autoRefreshTimer: ReturnType<typeof setInterval> | undefined;

  // ── Derived ────────────────────────────────────────────────────────────────

  $: isBusy = operation.length > 0;
  $: hasRepository = activeRepoPath.length > 0 && status !== null;
  $: changedFiles = status?.files ?? [];
  $: stagedCount = status?.files.filter((f) => f.staged !== null).length ?? 0;
  $: unstagedCount = status?.files.filter((f) => f.unstaged !== null).length ?? 0;
  $: conflictedFiles = changedFiles.filter((f) => f.staged === "conflicted" || f.unstaged === "conflicted");
  $: hasConflicts = conflictedFiles.length > 0;
  $: canCommit = hasRepository && stagedCount > 0 && commitMessage.trim().length > 0 && !hasConflicts && !isBusy;
  $: commitBlockReason = hasConflicts
    ? `${conflictedFiles.length} ${conflictedFiles.length === 1 ? "merge conflict must" : "merge conflicts must"} be resolved before committing.`
    : "";
  $: canCompare = hasRepository && compareFrom.length > 0 && compareTo.length > 0 && compareFrom !== compareTo && !isBusy;
  $: localBranches = branches.filter((b) => !b.remote);
  $: remoteBranches = branches.filter((b) => b.remote);

  // ── Lifecycle ──────────────────────────────────────────────────────────────

  onMount(() => {
    autoRefreshTimer = setInterval(() => { void autoRefreshTick(); }, AUTO_REFRESH_INTERVAL);
  });

  onDestroy(() => {
    if (autoRefreshTimer) clearInterval(autoRefreshTimer);
  });

  // ── Auto-refresh ───────────────────────────────────────────────────────────

  function statusFingerprint(value: GitStatus): string {
    return JSON.stringify({ branch: value.current_branch, upstream: value.upstream, ahead: value.ahead, behind: value.behind, files: value.files });
  }

  async function autoRefreshTick() {
    if (!autoRefreshEnabled || !activeRepoPath || isBusy || autoRefreshInFlight || resolveDialogOpen || compareDialogOpen || globalSearchOpen) return;
    autoRefreshInFlight = true;
    try {
      const nextStatus = await getStatus(activeRepoPath);
      if (statusFingerprint(nextStatus) === lastStatusFingerprint) return;
      applyStatus(nextStatus);
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    } catch { /* ignore transient errors */ } finally {
      autoRefreshInFlight = false;
    }
  }

  function toggleAutoRefresh() {
    autoRefreshEnabled = !autoRefreshEnabled;
    if (autoRefreshEnabled) void autoRefreshTick();
  }

  // ── Utilities ──────────────────────────────────────────────────────────────

  function applyStatus(nextStatus: GitStatus) {
    status = nextStatus;
    activeRepoPath = nextStatus.repo_path || activeRepoPath || repoPath.trim();
    repoPath = activeRepoPath;
    lastStatusFingerprint = statusFingerprint(nextStatus);
  }

  function errorToMessage(error: unknown): string {
    if (error instanceof Error) return error.message;
    if (typeof error === "string") return error;
    try { return JSON.stringify(error) ?? "Unknown error"; } catch { return "Unknown error"; }
  }

  async function runOperation(label: string, task: () => Promise<void>) {
    if (isBusy) return;
    operation = label;
    errorMessage = "";
    try { await task(); } catch (error) { errorMessage = errorToMessage(error); } finally { operation = ""; }
  }

  function normalizeExplorerPath(path: string): string {
    return path.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
  }

  function explorerPathExists(files: GitRepositoryFile[], path: string): boolean {
    if (!path) return false;
    const normalized = normalizeExplorerPath(path);
    return files.some((f) => {
      const fp = normalizeExplorerPath(f.path);
      return fp === normalized || fp.startsWith(`${normalized}/`);
    });
  }

  function defaultExpandedExplorerPaths(files: GitRepositoryFile[]): Set<string> {
    const expanded = new Set<string>();
    for (const file of files) {
      const parts = file.path.split(/[\\/]+/).filter(Boolean);
      if (parts.length > 1) expanded.add(parts[0]);
    }
    return expanded;
  }

  // ── Refresh helpers ────────────────────────────────────────────────────────

  async function refreshBranchList(path = activeRepoPath) {
    branches = await listBranches(path);
  }

  async function refreshCommitHistory(path = activeRepoPath) {
    commits = await listCommits(path, 100);
    const hashes = new Set(commits.map((c) => c.hash));
    if (compareFrom && !hashes.has(compareFrom)) compareFrom = "";
    if (compareTo && !hashes.has(compareTo)) compareTo = "";
    if (comparison && comparison.to_hash.length > 0 && (!hashes.has(comparison.from_hash) || !hashes.has(comparison.to_hash))) {
      comparison = null;
      compareDialogOpen = false;
      selectedDiffPath = "";
      pendingRestoreFile = null;
    }
  }

  async function refreshExplorerFiles(path = activeRepoPath) {
    repoFiles = await listRepositoryFiles(path);
    if (expandedExplorerPaths.size === 0) expandedExplorerPaths = defaultExpandedExplorerPaths(repoFiles);
    if (selectedExplorerPath && !explorerPathExists(repoFiles, selectedExplorerPath)) {
      selectedExplorerPath = "";
      selectedExplorerKind = "file";
      fileHistory = [];
    }
  }

  async function refreshFileHistory(path = activeRepoPath, file = selectedExplorerPath) {
    fileHistory = file ? await listFileHistory(path, file, 100) : [];
  }

  // ── Repository operations ──────────────────────────────────────────────────

  async function openRepo(pathOverride?: string) {
    const path = (pathOverride ?? repoPath).trim();
    if (!path) { errorMessage = "Enter a repository path."; return; }
    repoPath = path;

    await runOperation("Opening repository", async () => {
      const nextStatus = await openRepository(path);
      applyStatus(nextStatus);
      branches = []; commits = []; repoFiles = [];
      selectedExplorerPath = ""; selectedExplorerKind = "file";
      expandedExplorerPaths = new Set(); expandedCommitHashes = new Set();
      fileHistory = []; compareFrom = ""; compareTo = "";
      comparison = null; compareDialogOpen = false; selectedDiffPath = ""; pendingRestoreFile = null;
      if (globalSearchBusy) void cancelGlobalSearch();
      globalSearchResults = []; globalSearchOpen = false; globalSearchError = "";
      resolveDialogOpen = false; conflictTarget = ""; conflict = null;
      preparedResolutions = {};
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
    });
  }

  async function chooseRepositoryFolder() {
    if (isBusy) return;
    try {
      const selected = await openDialog({
        title: "Repository folder auswaehlen",
        directory: true,
        multiple: false,
        defaultPath: repoPath.trim() || activeRepoPath || undefined,
      });
      if (typeof selected !== "string") return;
      repoPath = selected;
      await openRepo(selected);
    } catch (error) {
      errorMessage = errorToMessage(error);
    }
  }

  async function refreshRepo() {
    if (!activeRepoPath) { await openRepo(); return; }
    await runOperation("Refreshing", async () => {
      applyStatus(await getStatus(activeRepoPath));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  async function checkout(branch: GitBranchInfo) {
    if (!activeRepoPath || branch.current) return;
    await runOperation(`Checking out ${branch.name}`, async () => {
      applyStatus(await checkoutBranch(activeRepoPath, branch.name));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  async function merge(branch: GitBranchInfo) {
    if (!activeRepoPath || branch.current) return;
    await runOperation(`Merging ${branch.name}`, async () => {
      applyStatus(await mergeBranch(activeRepoPath, branch.name));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  function openCredentialDialog(action: "push" | "pull") {
    if (!activeRepoPath) return;
    credDialogError = "";
    credDialogAction = action;
    credDialogOpen = true;
  }

  async function doActualPull(username: string, password: string) {
    errorMessage = "";
    await runOperation("Pulling", async () => {
      applyStatus(await pull(activeRepoPath, username, password));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
    if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
    else { credDialogOpen = false; credDialogAction = null; }
  }

  async function doActualPush(username: string, password: string) {
    errorMessage = "";
    await runOperation("Pushing", async () => {
      applyStatus(await push(activeRepoPath, username, password));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
    if (errorMessage) { credDialogError = errorMessage; errorMessage = ""; }
    else { credDialogOpen = false; credDialogAction = null; }
  }

  async function handleCredentialSubmit(username: string, password: string, save: boolean) {
    if (credDialogAction === "pull") await doActualPull(username, password);
    else if (credDialogAction === "push") await doActualPush(username, password);
    if (!credDialogOpen && save) sessionCredentials = { username, password };
  }

  async function pullRepo() {
    if (!activeRepoPath) return;
    if (sessionCredentials) {
      await doActualPull(sessionCredentials.username, sessionCredentials.password);
    } else {
      openCredentialDialog("pull");
    }
  }

  async function pushRepo() {
    if (!activeRepoPath) return;
    if (sessionCredentials) {
      await doActualPush(sessionCredentials.username, sessionCredentials.password);
    } else {
      openCredentialDialog("push");
    }
  }

  // ── File staging / restore ─────────────────────────────────────────────────

  async function stageFile(file: GitFileStatus) {
    await runOperation(`Staging ${file.path}`, async () => {
      applyStatus(await stageFiles(activeRepoPath, [file.path]));
      await refreshExplorerFiles(activeRepoPath);
    });
  }

  async function unstageFile(file: GitFileStatus) {
    await runOperation(`Unstaging ${file.path}`, async () => {
      applyStatus(await unstageFiles(activeRepoPath, [file.path]));
      await refreshExplorerFiles(activeRepoPath);
    });
  }

  async function discardFile(file: GitFileStatus, staged: boolean) {
    await runOperation(`Discarding ${file.path}`, async () => {
      applyStatus(await restoreFiles(activeRepoPath, [file.path], staged));
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  async function stageAllFiles() {
    const paths = changedFiles.filter((f) => f.unstaged !== null).map((f) => f.path);
    if (paths.length === 0) return;
    await runOperation("Staging all", async () => {
      applyStatus(await stageFiles(activeRepoPath, paths));
      await refreshExplorerFiles(activeRepoPath);
    });
  }

  async function unstageAllFiles() {
    const paths = changedFiles.filter((f) => f.staged !== null).map((f) => f.path);
    if (paths.length === 0) return;
    await runOperation("Unstaging all", async () => {
      applyStatus(await unstageFiles(activeRepoPath, paths));
      await refreshExplorerFiles(activeRepoPath);
    });
  }

  async function commitChanges() {
    const message = commitMessage.trim();
    if (!message || !activeRepoPath) return;
    if (hasConflicts) {
      errorMessage = "Resolve all merge conflicts before committing.";
      return;
    }
    await runOperation("Committing", async () => {
      applyStatus(await commit(activeRepoPath, message));
      commitMessage = "";
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  // ── Commit restore ─────────────────────────────────────────────────────────

  async function restoreCommit(target: GitCommit) {
    if (!activeRepoPath) return;
    const confirmed = window.confirm(`Reset current branch to ${target.short_hash}?\n\nThis moves the current branch and discards tracked local changes.`);
    if (!confirmed) return;
    await runOperation(`Restoring ${target.short_hash}`, async () => {
      applyStatus(await restoreToCommit(activeRepoPath, target.hash));
      await refreshBranchList(activeRepoPath);
      await refreshCommitHistory(activeRepoPath);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  async function restoreCommitFile(target: GitCommit, file: GitCommitFile): Promise<boolean> {
    if (!activeRepoPath) return false;
    const confirmed = window.confirm(`Restore ${file.path} from ${target.short_hash}?\n\nThis changes the file in your working tree so you can review and commit it.`);
    if (!confirmed) return false;
    await runOperation(`Restoring ${file.path}`, async () => {
      applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, file.path));
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
    return !errorMessage;
  }

  async function previewCommitFileFromHistory(target: GitCommit, file: GitCommitFile) {
    if (!activeRepoPath) return;
    await runOperation(`Diffing ${file.path}`, async () => {
      const result = await compareFileToHead(activeRepoPath, target.hash, file.path);
      const matchingFile = result.files.find((diffFile) =>
        diffFile.path === file.path || diffFile.old_path === file.old_path || diffFile.old_path === file.path,
      );
      comparison = result;
      selectedDiffPath = matchingFile?.path ?? result.files[0]?.path ?? file.path;
      pendingRestoreFile = { commit: target, file };
      compareDialogOpen = true;
    });
  }

  // ── Explorer interaction ───────────────────────────────────────────────────

  function toggleExplorerFolder(node: ExplorerNode) {
    if (node.kind !== "folder") return;
    const next = new Set(expandedExplorerPaths);
    if (next.has(node.path)) next.delete(node.path); else next.add(node.path);
    expandedExplorerPaths = next;
  }

  async function selectExplorerNode(node: ExplorerNode) {
    if (!activeRepoPath || (selectedExplorerPath === node.path && selectedExplorerKind === node.kind)) return;
    selectedExplorerPath = node.path;
    selectedExplorerKind = node.kind;
    await runOperation(`Loading ${node.path} history`, async () => {
      await refreshFileHistory(activeRepoPath, node.path);
    });
  }

  async function restoreSelectedFileFromCommit(target: GitCommit) {
    if (!activeRepoPath || !selectedExplorerPath) return;
    const kind = selectedExplorerKind === "folder" ? "folder" : "file";
    const confirmed = window.confirm(`Restore ${kind} ${selectedExplorerPath} from ${target.short_hash}?\n\nThis changes the selected ${kind} in your working tree so you can review and commit it.`);
    if (!confirmed) return;
    await runOperation(`Restoring ${selectedExplorerPath}`, async () => {
      applyStatus(await restoreFileFromCommit(activeRepoPath, target.hash, selectedExplorerPath));
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);
    });
  }

  // ── Compare ────────────────────────────────────────────────────────────────

  async function compareSelectedCommits() {
    if (!canCompare) return;
    await runOperation("Comparing commits", async () => {
      const result = await compareCommits(activeRepoPath, compareFrom, compareTo);
      comparison = result;
      selectedDiffPath = result.files[0]?.path ?? "";
      pendingRestoreFile = null;
      compareDialogOpen = true;
    });
  }

  async function diffSelectedFileFromCommit(historyCommit: GitCommit) {
    if (!activeRepoPath || !selectedExplorerPath) return;
    await runOperation(`Diffing ${selectedExplorerPath}`, async () => {
      const result = await diffFileAgainstWorkingTree(activeRepoPath, historyCommit.hash, selectedExplorerPath);
      comparison = result;
      selectedDiffPath = result.files[0]?.path ?? selectedExplorerPath;
      pendingRestoreFile = null;
      compareDialogOpen = true;
    });
  }

  function openCompareDialog() {
    if (comparison) compareDialogOpen = true;
  }

  function closeCompareDialog() {
    compareDialogOpen = false;
    pendingRestoreFile = null;
  }

  async function restorePreviewedCommitFile() {
    if (!pendingRestoreFile) return;
    const restored = await restoreCommitFile(pendingRestoreFile.commit, pendingRestoreFile.file);
    if (restored) closeCompareDialog();
  }

  function selectDiffFile(file: GitDiffFile) {
    selectedDiffPath = file.path;
  }

  async function runGlobalSearch(query: string, caseSensitive: boolean, limit: number) {
    if (!activeRepoPath || globalSearchBusy) return;
    const searchId = `search-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    globalSearchId = searchId;
    globalSearchBusy = true;
    globalSearchError = "";
    globalSearchResults = [];

    try {
      const results = await searchCodeIntroductions(activeRepoPath, query, caseSensitive, limit, searchId);
      if (globalSearchId === searchId) {
        globalSearchResults = results;
      }
    } catch (error) {
      if (globalSearchId === searchId) {
        const message = errorToMessage(error);
        globalSearchError = message.includes("abgebrochen") ? "Suche wurde abgebrochen." : message;
      }
    } finally {
      if (globalSearchId === searchId) {
        globalSearchBusy = false;
        globalSearchId = "";
      }
    }
  }

  async function cancelGlobalSearch() {
    if (!globalSearchId) return;
    const searchId = globalSearchId;
    globalSearchError = "Abbruch wird angefordert...";
    try {
      await cancelCodeSearch(searchId);
    } catch (error) {
      globalSearchError = errorToMessage(error);
    }
  }

  function closeGlobalSearchDialog() {
    if (globalSearchBusy) void cancelGlobalSearch();
    globalSearchOpen = false;
  }

  // ── Conflict resolution ────────────────────────────────────────────────────

  async function loadConflict(file: string) {
    conflictTarget = file;
    conflict = await readConflict(activeRepoPath, file);
  }

  async function openResolveDialog() {
    if (!hasConflicts || isBusy) return;
    const first = conflictedFiles[0].path;
    await runOperation("Loading conflicts", async () => {
      preparedResolutions = {};
      resolveDialogOpen = true;
      await loadConflict(first);
    });
  }

  async function selectConflictFile(path: string) {
    if (path === conflictTarget || isBusy) return;
    await runOperation(`Loading ${path}`, async () => {
      await loadConflict(path);
    });
  }

  async function handleMarkResolved(path: string, resolution: PreparedResolution) {
    preparedResolutions = { ...preparedResolutions, [path]: resolution };

    const next = conflictedFiles.find((f) => f.path !== path && preparedResolutions[f.path] == null);
    if (next) {
      await runOperation(`Loading ${next.path}`, async () => {
        await loadConflict(next.path);
      });
    }
  }

  async function applyPreparedResolutions() {
    if (!activeRepoPath || isBusy || Object.keys(preparedResolutions).length === 0) return;
    const entries = Object.entries(preparedResolutions);
    await runOperation(`Resolving ${entries.length} ${entries.length === 1 ? "file" : "files"}`, async () => {
      let nextStatus: GitStatus | null = null;
      for (const [file, prepared] of entries) {
        nextStatus = prepared.kind === "side"
          ? await resolveConflictSide(activeRepoPath, file, prepared.side)
          : await resolveConflict(activeRepoPath, file, prepared.content);
      }
      preparedResolutions = {};
      if (nextStatus) applyStatus(nextStatus);
      await refreshExplorerFiles(activeRepoPath);
      await refreshFileHistory(activeRepoPath);

      const remaining = (nextStatus?.files ?? status?.files ?? []).filter(
        (f) => f.staged === "conflicted" || f.unstaged === "conflicted",
      );
      if (remaining.length === 0) {
        resolveDialogOpen = false;
        conflict = null;
        conflictTarget = "";
      } else {
        await loadConflict(remaining[0].path);
      }
    });
  }

  // ── Event handlers ─────────────────────────────────────────────────────────

  function submitRepo(event: SubmitEvent) { event.preventDefault(); void openRepo(); }

  function handleWindowKeydown(event: KeyboardEvent) {
    if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
    else if (event.key === "Escape" && globalSearchOpen) closeGlobalSearchDialog();
  }
</script>

<svelte:head>
  <title>GitLite</title>
</svelte:head>

<svelte:window on:keydown={handleWindowKeydown} />

<main class="shell">
  <TitleBar
    branch={status?.current_branch ?? ""}
    ahead={status?.ahead ?? 0}
    behind={status?.behind ?? 0}
    repoName={activeRepoPath ? (activeRepoPath.split(/[\\/]/).filter(Boolean).slice(-1)[0] ?? "") : ""}
    {hasRepository}
    {isBusy}
    {operation}
    {autoRefreshEnabled}
    {autoRefreshInFlight}
    onPull={pullRepo}
    onPush={pushRepo}
    onRefresh={refreshRepo}
    onSearch={() => { globalSearchOpen = true; }}
    onToggleAutoRefresh={toggleAutoRefresh}
  />

  <div class="shell-body">

    <!-- Repository path form -->
    <header class="topbar">
      <form class="repo-form" onsubmit={submitRepo}>
        <label for="repo-path">Repository</label>
        <input
          id="repo-path"
          bind:value={repoPath}
          autocomplete="off"
          spellcheck="false"
          placeholder="/path/to/repository"
          disabled={isBusy}
        />
        <button
          class="btn-secondary repo-browse"
          type="button"
          onclick={chooseRepositoryFolder}
          disabled={isBusy}
          title="Repository-Ordner auswaehlen"
          aria-label="Repository-Ordner auswaehlen"
        >
          <FolderOpen size={16} aria-hidden="true" />
          Browse
        </button>
        <button class="btn-primary" type="submit" disabled={isBusy || repoPath.trim().length === 0}>
          {#if operation === "Opening repository"}
            <LoaderCircle class="spin" size={16} aria-hidden="true" />
          {:else}
            <Check size={16} aria-hidden="true" />
          {/if}
          Open
        </button>
      </form>
    </header>

    <!-- Status notices -->
    {#if errorMessage}
      <section class="notice error" role="alert">
        <AlertCircle size={17} aria-hidden="true" />
        <span>{errorMessage}</span>
      </section>
    {/if}

    {#if operation && operation !== "Opening repository"}
      <section class="notice busy" aria-live="polite">
        <LoaderCircle class="spin" size={17} aria-hidden="true" />
        <span>{operation}</span>
      </section>
    {/if}

    {#if hasConflicts}
      <section class="notice conflict" role="alert">
        <GitMerge size={17} aria-hidden="true" />
        <span>{conflictedFiles.length} {conflictedFiles.length === 1 ? "file has" : "files have"} merge conflicts.</span>
        <button type="button" onclick={openResolveDialog} disabled={isBusy}>Resolve conflicts</button>
      </section>
    {/if}

    <!-- Workspace -->
    <section class="workspace" aria-label="Git workspace">

      <!-- Left sidebar: branches + explorer -->
      <aside class="left-sidebar" aria-label="Repository navigation">
        <BranchPanel
          {branches}
          {localBranches}
          {remoteBranches}
          {hasRepository}
          {isBusy}
          onCheckout={checkout}
          onMerge={merge}
        />
        <ExplorerPanel
          {repoFiles}
          {expandedExplorerPaths}
          {selectedExplorerPath}
          {selectedExplorerKind}
          {hasRepository}
          {isBusy}
          onToggleFolder={toggleExplorerFolder}
          onSelectNode={selectExplorerNode}
        />
      </aside>

      <!-- Center: summary + status + commit + compare -->
      <section class="main-panel" aria-label="Repository status">
        <div class="repo-summary">
          <div class="repo-meta">
            <GitBranch size={13} aria-hidden="true" />
            <strong class="repo-branch">{status?.current_branch ?? "No repository"}</strong>
            {#if activeRepoPath}
              <span class="repo-path" title={activeRepoPath}>{activeRepoPath}</span>
            {/if}
          </div>
          <div class="sync-stats" aria-label="Sync state">
            {#if status?.upstream}<span title="Upstream">{status.upstream}</span>{/if}
            <strong>{status?.ahead ?? 0} ahead</strong>
            <strong>{status?.behind ?? 0} behind</strong>
          </div>
        </div>

        <div class="top-section">
          <StatusPanel
            {changedFiles}
            {stagedCount}
            {unstagedCount}
            {hasRepository}
            {isBusy}
            {status}
            onStage={stageFile}
            onUnstage={unstageFile}
            onDiscard={discardFile}
            onStageAll={stageAllFiles}
            onUnstageAll={unstageAllFiles}
          />
          <CommitPanel
            {commitMessage}
            {canCommit}
            {commitBlockReason}
            {hasRepository}
            {isBusy}
            {operation}
            {stagedCount}
            onCommit={commitChanges}
            onCommitMessageChange={(msg) => { commitMessage = msg; }}
          />
        </div>

        <ComparePanel
          {commits}
          {hasRepository}
          {isBusy}
          {compareFrom}
          {compareTo}
          {canCompare}
          {comparison}
          {operation}
          onCompareFromChange={(val) => { compareFrom = val; }}
          onCompareToChange={(val) => { compareTo = val; }}
          onCompare={compareSelectedCommits}
          onOpenDialog={openCompareDialog}
        />
      </section>

      <!-- Right sidebar: commit graph + file history -->
      <aside class="history-aside" aria-label="Commit history">
        <HistoryPanel
          {commits}
          {hasRepository}
          {isBusy}
          {expandedCommitHashes}
          onRestoreCommit={restoreCommit}
          onPreviewCommitFile={previewCommitFileFromHistory}
          onToggleCommitFiles={(hash) => {
            const next = new Set(expandedCommitHashes);
            if (next.has(hash)) next.delete(hash); else next.add(hash);
            expandedCommitHashes = next;
          }}
        />
        <FileHistoryPanel
          {fileHistory}
          {selectedExplorerPath}
          selectedExplorerLabel={selectedExplorerPath ? `${selectedExplorerKind === "folder" ? "Folder" : "File"} history` : "File history"}
          {hasRepository}
          {isBusy}
          onDiff={diffSelectedFileFromCommit}
          onRestore={restoreSelectedFileFromCommit}
        />
      </aside>
    </section>
  </div>
</main>

<!-- Compare diff dialog -->
{#if compareDialogOpen && comparison}
  <CompareDialog
    {comparison}
    {selectedDiffPath}
    {isBusy}
    restoreLabel={pendingRestoreFile ? "Restore file" : ""}
    onClose={closeCompareDialog}
    onRestore={restorePreviewedCommitFile}
    onSelectFile={selectDiffFile}
  />
{/if}

{#if globalSearchOpen}
  <GlobalSearchDialog
    {hasRepository}
    {isBusy}
    isSearching={globalSearchBusy}
    error={globalSearchError}
    results={globalSearchResults}
    onClose={closeGlobalSearchDialog}
    onSearch={runGlobalSearch}
    onCancel={cancelGlobalSearch}
  />
{/if}

<!-- Credential dialog for push/pull -->
{#if credDialogOpen && credDialogAction}
  <CredentialDialog
    action={credDialogAction}
    error={credDialogError}
    {isBusy}
    onSubmit={handleCredentialSubmit}
    onCancel={() => { credDialogOpen = false; credDialogAction = null; credDialogError = ""; }}
  />
{/if}

<!-- Conflict resolve dialog -->
{#if resolveDialogOpen}
  <ResolveDialog
    {conflictedFiles}
    {conflictTarget}
    {conflict}
    {preparedResolutions}
    {isBusy}
    {operation}
    onClose={() => { resolveDialogOpen = false; }}
    onSelectFile={selectConflictFile}
    onMarkResolved={handleMarkResolved}
    onApply={applyPreparedResolutions}
  />
{/if}
