import { fetchCurrentUser, fetchRepoBranches, fetchRepoContents, fetchRepositories } from "./gitea-api.js";
import { getActiveServer, getState, setSettings, updateSettings, addRecentRepo } from "./state.js";
import {
  browseApplication,
  browseDirectory,
  checkoutBranch,
  commitChanges,
  createBranch,
  deleteBranch,
  getCommitDetail,
  getCommitHistory,
  getFileDiff,
  getRepositorySyncStatus,
  getWorkingTreeStatus,
  listLocalRepoBranches,
  listLocalRepoTree,
  openInExternalEditor,
  openInFileExplorer,
  readLocalRepoFile,
  renameBranch,
  runGitBranch,
  runGitClone,
  runGitFetch,
  runGitPull,
  runGitPublishBranch,
  runGitPush,
  runGitSync,
  runGitStatus,
  scanInstalledIdes,
  scanLocalRepos,
  testGiteaConnection,
} from "./tauri-api.js";

const appRoot = document.getElementById("app");

const mockRepos = [
  { id: 1, full_name: "alice/portfolio", private: false, clone_url: "https://example.com/portfolio.git", updated_at: "today", owner: { login: "alice", type: "User" } },
  { id: 2, full_name: "alice/dotfiles", private: true, clone_url: "https://example.com/dotfiles.git", updated_at: "yesterday", owner: { login: "alice", type: "User" } },
  { id: 3, full_name: "acme-corp/client-ui", private: false, clone_url: "https://example.com/client-ui.git", updated_at: "today", owner: { login: "acme-corp", type: "Organization" } },
  { id: 4, full_name: "acme-corp/api-server", private: true, clone_url: "https://example.com/api-server.git", updated_at: "3 days ago", owner: { login: "acme-corp", type: "Organization" } },
  { id: 5, full_name: "oss-collective/toolkit", private: false, clone_url: "https://example.com/toolkit.git", updated_at: "last week", owner: { login: "oss-collective", type: "Organization" } },
];

let repositories = [...mockRepos];
let currentUserLogin = "";
let serverTestResult = "";
let settingsNotice = "";
let gitOutput = "";
let activeView = "changes"; // "changes" | "history"
let activeModal = ""; // "" | "repos" | "clone" | "servers" | "settings" | "viewer"
let utilityMenuOpen = false;
let repoOwnerFilter = "all";
let activeReposTab = "local"; // "local" | "server"
const maxPreviewBytes = 256 * 1024;
const defaultRepositoryName = "Gitpub-Desktop";
const defaultBranchName = "main";
const DEFAULT_EDITOR_VALUE = "__default_code__";
const CUSTOM_EDITOR_VALUE = "__custom__";

function disableInputAutocomplete(root = document) {
  root.querySelectorAll("input, textarea").forEach((field) => {
    if (field instanceof HTMLInputElement && ["button", "checkbox", "file", "hidden", "radio", "reset", "submit"].includes(field.type)) {
      return;
    }

    field.setAttribute("autocomplete", field instanceof HTMLInputElement && field.type === "password" ? "new-password" : "off");
  });
}

function escapeSvgAttr(value = "") {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function svgAttrs(attrs = {}) {
  return Object.entries(attrs)
    .filter(([, value]) => value !== undefined && value !== null && value !== false)
    .map(([key, value]) => `${key}="${escapeSvgAttr(value)}"`)
    .join(" ");
}

function lucideIcon(name, { size = 16, className = "", strokeWidth = 2, attrs = {} } = {}) {
  const icon = window.lucide?.icons?.[name];
  if (!icon) return "";

  const iconAttrs = svgAttrs({
    xmlns: "http://www.w3.org/2000/svg",
    width: size,
    height: size,
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    "stroke-width": strokeWidth,
    "stroke-linecap": "round",
    "stroke-linejoin": "round",
    "aria-hidden": "true",
    focusable: "false",
    class: ["gd-lucide-icon", className].filter(Boolean).join(" "),
    ...attrs,
  });
  const children = icon.map(([tag, childAttrs]) => `<${tag} ${svgAttrs(childAttrs)} />`).join("");
  return `<svg ${iconAttrs}>${children}</svg>`;
}

const FOLDER_ICON = lucideIcon("Folder", { size: 16, className: "gd-folder-icon" });
const FILE_ICON = lucideIcon("File", { size: 16, className: "gd-file-icon" });
const LOCAL_REPO_ICON = lucideIcon("Monitor", { size: 18 });
const BRANCH_ICON = lucideIcon("GitBranch", { size: 16 });
const SYNC_ICON = lucideIcon("RefreshCw", { size: 15 });
const PULL_ICON = lucideIcon("Download", { size: 15 });
const PUSH_ICON = lucideIcon("Upload", { size: 15 });
const PUBLISH_ICON = lucideIcon("CloudUpload", { size: 15 });
const EXPLORER_ICON = lucideIcon("FolderOpen", { size: 15 });
const EDITOR_ICON = lucideIcon("SquarePen", { size: 15 });
const WINDOW_MINIMIZE_ICON = lucideIcon("Minus", { size: 12 });
const WINDOW_MAXIMIZE_ICON = lucideIcon("Square", { size: 12 });
const WINDOW_CLOSE_ICON = lucideIcon("X", { size: 12 });
const CHEVRON_DOWN_ICON = lucideIcon("ChevronDown", { size: 10, className: "gd-cell-caret" });
const MORE_ICON = lucideIcon("Ellipsis", { size: 15 });
const REFRESH_ICON = lucideIcon("RefreshCw", { size: 14 });
const REPOSITORY_ICON = lucideIcon("BookOpen", { size: 14 });
const CLONE_ICON = lucideIcon("GitFork", { size: 14 });
const SERVER_ICON = lucideIcon("Server", { size: 14 });
const SETTINGS_ICON = lucideIcon("Settings", { size: 14 });
const PLUS_ICON = lucideIcon("Plus", { size: 14 });
const CHECK_ICON = lucideIcon("Check", { size: 13 });
const EMPTY_DIFF_ICON = lucideIcon("FileDiff", { size: 44, strokeWidth: 1.6 });
const EMPTY_FILE_ICON = lucideIcon("File", { size: 44, strokeWidth: 1.6 });
const EMPTY_REPO_ICON = lucideIcon("FolderGit2", { size: 44, strokeWidth: 1.6 });

function errorMessage(error, fallback = "Unknown error.") {
  if (typeof error === "string" && error.trim()) return error;
  if (error instanceof Error && error.message) return error.message;
  if (error && typeof error === "object") {
    if (typeof error.message === "string" && error.message.trim()) return error.message;
    try {
      const serialized = JSON.stringify(error);
      if (serialized && serialized !== "{}") return serialized;
    } catch {
      // Fall through to the generic fallback.
    }
  }
  return fallback;
}

function currentPlatform() {
  const platform = navigator.userAgentData?.platform || navigator.platform || "";
  const userAgent = navigator.userAgent || "";
  const value = `${platform} ${userAgent}`.toLowerCase();

  if (value.includes("mac")) return "macos";
  if (value.includes("win")) return "windows";
  return "linux";
}

function applyPlatformChrome() {
  appRoot.dataset.platform = currentPlatform();
}

function currentTauriWindow() {
  const tauriWindow = window.__TAURI__?.window;
  if (tauriWindow?.getCurrentWindow) {
    return tauriWindow.getCurrentWindow();
  }
  return tauriWindow?.appWindow || null;
}

async function handleWindowAction(action) {
  const appWindow = currentTauriWindow();
  if (!appWindow) {
    console.warn("Tauri window API not available");
    return;
  }

  try {
    if (action === "minimize") {
      await appWindow.minimize();
    } else if (action === "maximize") {
      const isMaximized = await appWindow.isMaximized();
      if (isMaximized) {
        await appWindow.unmaximize();
      } else {
        await appWindow.maximize();
      }
    } else if (action === "close") {
      await appWindow.close();
    }
  } catch (error) {
    console.error(`Unable to ${action} window:`, error);
  }
}

async function startWindowDrag() {
  const appWindow = currentTauriWindow();
  if (!appWindow) return;

  try {
    await appWindow.startDragging();
  } catch (error) {
    console.error("Unable to start window drag:", error);
  }
}

function uid() {
  return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
}

function escapeHtml(value = "") {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;");
}

function formatBytes(value) {
  if (!Number.isFinite(value)) return "";
  if (value < 1024) return `${value} B`;
  if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`;
  return `${(value / 1024 / 1024).toFixed(1)} MB`;
}

function parentPath(path = "") {
  const parts = path.split("/").filter(Boolean);
  parts.pop();
  return parts.join("/");
}

function repoNameFromPath(path = "") {
  return path.split(/[/\\]/).filter(Boolean).pop() || path;
}

function parentFolderName(path = "") {
  const parts = path.split(/[/\\]/).filter(Boolean);
  return parts.length > 1 ? parts[parts.length - 2] : "Local";
}

function repoNameFromUrl(url = "") {
  const cleanUrl = url.trim().split(/[?#]/)[0].replace(/\/+$/, "").replace(/\.git$/i, "");
  return cleanUrl.split(/[/\\:]/).filter(Boolean).pop() || "";
}

function joinDirectoryPath(parent = "", child = "") {
  if (!parent || !child) return parent || child;
  const separator = parent.includes("\\") ? "\\" : "/";
  return `${parent.replace(/[\\/]+$/, "")}${separator}${child}`;
}

function currentRepositoryName() {
  return getState().selectedRepoName || defaultRepositoryName;
}

function currentBranchName() {
  if (getState().sync.isDetached) return "Detached HEAD";
  return getState().workingTree.branch || getState().viewer.branch || defaultBranchName;
}

function normalizeRemoteUrl(value = "") {
  return value.trim().replace(/\/+$/, "").replace(/\.git$/i, "").toLowerCase();
}

function serverRepoRemoteUrls() {
  const urls = repositories.flatMap((repo) => [
    repo.clone_url,
    repo.ssh_url,
    repo.html_url,
    repo.original_url,
  ]);
  return [...new Set(urls.map((url) => normalizeRemoteUrl(url || "")).filter(Boolean))];
}

function repoOwnerName(repo) {
  return repo.owner?.login || repo.full_name.split("/")[0] || "Unknown";
}

function repoRemoteUrls(repo) {
  return [repo.clone_url, repo.ssh_url, repo.html_url, repo.original_url]
    .map((url) => normalizeRemoteUrl(url || ""))
    .filter(Boolean);
}

function matchingServerRepoByRemote(remoteUrl = "") {
  const normalizedRemote = normalizeRemoteUrl(remoteUrl);
  if (!normalizedRemote) return null;
  return repositories.find((repo) => repoRemoteUrls(repo).includes(normalizedRemote)) || null;
}

function matchingServerRepoByName(name = "") {
  const normalizedName = name.trim().toLowerCase();
  if (!normalizedName) return null;
  return repositories.find((repo) => (repo.full_name.split("/").pop() || "").toLowerCase() === normalizedName) || null;
}

function applyTheme() {
  const theme = getState().settings.theme || "dark";
  const systemPrefersLight = window.matchMedia?.("(prefers-color-scheme: light)")?.matches;
  document.documentElement.dataset.theme = theme === "system" && systemPrefersLight ? "light" : theme;
}

function selectedGitPath() {
  return getState().settings.gitExecutablePath;
}

function isDetectedEditorPath(value = "", detectedEditors = []) {
  const normalizedValue = value.trim().toLowerCase();
  return detectedEditors.some((editor) => editor.executablePath.toLowerCase() === normalizedValue);
}

function selectedEditorDropdownValue(state) {
  const editorPath = state.settings.externalEditorPath?.trim() || "";
  if (!editorPath) return DEFAULT_EDITOR_VALUE;
  return isDetectedEditorPath(editorPath, state.installedIdes) ? editorPath : CUSTOM_EDITOR_VALUE;
}

function externalEditorOptionsTemplate(state) {
  const editorPath = state.settings.externalEditorPath?.trim() || "";
  const selectedValue = selectedEditorDropdownValue(state);
  const detectedOptions = state.installedIdes
    .map((editor) => `<option value="${escapeHtml(editor.executablePath)}" ${selectedValue === editor.executablePath ? "selected" : ""}>${escapeHtml(editor.name)}</option>`)
    .join("");

  return `
    <option value="${DEFAULT_EDITOR_VALUE}" ${selectedValue === DEFAULT_EDITOR_VALUE ? "selected" : ""}>VS Code command (code)</option>
    ${detectedOptions}
    <option value="${CUSTOM_EDITOR_VALUE}" ${selectedValue === CUSTOM_EDITOR_VALUE ? "selected" : ""}>Custom application…</option>
  `;
}

function windowControlsTemplate(isMacos = false) {
  const controls = isMacos
    ? [
        { action: "close", label: "Close", icon: WINDOW_CLOSE_ICON, className: "gd-window-close" },
        { action: "minimize", label: "Minimize", icon: WINDOW_MINIMIZE_ICON, className: "gd-window-minimize" },
        { action: "maximize", label: "Maximize", icon: WINDOW_MAXIMIZE_ICON, className: "gd-window-maximize" },
      ]
    : [
        { action: "minimize", label: "Minimize", icon: WINDOW_MINIMIZE_ICON, className: "gd-window-minimize" },
        { action: "maximize", label: "Maximize", icon: WINDOW_MAXIMIZE_ICON, className: "gd-window-maximize" },
        { action: "close", label: "Close", icon: WINDOW_CLOSE_ICON, className: "gd-window-close" },
      ];

  return `<div class="gd-window-controls" role="group" aria-label="Window controls">
    ${controls.map((control) => `
      <button class="gd-window-control ${control.className}" type="button" data-window-action="${control.action}" title="${control.label}" aria-label="${control.label} window">${control.icon}</button>
    `).join("")}
  </div>`;
}

function statusLabel(status = "") {
  const labels = {
    modified: "Modified",
    added: "Added",
    deleted: "Deleted",
    renamed: "Renamed",
    untracked: "Untracked",
    conflicted: "Conflicted",
  };
  return labels[status] || "Modified";
}

function pluralize(count, singular, plural = `${singular}s`) {
  return `${count} ${count === 1 ? singular : plural}`;
}

function remoteDisplayName(sync = {}) {
  return sync.upstreamRemote || sync.defaultRemote || "origin";
}

function syncButtonConfig(state) {
  if (!state.selectedRepoPath) {
    return {
      label: "No Repository",
      subLabel: "Select a repo to begin",
      tooltip: "Select a repository to sync.",
      icon: SYNC_ICON,
      action: "",
      disabled: true,
    };
  }

  const sync = state.sync;
  const remote = remoteDisplayName(sync);
  const operationLabels = {
    fetch: "Fetching…",
    pull: "Pulling…",
    push: "Pushing…",
    sync: "Syncing…",
    publish: "Publishing…",
  };

  if (sync.operation) {
    return {
      label: operationLabels[sync.operation] || "Working…",
      subLabel: "Git operation in progress",
      tooltip: "A Git operation is already running.",
      icon: sync.operation === "pull" ? PULL_ICON : sync.operation === "publish" || sync.operation === "push" ? PUSH_ICON : SYNC_ICON,
      action: sync.operation,
      disabled: true,
      loading: true,
    };
  }

  if (sync.loading && !sync.lastUpdated) {
    return {
      label: "Loading…",
      subLabel: "Reading repository state",
      tooltip: "Reading repository sync state.",
      icon: SYNC_ICON,
      action: "",
      disabled: true,
      loading: true,
    };
  }

  if (sync.isDetached) {
    return {
      label: "Detached HEAD",
      subLabel: "Checkout a branch to sync",
      tooltip: "Detached HEAD cannot be pushed or pulled. Checkout a branch first.",
      icon: BRANCH_ICON,
      action: "",
      disabled: true,
    };
  }

  if (!sync.hasRemote && sync.lastUpdated) {
    return {
      label: "No remote",
      subLabel: "Add a remote to sync",
      tooltip: "This repository does not have a Git remote configured.",
      icon: SYNC_ICON,
      action: "",
      disabled: true,
    };
  }

  if (sync.isUnpublished || (!sync.upstream && sync.hasRemote)) {
    return {
      label: "Publish branch",
      subLabel: `Publish ${sync.branch || "branch"} to ${remote}`,
      tooltip: `Publish ${sync.branch || "the current branch"} to ${remote}.`,
      icon: PUBLISH_ICON,
      action: "publish",
      disabled: false,
    };
  }

  if (sync.behind > 0 && sync.ahead > 0) {
    return {
      label: "Sync changes",
      subLabel: `${pluralize(sync.behind, "commit")} to pull · ${pluralize(sync.ahead, "commit")} to push`,
      tooltip: `${pluralize(sync.behind, "commit")} to pull and ${pluralize(sync.ahead, "commit")} ready to push.`,
      icon: SYNC_ICON,
      action: "sync",
      disabled: false,
    };
  }

  if (sync.behind > 0) {
    return {
      label: `Pull ${remote}`,
      subLabel: `${pluralize(sync.behind, "commit")} to pull`,
      tooltip: `${pluralize(sync.behind, "commit")} to pull from ${remote}.`,
      icon: PULL_ICON,
      action: "pull",
      disabled: false,
    };
  }

  if (sync.ahead > 0) {
    return {
      label: `Push ${remote}`,
      subLabel: `${pluralize(sync.ahead, "commit")} ready to push`,
      tooltip: `${pluralize(sync.ahead, "commit")} ready to push to ${remote}.`,
      icon: PUSH_ICON,
      action: "push",
      disabled: false,
    };
  }

  return {
    label: `Fetch ${remote}`,
    subLabel: sync.error || "Your branch is up to date",
    tooltip: sync.error ? `Last sync check failed: ${sync.error}` : "Your branch is up to date.",
    icon: SYNC_ICON,
    action: "fetch",
    disabled: false,
  };
}

function groupedChangedFiles(files = []) {
  const order = ["modified", "added", "deleted", "renamed", "untracked", "conflicted"];
  const groups = new Map(order.map((status) => [status, []]));
  files.forEach((file) => {
    const key = groups.has(file.status) ? file.status : "modified";
    groups.get(key).push(file);
  });
  return order.map((status) => ({ status, files: groups.get(status) })).filter((group) => group.files.length);
}

function diffBinaryImagePreviewTemplate(diffResult) {
  const mime = escapeHtml(diffResult.previewMime || "");
  const b64 = diffResult.previewBase64 || "";
  const caption = diffResult.diff ? escapeHtml(diffResult.diff) : "";
  return `<div class="diff-preview diff-binary-image-panel">
    <div class="diff-image-preview-frame">
      <img class="diff-image-preview" alt="" decoding="async" src="data:${mime};base64,${b64}" />
    </div>
    ${caption ? `<pre class="diff-binary-caption">${caption}</pre>` : ""}
  </div>`;
}

function diffTemplate(diffResult) {
  if (!diffResult) {
    return workflowEmptyStateTemplate({
      title: "Select a changed file",
      message: "Choose a file from the changes list to inspect its diff before committing.",
      icon: "diff",
      actions: false,
    });
  }
  if (diffResult.previewBase64 && diffResult.previewMime) {
    return diffBinaryImagePreviewTemplate(diffResult);
  }
  if (diffResult.isBinary) {
    return workflowEmptyStateTemplate({
      title: "Binary file",
      message: diffResult.diff || "Diff preview is not available for binary files.",
      icon: "file",
      actions: false,
    });
  }
  if (diffResult.isDeleted && !diffResult.diff) {
    return workflowEmptyStateTemplate({
      title: "Deleted file",
      message: "This file was removed from the working tree.",
      icon: "file",
      actions: false,
    });
  }
  return `<div class="diff-preview diff-preview-inline">${renderDiff(diffResult.diff || "")}</div>`;
}

function branchDisplayName(branch) {
  return branch.localName || branch.name;
}

function branchOptionTemplate(branch, switchingTo = "") {
  const displayName = branchDisplayName(branch);
  const isSwitching = switchingTo === branch.name || switchingTo === displayName;
  const status = isSwitching ? "Switching..." : branch.current ? "Current" : branch.isRemote ? (branch.remoteName || "Remote") : "";
  const title = branch.isRemote
    ? `Checkout ${displayName} as a local branch tracking ${branch.name}`
    : `Checkout ${displayName}`;
  return `<button class="branch-menu-item ${branch.current ? "active" : ""}" data-branch-name="${escapeHtml(branch.name)}" type="button" title="${escapeHtml(title)}" ${switchingTo ? "disabled" : ""}>
    <span class="branch-menu-name">
      <span>${escapeHtml(displayName)}</span>
      ${branch.isRemote ? `<span class="branch-menu-subtitle">Remote: ${escapeHtml(branch.name)}</span>` : ""}
    </span>
    ${status ? `<span class="branch-current-mark">${escapeHtml(status)}</span>` : ""}
  </button>`;
}

function renderDiff(diff = "") {
  const lines = diff.split("\n");
  return lines
    .map((line) => {
      let kind = "context";
      if (line.startsWith("+++") || line.startsWith("---")) kind = "meta";
      else if (line.startsWith("@@")) kind = "hunk";
      else if (line.startsWith("+")) kind = "added";
      else if (line.startsWith("-")) kind = "removed";
      const sign = line.slice(0, 1) || " ";
      return `<div class="diff-line diff-line-${kind}">
        <span class="diff-gutter">${escapeHtml(sign)}</span>
        <code>${escapeHtml(line || " ")}</code>
      </div>`;
    })
    .join("");
}

function emptyStateIcon(type = "repo") {
  if (type === "diff") {
    return EMPTY_DIFF_ICON;
  }
  if (type === "file") {
    return EMPTY_FILE_ICON;
  }
  return EMPTY_REPO_ICON;
}

function workflowEmptyStateTemplate({ title, message, icon = "repo", actions = true }) {
  return `<div class="workflow-empty-state">
    <div class="workflow-empty-icon">${emptyStateIcon(icon)}</div>
    <h2>${escapeHtml(title)}</h2>
    <p>${escapeHtml(message)}</p>
    ${actions ? `<div class="workflow-empty-actions">
      <button class="primary-blue" type="button" data-open-modal="clone">Clone Repository</button>
      <button type="button" data-open-modal="repos">Open Repository</button>
    </div>` : ""}
  </div>`;
}

function openBranchDialog(mode, target = "") {
  const dialog = getState().branches.dialog;
  dialog.mode = mode;
  dialog.target = target;
  dialog.value = mode === "rename" ? target : "";
  dialog.error = "";
  getState().branches.menuOpen = false;
  render();
}

function closeBranchDialog() {
  Object.assign(getState().branches.dialog, { mode: "", target: "", value: "", error: "" });
  render();
}

function branchDialogTemplate(state, displayBranchName) {
  const dialog = state.branches.dialog;
  if (!dialog.mode) return "";
  const isDelete = dialog.mode === "delete";
  const title = dialog.mode === "create" ? "Create Branch" : dialog.mode === "rename" ? "Rename Branch" : "Delete Branch";
  const description = isDelete
    ? "Choose a local branch to delete. This only removes the local branch."
    : dialog.mode === "create"
      ? `Create a new branch from ${displayBranchName}.`
      : `Rename ${dialog.target}.`;
  return `<div class="modal-backdrop" role="presentation">
    <div class="modal-card" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}">
      <h3>${escapeHtml(title)}</h3>
      <p class="muted">${escapeHtml(description)}</p>
      ${isDelete ? `
        <select id="branch-dialog-input">
          <option value="">Select branch...</option>
          ${state.branches.items.filter((branch) => !branch.current).map((branch) => `<option value="${escapeHtml(branch.name)}" ${branch.name === dialog.target ? "selected" : ""}>${escapeHtml(branch.name)}</option>`).join("")}
        </select>
        <div class="danger-note">This action cannot delete the current branch and may fail if the branch is not fully merged.</div>
      ` : `
        <input id="branch-dialog-input" value="${escapeHtml(dialog.value)}" placeholder="Branch name" />
      `}
      ${dialog.error ? `<div class="viewer-error">${escapeHtml(dialog.error)}</div>` : ""}
      <div class="modal-actions">
        <button id="branch-dialog-cancel" type="button">Cancel</button>
        <button id="branch-dialog-confirm" class="${isDelete ? "danger" : "primary"}" type="button">${isDelete ? "Delete Branch" : "Save"}</button>
      </div>
    </div>
  </div>`;
}

function languageForPath(path = "") {
  const extension = path.split(".").pop()?.toLowerCase();
  const names = {
    js: "JavaScript",
    ts: "TypeScript",
    tsx: "TypeScript",
    jsx: "JavaScript",
    rs: "Rust",
    json: "JSON",
    css: "CSS",
    html: "HTML",
    htm: "HTML",
    md: "Markdown",
    toml: "TOML",
    yml: "YAML",
    yaml: "YAML",
  };
  return names[extension] || "Text";
}

function highlightCode(content = "", language = "Text") {
  let html = escapeHtml(content);
  if (["JavaScript", "TypeScript", "Rust"].includes(language)) {
    html = html.replace(
      /\b(async|await|const|let|var|function|return|if|else|for|while|class|struct|enum|impl|fn|pub|use|mod|match|Ok|Err|true|false|null)\b/g,
      '<span class="syntax-keyword">$1</span>'
    );
  } else if (language === "JSON") {
    html = html.replace(/(&quot;[^&]*?&quot;)(\s*:)/g, '<span class="syntax-key">$1</span>$2');
  } else if (language === "CSS") {
    html = html.replace(/([.#]?[a-zA-Z0-9_-]+)(\s*\{)/g, '<span class="syntax-key">$1</span>$2');
  }
  return html;
}

function isMarkdownPath(path = "") {
  return /\.(md|markdown)$/i.test(path);
}

function safeMarkdownHref(value = "") {
  const href = value.trim();
  const lowerHref = href.toLowerCase();
  if (!href || lowerHref.startsWith("javascript:") || lowerHref.startsWith("data:")) {
    return "";
  }
  return href;
}

function renderMarkdownInline(value = "") {
  return escapeHtml(value)
    .replace(/`([^`]+)`/g, "<code>$1</code>")
    .replace(/\[([^\]]+)\]\(([^)\s]+)(?:\s+&quot;[^&]*&quot;)?\)/g, (_match, label, href) => {
      const safeHref = safeMarkdownHref(href.replaceAll("&amp;", "&"));
      return safeHref ? `<a href="${escapeHtml(safeHref)}">${label}</a>` : label;
    })
    .replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
    .replace(/__([^_]+)__/g, "<strong>$1</strong>")
    .replace(/\*([^*]+)\*/g, "<em>$1</em>")
    .replace(/_([^_]+)_/g, "<em>$1</em>");
}

function renderMarkdown(content = "") {
  const lines = content.replace(/\r\n/g, "\n").split("\n");
  const html = [];
  let inFence = false;
  let fenceLanguage = "";
  let fenceLines = [];
  let listType = "";
  let listItems = [];

  const flushList = () => {
    if (!listType) return;
    html.push(`<${listType}>${listItems.map((item) => `<li>${renderMarkdownInline(item)}</li>`).join("")}</${listType}>`);
    listType = "";
    listItems = [];
  };

  const flushFence = () => {
    html.push(`<pre class="markdown-code" data-language="${escapeHtml(fenceLanguage)}"><code>${escapeHtml(fenceLines.join("\n"))}</code></pre>`);
    inFence = false;
    fenceLanguage = "";
    fenceLines = [];
  };

  for (const line of lines) {
    const fence = line.match(/^```(\w+)?\s*$/);
    if (fence) {
      if (inFence) {
        flushFence();
      } else {
        flushList();
        inFence = true;
        fenceLanguage = fence[1] || "";
        fenceLines = [];
      }
      continue;
    }

    if (inFence) {
      fenceLines.push(line);
      continue;
    }

    if (!line.trim()) {
      flushList();
      continue;
    }

    const heading = line.match(/^(#{1,6})\s+(.+)$/);
    if (heading) {
      flushList();
      const level = heading[1].length;
      html.push(`<h${level}>${renderMarkdownInline(heading[2])}</h${level}>`);
      continue;
    }

    if (/^\s*[-*_]{3,}\s*$/.test(line)) {
      flushList();
      html.push("<hr>");
      continue;
    }

    const quote = line.match(/^>\s?(.*)$/);
    if (quote) {
      flushList();
      html.push(`<blockquote>${renderMarkdownInline(quote[1])}</blockquote>`);
      continue;
    }

    const unordered = line.match(/^\s*[-*+]\s+(.+)$/);
    if (unordered) {
      if (listType && listType !== "ul") flushList();
      listType = "ul";
      listItems.push(unordered[1]);
      continue;
    }

    const ordered = line.match(/^\s*\d+\.\s+(.+)$/);
    if (ordered) {
      if (listType && listType !== "ol") flushList();
      listType = "ol";
      listItems.push(ordered[1]);
      continue;
    }

    flushList();
    html.push(`<p>${renderMarkdownInline(line)}</p>`);
  }

  if (inFence) flushFence();
  flushList();
  return html.join("");
}

function decodeBase64Content(content = "") {
  const cleaned = content.replace(/\s/g, "");
  const binary = atob(cleaned);
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
  if (bytes.byteLength > maxPreviewBytes) {
    return { content: "", size: bytes.byteLength, isBinary: false, tooLarge: true };
  }
  if (bytes.includes(0)) {
    return { content: "", size: bytes.byteLength, isBinary: true, tooLarge: false };
  }
  try {
    return {
      content: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
      size: bytes.byteLength,
      isBinary: false,
      tooLarge: false,
    };
  } catch {
    return { content: "", size: bytes.byteLength, isBinary: true, tooLarge: false };
  }
}

function normaliseRemoteEntries(contents) {
  const items = Array.isArray(contents) ? contents : [contents];
  return items
    .map((item) => ({
      name: item.name,
      path: item.path || item.name,
      type: item.type === "dir" ? "dir" : "file",
      size: item.size,
    }))
    .sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
}

function normaliseLocalEntries(entries) {
  return entries.map((entry) => ({
    name: entry.name,
    path: entry.path,
    type: entry.entryType === "tree" ? "dir" : "file",
    size: entry.size,
  }));
}

function serverFormTemplate(server = null) {
  const defaults = {
    id: "",
    displayName: "",
    serverUrl: "",
    authMethod: "token",
    token: "",
    username: "",
    password: "",
  };
  const config = { ...defaults, ...(server || {}) };
  return `
    <div class="stack" id="server-form-card">
      <h4 class="gd-modal-section-title">${server ? "Edit Server" : "Add Server"}</h4>
      <input type="hidden" name="id" value="${escapeHtml(config.id)}" />
      <div>
        <div class="label">Display name</div>
        <input name="displayName" value="${escapeHtml(config.displayName)}" placeholder="Gitpub Main" />
      </div>
      <div>
        <div class="label">Gitea server URL</div>
        <input name="serverUrl" value="${escapeHtml(config.serverUrl)}" placeholder="https://git.example.com" />
      </div>
      <div>
        <div class="label">Auth method</div>
        <select name="authMethod">
          <option value="token" ${config.authMethod === "token" ? "selected" : ""}>Access Token</option>
          <option value="password" ${config.authMethod === "password" ? "selected" : ""}>Username / Password</option>
        </select>
      </div>
      <div>
        <div class="label">Access token</div>
        <input name="token" value="${escapeHtml(config.token)}" placeholder="gitea token" />
      </div>
      <div class="row">
        <div style="flex:1">
          <div class="label">Username</div>
          <input name="username" value="${escapeHtml(config.username)}" placeholder="username" />
        </div>
        <div style="flex:1">
          <div class="label">Password</div>
          <input type="password" name="password" value="${escapeHtml(config.password)}" placeholder="password" />
        </div>
      </div>
      <div class="row wrap">
        <button id="test-server-btn" type="button">Test connection</button>
        <button id="save-server-btn" class="primary" type="button">Save server</button>
      </div>
      <div class="muted">${escapeHtml(serverTestResult || settingsNotice)}</div>
    </div>
  `;
}

function welcomeView() {
  appRoot.innerHTML = `
    <div class="welcome-wrap">
      <div class="welcome-card stack">
        <div class="welcome-logo">
          <img src="/assets/logos/Gitpub-Word-Logo-2-White.svg" alt="Gitpub" class="welcome-logo-img" />
          <span class="welcome-logo-desktop">Desktop</span>
        </div>
        <div class="panel stack">
          <h1 class="title">Welcome to Gitpub Desktop</h1>
          <p class="subtitle">
            Connect your first Gitea backend. Gitpub Desktop works with any compatible Gitea server.
          </p>
          ${serverFormTemplate(null)}
        </div>
      </div>
    </div>
  `;
  disableInputAutocomplete(appRoot);
  bindServerFormEvents();
}

function isOrgRepo(repo) {
  const ownerLogin = repo.owner?.login || repo.full_name.split("/")[0];
  if (currentUserLogin) {
    return ownerLogin.toLowerCase() !== currentUserLogin.toLowerCase();
  }
  return repo.owner?.type === "Organization" || repo.owner?.type === "organization";
}

function filteredRepositories() {
  const searchTerm = getState().repoSearch.trim().toLowerCase();
  let result = repositories;

  if (repoOwnerFilter === "personal") {
    result = result.filter((repo) => !isOrgRepo(repo));
  } else if (repoOwnerFilter === "orgs") {
    result = result.filter((repo) => isOrgRepo(repo));
  }

  if (searchTerm) {
    result = result.filter((repo) => repo.full_name.toLowerCase().includes(searchTerm));
  }

  return result;
}

function groupedByOrg(repos) {
  const groups = new Map();
  for (const repo of repos) {
    const org = repoOwnerName(repo);
    if (!groups.has(org)) groups.set(org, []);
    groups.get(org).push(repo);
  }
  return groups;
}

function localRepoOwnerName({ path = "", name = "", matchedRemoteUrl = "" } = {}) {
  const remoteRepo = matchingServerRepoByRemote(matchedRemoteUrl);
  if (remoteRepo) return repoOwnerName(remoteRepo);

  const repoName = name || repoNameFromPath(path);
  const namedRepo = matchingServerRepoByName(repoName);
  if (namedRepo) return repoOwnerName(namedRepo);

  return parentFolderName(path);
}

function groupedLocalRepos(items, ownerForItem) {
  const groups = new Map();
  for (const item of items) {
    const owner = ownerForItem(item) || "Local";
    if (!groups.has(owner)) groups.set(owner, []);
    groups.get(owner).push(item);
  }
  return groups;
}

function serverRepoListTemplate(repos) {
  const visibleRepos = repos.slice(0, 60);
  if (!visibleRepos.length) {
    return `<div class="gd-modal-empty">No repositories found</div>`;
  }

  return Array.from(groupedByOrg(visibleRepos).entries()).map(([owner, ownerRepos]) => `
    <div class="repo-owner-group">
      <div class="repo-owner-divider">
        <span>${escapeHtml(owner)}</span>
      </div>
      ${ownerRepos.map((repo) => `
        <div class="gd-modal-list-item">
          <div class="gd-modal-item-info">
            <strong class="accent">${escapeHtml(repo.full_name)}</strong>
            <span class="muted">${repo.private ? "Private" : "Public"} · ${escapeHtml(repo.updated_at || "recently")}</span>
          </div>
          <div class="row" style="gap:5px;flex-shrink:0">
            <button class="view-repo-btn" data-repo-name="${escapeHtml(repo.full_name)}" type="button" style="font-size:12px;padding:3px 8px">View</button>
            <button class="clone-repo-btn primary-blue" data-clone-url="${escapeHtml(repo.clone_url || "")}" type="button" style="font-size:12px;padding:3px 8px">Clone</button>
          </div>
        </div>`).join("")}
    </div>
  `).join("");
}

function recentLocalReposTemplate(paths = []) {
  if (!paths.length) {
    return `<div class="gd-modal-empty">No recent repositories</div>`;
  }

  return Array.from(groupedLocalRepos(paths, (path) => localRepoOwnerName({ path })).entries()).map(([owner, ownerPaths]) => `
    <div class="repo-owner-group">
      <div class="repo-owner-divider">
        <span>${escapeHtml(owner)}</span>
      </div>
      ${ownerPaths.map((path) => `
        <div class="gd-modal-list-item">
          <div class="gd-modal-item-info">
            <strong>${escapeHtml(repoNameFromPath(path))}</strong>
            <span class="muted gd-path-text" title="${escapeHtml(path)}">${escapeHtml(path)}</span>
          </div>
          <div class="row" style="gap:5px;flex-shrink:0">
            <button class="open-recent-repo-btn" data-recent-repo-path="${escapeHtml(path)}" type="button">Open</button>
            <button class="remove-recent-repo-btn danger" data-recent-repo-path="${escapeHtml(path)}" type="button" title="Remove from list" aria-label="Remove from list">${WINDOW_CLOSE_ICON}</button>
          </div>
        </div>`).join("")}
    </div>
  `).join("");
}

function localRepoScanTemplate() {
  const state = getState();
  const results = state.localRepoScanResults || [];

  if (state.localRepoScanLoading) {
    return `<div class="viewer-loading">Scanning local folders…</div>`;
  }

  if (state.localRepoScanError) {
    return `<div class="viewer-error">${escapeHtml(state.localRepoScanError)}</div>`;
  }

  if (!results.length) {
    return `<div class="gd-modal-empty">No local repositories scanned yet. Only repos with remotes from the selected server will appear.</div>`;
  }

  return `<div class="gd-modal-scan-list">
    ${Array.from(groupedLocalRepos(results, (repo) => localRepoOwnerName(repo)).entries()).map(([owner, ownerRepos]) => `
      <div class="repo-owner-group">
        <div class="repo-owner-divider">
          <span>${escapeHtml(owner)}</span>
        </div>
        ${ownerRepos.map((repo) => `
          <div class="gd-modal-list-item">
            <div class="gd-modal-item-info">
              <strong>${escapeHtml(repo.name || repoNameFromPath(repo.path))}</strong>
              <span class="muted gd-path-text" title="${escapeHtml(repo.path)}">${escapeHtml(repo.path)}</span>
              ${repo.matchedRemoteUrl ? `<span class="muted" style="font-size:11px">${escapeHtml(repo.matchedRemoteUrl)}</span>` : ""}
            </div>
            <div class="row" style="gap:5px;flex-shrink:0">
              <button class="use-scanned-repo-btn" data-repo-path="${escapeHtml(repo.path)}" data-repo-name="${escapeHtml(repo.name || repoNameFromPath(repo.path))}" type="button">Open</button>
              <button class="view-scanned-repo-btn" data-repo-path="${escapeHtml(repo.path)}" data-repo-name="${escapeHtml(repo.name || repoNameFromPath(repo.path))}" type="button">Viewer</button>
            </div>
          </div>`).join("")}
      </div>
    `).join("")}
  </div>`;
}

function breadcrumbTemplate(repoName, path = "") {
  const parts = path.split("/").filter(Boolean);
  const crumbs = [`<button class="viewer-crumb-btn" data-viewer-path="" type="button">${escapeHtml(repoName)}</button>`];
  parts.forEach((part, index) => {
    const crumbPath = parts.slice(0, index + 1).join("/");
    crumbs.push(`<span class="viewer-crumb-sep">/</span><button class="viewer-crumb-btn" data-viewer-path="${escapeHtml(crumbPath)}" type="button">${escapeHtml(part)}</button>`);
  });
  return crumbs.join("");
}

function filePreviewTemplate(file) {
  const language = languageForPath(file.path);
  const meta = [language, formatBytes(file.size)].filter(Boolean).join(" · ");

  const header = `
    <div class="viewer-file-panel-header">
      <span class="viewer-file-name">${FILE_ICON} ${escapeHtml(file.path.split("/").pop())}</span>
      <span class="muted">${escapeHtml(meta)}</span>
    </div>`;

  if (file.previewBase64 && file.previewMime) {
    const mime = escapeHtml(file.previewMime);
    const b64 = file.previewBase64;
    return `${header}<div class="viewer-image-preview-wrap"><img class="viewer-image-preview" alt="" decoding="async" src="data:${mime};base64,${b64}" /></div>`;
  }
  if (file.tooLarge) {
    return header + `<div class="empty-state viewer-empty"><div>File too large to preview</div><div class="muted">Preview is limited to ${formatBytes(maxPreviewBytes)}.</div></div>`;
  }
  if (file.isBinary) {
    return header + `<div class="empty-state viewer-empty"><div>Binary file</div><div class="muted">Binary content cannot be previewed.</div></div>`;
  }
  if (isMarkdownPath(file.path)) {
    return header + `<div class="markdown-body">${renderMarkdown(file.content || "")}</div>`;
  }
  return header + `<pre class="code-preview" data-language="${escapeHtml(language)}"><code>${highlightCode(file.content || "", language)}</code></pre>`;
}

// ── Modal content functions ───────────────────────────────────────────────────

function reposModalContent(state) {
  const visibleRepos = filteredRepositories();
  const activeServer = getActiveServer();
  return `
    <div class="gd-modal-tabs" role="tablist" aria-label="Repository sources">
      <button class="gd-modal-tab ${activeReposTab === "local" ? "active" : ""}" data-repos-tab="local" type="button" role="tab" aria-selected="${activeReposTab === "local"}">Local Repositories</button>
      <button class="gd-modal-tab ${activeReposTab === "server" ? "active" : ""}" data-repos-tab="server" type="button" role="tab" aria-selected="${activeReposTab === "server"}">Server Repositories</button>
    </div>
    <div class="gd-modal-tab-panel">
      ${activeReposTab === "local" ? `
      <div class="gd-modal-section">
        <h4 class="gd-modal-section-title">Recent Local Repositories</h4>
        ${recentLocalReposTemplate(state.settings.recentRepositories)}

        <div class="gd-modal-divider"></div>

        <h4 class="gd-modal-section-title">Open from Path</h4>
        <div class="row" style="gap:6px">
          <input id="local-repo-path-input" placeholder="Paste a repository path…" value="${escapeHtml(state.localRepoPathInput)}" />
          <button id="open-local-repo-btn" type="button" style="white-space:nowrap">Open</button>
        </div>

        <div class="gd-modal-divider"></div>

        <h4 class="gd-modal-section-title">Find Local Repositories</h4>
        <div class="row" style="gap:6px;margin-bottom:6px">
          <input id="local-repo-scan-root-input" placeholder="Folder to scan, e.g. F:\\Repos" value="${escapeHtml(state.localRepoScanRootInput || state.settings.defaultCloneDirectory)}" />
          <button id="scan-local-repos-btn" type="button" ${state.localRepoScanLoading ? "disabled" : ""} style="white-space:nowrap">${state.localRepoScanLoading ? "Scanning…" : "Scan"}</button>
        </div>
        ${localRepoScanTemplate()}
      </div>
      ` : `
      <div class="gd-modal-section gd-modal-section-alt">
        <div class="gd-modal-section-header">
          <h4 class="gd-modal-section-title" style="margin:0">Server Repositories</h4>
          <div class="row" style="gap:8px;align-items:center">
            <span class="muted" style="font-size:12px">${escapeHtml(activeServer?.displayName || "No server")}</span>
            <button id="refresh-repos-btn" type="button" style="font-size:12px;padding:3px 8px">Refresh</button>
          </div>
        </div>
        <input id="repo-search-input" placeholder="Search repositories…" value="${escapeHtml(state.repoSearch)}" style="margin-bottom:8px" />
        <div class="repo-filter-pills" style="margin-bottom:10px">
          <button class="pill-btn ${repoOwnerFilter === "all" ? "active" : ""}" data-owner-filter="all">All</button>
          <button class="pill-btn ${repoOwnerFilter === "personal" ? "active" : ""}" data-owner-filter="personal">Personal</button>
          <button class="pill-btn ${repoOwnerFilter === "orgs" ? "active" : ""}" data-owner-filter="orgs">Organizations</button>
        </div>
        <div class="gd-modal-scroll-list">
          ${serverRepoListTemplate(visibleRepos)}
        </div>
      </div>
      `}
    </div>
  `;
}

function cloneModalContent(state) {
  const visibleRepos = filteredRepositories();
  return `
    <div class="gd-modal-narrow stack">
      <div>
        <div class="label">Server</div>
        <select id="clone-server-select">
          ${state.settings.servers.map((server) => `<option value="${server.id}" ${server.id === state.settings.activeServerId ? "selected" : ""}>${escapeHtml(server.displayName)}</option>`).join("")}
        </select>
      </div>
      <div>
        <div class="label">Remote repository</div>
        <select id="clone-repo-select">
          <option value="">Paste URL manually below</option>
          ${visibleRepos.slice(0, 100).map((repo) => `<option value="${escapeHtml(repo.clone_url || "")}" ${repo.clone_url === state.cloneUrlInput ? "selected" : ""}>${escapeHtml(repo.full_name)}</option>`).join("")}
        </select>
      </div>
      <div>
        <div class="label">Repository URL</div>
        <input id="clone-url-input" placeholder="https://git.example.com/org/repo.git" value="${escapeHtml(state.cloneUrlInput)}" />
      </div>
      <div>
        <div class="label">Destination path</div>
        <div class="gd-path-picker">
          <input id="clone-destination-input" placeholder="${escapeHtml(state.settings.defaultCloneDirectory || "/Users/me/code/repo")}" value="${escapeHtml(state.cloneDestinationInput || state.settings.defaultCloneDirectory)}" />
          <button id="clone-destination-browse-btn" type="button">Browse</button>
        </div>
        <span class="muted" style="font-size:12px">Choose a parent folder and Gitpub will create the repository folder inside it.</span>
      </div>
      <button id="clone-btn" class="primary" type="button">Clone Repository</button>
      ${gitOutput
        ? `<pre class="git-output" style="max-height:180px;overflow:auto;font-size:11px">${escapeHtml(gitOutput)}</pre>`
        : `<span class="muted" style="font-size:12px">Clone output will appear here.</span>`}
    </div>
  `;
}

function serversModalContent(state) {
  const servers = state.settings.servers;
  return `
    <div class="gd-modal-two-col">
      <div class="gd-modal-section">
        <div class="gd-modal-section-header">
          <h4 class="gd-modal-section-title" style="margin:0">Connected Servers</h4>
          <button id="add-server-btn" type="button" class="primary">${PLUS_ICON} Add Server</button>
        </div>
        ${settingsNotice ? `<div class="muted" style="font-size:12px;margin-bottom:8px">${escapeHtml(settingsNotice)}</div>` : ""}
        ${servers.length
          ? servers.map((server) => `
              <div class="gd-server-item ${server.id === state.settings.activeServerId ? "gd-server-active" : ""}">
                <div class="gd-server-name">${escapeHtml(server.displayName)}</div>
                <div class="gd-server-url muted">${escapeHtml(server.serverUrl)}</div>
                <div class="row" style="gap:4px;margin-top:6px">
                  <button class="set-default-server-btn" data-id="${server.id}" type="button" style="font-size:12px;padding:2px 8px">${server.id === state.settings.activeServerId ? `${CHECK_ICON} Active` : "Set active"}</button>
                  <button class="edit-server-btn" data-id="${server.id}" type="button" style="font-size:12px;padding:2px 8px">Edit</button>
                  <button class="delete-server-btn danger" data-id="${server.id}" type="button" style="font-size:12px;padding:2px 8px">Remove</button>
                </div>
              </div>`).join("")
          : `<div class="gd-modal-empty">No servers configured</div>`}
      </div>
      <div class="gd-modal-section gd-modal-section-alt" id="server-form-slot">
        <div class="gd-modal-empty">Select a server to edit, or add a new one.</div>
      </div>
    </div>
  `;
}

function settingsModalContent(state) {
  const editorDropdownValue = selectedEditorDropdownValue(state);
  const customEditorPath = editorDropdownValue === CUSTOM_EDITOR_VALUE ? state.settings.externalEditorPath?.trim() || "" : "";
  return `
    <div class="gd-modal-two-col">
      <div class="gd-modal-section">
        <h4 class="gd-modal-section-title">Appearance</h4>
        <div class="label">Theme</div>
        <select id="theme-select">
          <option value="dark" ${state.settings.theme === "dark" ? "selected" : ""}>Dark</option>
          <option value="light" ${state.settings.theme === "light" ? "selected" : ""}>Light</option>
          <option value="system" ${state.settings.theme === "system" ? "selected" : ""}>System</option>
        </select>

        <div class="gd-modal-divider"></div>

        <h4 class="gd-modal-section-title">Git</h4>
        <div class="label">Git executable path</div>
        <input id="git-path-input" value="${escapeHtml(state.settings.gitExecutablePath)}" placeholder="git or /usr/bin/git" />
        <div class="label">Default clone directory</div>
        <input id="default-clone-dir-input" value="${escapeHtml(state.settings.defaultCloneDirectory)}" placeholder="/Users/me/code" />
        <div class="label">Code editor</div>
        <div class="gd-editor-picker">
          <select id="external-editor-select">
            ${externalEditorOptionsTemplate(state)}
          </select>
          <button id="rescan-editors-btn" type="button" ${state.installedIdeScanLoading ? "disabled" : ""}>${state.installedIdeScanLoading ? "Scanning..." : "Rescan"}</button>
        </div>
        ${state.installedIdeScanError ? `<div class="viewer-error" style="margin-top:6px">${escapeHtml(state.installedIdeScanError)}</div>` : ""}
        ${!state.installedIdeScanLoading && !state.installedIdes.length ? `<div class="muted" style="font-size:12px;margin-top:4px">No installed IDEs detected yet. You can use the default <code>code</code> command or choose a custom app.</div>` : ""}
        <div id="custom-editor-row" class="gd-path-picker ${editorDropdownValue === CUSTOM_EDITOR_VALUE ? "" : "hidden"}" style="margin-top:8px">
          <input id="custom-editor-input" value="${escapeHtml(customEditorPath)}" placeholder="Choose an application or executable" />
          <button id="custom-editor-browse-btn" type="button">Browse</button>
        </div>

        <div class="gd-modal-divider"></div>

        <div class="label">Default server</div>
        <select id="default-server-select">
          <option value="">None</option>
          ${state.settings.servers.map((server) => `<option value="${server.id}" ${server.id === state.settings.activeServerId ? "selected" : ""}>${escapeHtml(server.displayName)}</option>`).join("")}
        </select>
        <label class="gd-check-all" style="margin-top:6px">
          <input id="auto-fetch-checkbox" type="checkbox" ${state.settings.autoFetchOnRepoOpen ? "checked" : ""} />
          <span>Auto-fetch when opening a repository</span>
        </label>
        <button id="save-basic-settings-btn" class="primary" type="button" style="margin-top:4px">Save Settings</button>
        ${settingsNotice ? `<div class="muted" style="font-size:12px;margin-top:4px">${escapeHtml(settingsNotice)}</div>` : ""}
      </div>
      <div class="gd-modal-section gd-modal-section-alt">
        <h4 class="gd-modal-section-title">About</h4>
        <div class="muted" style="font-size:13px;line-height:1.75">
          <p style="margin:0 0 8px"><strong style="color:var(--text-main)">Gitpub Desktop</strong></p>
          <p style="margin:0 0 8px">A focused desktop client for Gitea-compatible Git servers.</p>
          <p style="margin:0">Built with Tauri 2 and vanilla JavaScript.</p>
        </div>
      </div>
    </div>
  `;
}

function viewerModalContent(state) {
  const viewer = state.viewer;
  if (!viewer.source) {
    return `<div class="gd-modal-empty" style="padding:48px;text-align:center">
      <p style="font-size:15px;margin:0 0 8px">No repository open in viewer</p>
      <p class="muted" style="font-size:13px;margin:0">Open a repository from the Repositories panel first.</p>
    </div>`;
  }

  const rows = [];
  if (viewer.path) {
    rows.push(`<button class="viewer-row" data-entry-type="dir" data-entry-path="${escapeHtml(parentPath(viewer.path))}" type="button">
      <span class="viewer-row-icon">${FOLDER_ICON}</span>
      <span class="viewer-row-name">..</span>
      <span class="viewer-row-size"></span>
    </button>`);
  }
  viewer.entries.forEach((entry) => {
    const active = viewer.selectedFile?.path === entry.path ? " active" : "";
    rows.push(`<button class="viewer-row${active}" data-entry-type="${entry.type}" data-entry-path="${escapeHtml(entry.path)}" type="button">
      <span class="viewer-row-icon">${entry.type === "dir" ? FOLDER_ICON : FILE_ICON}</span>
      <span class="viewer-row-name ${entry.type === "dir" ? "viewer-dir-name" : ""}">${escapeHtml(entry.name)}</span>
      <span class="viewer-row-size muted">${entry.type === "file" ? formatBytes(entry.size) : ""}</span>
    </button>`);
  });

  const showReadme = viewer.readmeFile && !viewer.selectedFile;

  return `
    <div class="gd-modal-viewer">
      <div class="gd-modal-viewer-sidebar">
        <div class="gd-modal-viewer-controls">
          <div class="viewer-crumb-row">
            ${breadcrumbTemplate(viewer.repoName || "repo", viewer.path)}
          </div>
          <div class="row" style="gap:6px;align-items:center;flex-shrink:0">
            <select id="viewer-branch-select" style="width:auto;font-size:12px;padding:3px 8px" ${viewer.loading ? "disabled" : ""}>
              ${viewer.branches.map((b) => `<option value="${escapeHtml(b.name)}" ${b.name === viewer.branch ? "selected" : ""}>${escapeHtml(b.name)}${b.current ? " ✓" : ""}</option>`).join("")}
            </select>
            <button id="viewer-refresh-btn" type="button" style="padding:3px 8px;font-size:12px" aria-label="Refresh viewer" title="Refresh viewer">${REFRESH_ICON}</button>
          </div>
        </div>
        <div class="gd-viewer-tree">
          ${rows.join("")}
          ${!rows.length && !viewer.loading ? `<div class="gd-left-empty"><span class="muted">Empty folder</span></div>` : ""}
        </div>
      </div>
      <div class="gd-modal-viewer-main">
        ${viewer.error ? `<div class="viewer-error" style="margin:12px">${escapeHtml(viewer.error)}</div>` : ""}
        ${viewer.loading ? `<div class="viewer-loading" style="margin:12px">Loading…</div>` : ""}
        ${viewer.selectedFile ? `<div style="height:100%;display:flex;flex-direction:column">${filePreviewTemplate(viewer.selectedFile)}</div>` : ""}
        ${showReadme ? `
          <div class="viewer-readme-panel" style="border:0;border-radius:0;margin:0">
            <div class="viewer-readme-header">${FILE_ICON}<span>${escapeHtml(viewer.readmeFile.path.split("/").pop())}</span></div>
            <div class="viewer-readme-body markdown-body">${renderMarkdown(viewer.readmeFile.content || "")}</div>
          </div>` : ""}
        ${!viewer.selectedFile && !viewer.readmeFile && !viewer.loading
          ? `<div class="gd-modal-empty" style="padding:48px;text-align:center">Select a file from the tree to preview</div>`
          : ""}
      </div>
    </div>
  `;
}

function modalTemplate(state) {
  if (!activeModal) return "";
  const viewer = state.viewer;
  const configs = {
    repos: { title: "Repositories", content: reposModalContent(state), wide: true },
    clone: { title: "Clone Repository", content: cloneModalContent(state), wide: false },
    servers: { title: "Gitea Servers", content: serversModalContent(state), wide: true },
    settings: { title: "Settings", content: settingsModalContent(state), wide: true },
    viewer: { title: viewer.repoName ? `File Viewer — ${escapeHtml(viewer.repoName)}` : "File Viewer", content: viewerModalContent(state), wide: true, fullHeight: true },
  };
  const cfg = configs[activeModal];
  if (!cfg) return "";

  return `<div class="gd-modal-backdrop" id="modal-backdrop">
    <div class="gd-modal ${cfg.wide ? "gd-modal-wide" : "gd-modal-narrow-dialog"} ${cfg.fullHeight ? "gd-modal-full-height" : ""}" role="dialog" aria-modal="true" aria-label="${cfg.title}">
      <div class="gd-modal-header">
        <h3 class="gd-modal-title">${cfg.title}</h3>
        <button id="modal-close-btn" type="button" class="gd-modal-close" aria-label="Close">
          ${WINDOW_CLOSE_ICON}
        </button>
      </div>
      <div class="gd-modal-body">
        ${cfg.content}
      </div>
    </div>
  </div>`;
}

// ── Main dashboard view ───────────────────────────────────────────────────────

function dashboardView() {
  const state = getState();
  const displayRepoName = currentRepositoryName();
  const displayBranchName = currentBranchName();
  const hasLocalRepo = Boolean(state.selectedRepoPath);
  const isMacos = currentPlatform() === "macos";
  const toolbarPlatformClass = isMacos ? "gd-toolbar-macos" : "gd-toolbar-standard";
  const windowControlsHTML = windowControlsTemplate(isMacos);

  // ── Repository identity ─────────────────────────────────────────────────
  const repoIdentity = (() => {
    if (!state.selectedRepoPath && !state.selectedRepoName) {
      return { primary: "No repository", secondary: "Select or clone a repository" };
    }
    const name = state.selectedRepoName || repoNameFromPath(state.selectedRepoPath);
    const serverRepo = repositories.find((r) => {
      const rName = r.full_name.split("/")[1] || "";
      return rName === name || r.full_name === name;
    });
    if (serverRepo) {
      return { primary: serverRepo.full_name, secondary: state.selectedRepoPath || "" };
    }
    const pathParts = (state.selectedRepoPath || "").split(/[/\\]/).filter(Boolean);
    const primary = pathParts.length >= 2 ? pathParts.slice(-2).join("/") : name;
    return { primary, secondary: state.selectedRepoPath || "" };
  })();

  // ── Contextual sync action ──────────────────────────────────────────────
  const syncCfg = syncButtonConfig(state);

  // ── Toolbar ─────────────────────────────────────────────────────────────
  const toolbarHTML = `
    ${isMacos ? windowControlsHTML : ""}
    <div class="gd-toolbar-left">
      <button class="gd-toolbar-cell gd-repo-cell" type="button" data-open-modal="repos" title="${escapeHtml(state.selectedRepoPath || repoIdentity.primary)}">
        <span class="gd-cell-icon">${LOCAL_REPO_ICON}</span>
        <span class="gd-cell-copy">
          <span class="gd-cell-label">Current repository</span>
          <span class="gd-cell-value">${escapeHtml(repoIdentity.primary)}</span>
        </span>
      </button>
      <div class="gd-branch-wrap">
        <button id="branch-menu-btn" class="gd-toolbar-cell gd-branch-toolbar" type="button" ${!hasLocalRepo || state.branches.loading || state.branches.switchingTo ? "disabled" : ""} title="${escapeHtml(displayBranchName)}">
          <span class="gd-cell-icon">${BRANCH_ICON}</span>
          <span class="gd-cell-copy">
            <span class="gd-cell-label">Current branch</span>
            <span class="gd-cell-value">${escapeHtml(displayBranchName)}</span>
          </span>
          ${CHEVRON_DOWN_ICON}
        </button>
        ${state.branches.menuOpen ? `<div class="branch-menu">
          <div class="branch-menu-section">
            <div class="branch-menu-label">Switch Branch</div>
            ${state.branches.error ? `<div class="branch-menu-message danger-subtle">${escapeHtml(state.branches.error)}</div>` : ""}
            ${state.branches.switchingTo ? `<div class="branch-menu-message">Switching to ${escapeHtml(state.branches.switchingTo)}...</div>` : ""}
            ${(state.branches.items.length ? state.branches.items : [{ name: displayBranchName, localName: displayBranchName, current: true }]).map((branch) => branchOptionTemplate(branch, state.branches.switchingTo)).join("")}
          </div>
          <div class="branch-menu-section">
            <button class="branch-menu-action" data-branch-action="create" type="button">Create new branch…</button>
            <button class="branch-menu-action" data-branch-action="rename" data-branch-name="${escapeHtml(displayBranchName)}" type="button" ${!displayBranchName || state.sync.isDetached ? "disabled" : ""}>Rename current branch…</button>
            <button class="branch-menu-action danger-subtle" data-branch-action="delete" type="button">Delete branch…</button>
          </div>
        </div>` : ""}
      </div>
    </div>

    <div class="gd-toolbar-center">
      <button class="gd-sync-btn ${syncCfg.loading ? "is-loading" : ""}" id="git-sync-action-btn" type="button" ${syncCfg.disabled ? "disabled" : ""} data-sync-action="${escapeHtml(syncCfg.action || "")}" title="${escapeHtml(syncCfg.tooltip)}" aria-label="${escapeHtml(syncCfg.tooltip)}">
        <span class="gd-sync-icon">${syncCfg.icon}</span>
        <span class="gd-cell-copy">
          <span class="gd-cell-value gd-sync-label">${escapeHtml(syncCfg.label)}</span>
          <span class="gd-cell-label">${escapeHtml(syncCfg.subLabel)}</span>
        </span>
      </button>
      <div class="gd-toolbar-drag-space" data-tauri-drag-region aria-hidden="true"></div>
    </div>

    <div class="gd-toolbar-right">
      <div class="gd-external-actions" role="group" aria-label="Repository actions">
        <button id="open-file-explorer-btn" class="gd-icon-action" type="button" ${!hasLocalRepo ? "disabled" : ""} title="Open in File Explorer" aria-label="Open in File Explorer">
          ${EXPLORER_ICON}
        </button>
        <button id="open-code-editor-btn" class="gd-icon-action" type="button" ${!hasLocalRepo ? "disabled" : ""} title="Open in Code Editor" aria-label="Open in Code Editor">
          ${EDITOR_ICON}
        </button>
      </div>
      <div class="gd-view-toggle" role="group" aria-label="View mode">
        <button class="${activeView === "changes" ? "active" : ""}" data-view="changes" type="button">Changes</button>
        <button class="${activeView === "history" ? "active" : ""}" data-view="history" type="button">History</button>
      </div>
      <div class="gd-utility-wrap">
        <button id="utility-menu-btn" class="gd-utility-btn" type="button" title="More options" aria-label="More options">
          ${MORE_ICON}
        </button>
        ${utilityMenuOpen ? `<div class="gd-utility-menu" role="menu">
          <button class="gd-utility-menu-item" data-open-modal="repos" type="button" role="menuitem">
            ${REPOSITORY_ICON}
            Open Repository
          </button>
          <button class="gd-utility-menu-item" data-open-modal="clone" type="button" role="menuitem">
            ${CLONE_ICON}
            Clone Repository
          </button>
          <div class="gd-utility-separator"></div>
          <button class="gd-utility-menu-item" data-open-modal="viewer" type="button" role="menuitem">
            ${FILE_ICON}
            File Viewer
          </button>
          <button class="gd-utility-menu-item" id="open-file-explorer-menu-btn" type="button" role="menuitem" ${!hasLocalRepo ? "disabled" : ""}>
            ${EXPLORER_ICON}
            Open in File Explorer
          </button>
          <button class="gd-utility-menu-item" id="open-code-editor-menu-btn" type="button" role="menuitem" ${!hasLocalRepo ? "disabled" : ""}>
            ${EDITOR_ICON}
            Open in Code Editor
          </button>
          <div class="gd-utility-separator"></div>
          <button class="gd-utility-menu-item" data-open-modal="servers" type="button" role="menuitem">
            ${SERVER_ICON}
            Servers
          </button>
          <button class="gd-utility-menu-item" data-open-modal="settings" type="button" role="menuitem">
            ${SETTINGS_ICON}
            Settings
          </button>
        </div>` : ""}
      </div>
    </div>
    ${isMacos ? "" : windowControlsHTML}
  `;

  // ── Sidebar content ─────────────────────────────────────────────────────
  const filteredFiles = state.workingTree.files.filter((file) =>
    file.path.toLowerCase().includes(state.changesFilter.trim().toLowerCase())
  );
  const selectedCount = filteredFiles.filter((file) => state.workingTree.selectedPaths.has(file.path)).length;
  const allSelected = filteredFiles.length > 0 && selectedCount === filteredFiles.length;

  let leftContentHTML = "";

  if (activeView === "history") {
    const historyFilter = state.historyFilter.trim().toLowerCase();
    const commits = state.history.commits.filter((commit) =>
      [commit.title, commit.author, commit.shortHash].some((v) => (v || "").toLowerCase().includes(historyFilter))
    );
    leftContentHTML = `
      <div class="gd-changes-search-row gd-history-search-row">
        <input id="history-filter-input" class="gd-filter-input" placeholder="Filter history…" value="${escapeHtml(state.historyFilter)}" style="grid-column:1/-1" />
      </div>
      <div class="gd-history-list">
        ${state.history.loading ? `<div class="viewer-loading" style="padding:10px 12px">Loading…</div>` : ""}
        ${state.history.error ? `<div class="viewer-error" style="margin:8px">${escapeHtml(state.history.error)}</div>` : ""}
        ${!hasLocalRepo ? `<div class="gd-left-empty-minimal"><span class="muted">No repository selected</span></div>` : ""}
        ${hasLocalRepo && !state.history.loading && commits.length
          ? commits.map((commit) => `
              <button class="gd-history-item commit-item ${state.history.selectedHash === commit.hash ? "active" : ""}" data-commit-hash="${escapeHtml(commit.hash)}" type="button">
                <span class="gd-history-info">
                  <span class="gd-history-name">${escapeHtml(commit.title)}</span>
                  <span class="gd-history-path muted">${escapeHtml(commit.author)} · ${escapeHtml(commit.shortHash)}</span>
                </span>
              </button>`).join("")
          : ""}
        ${hasLocalRepo && !state.history.loading && !commits.length
          ? `<div class="gd-left-empty-minimal"><span class="muted">No commits found</span></div>`
          : ""}
      </div>
    `;
  } else {
    leftContentHTML = `
      <div class="gd-changes-search-row">
        <button class="gd-filter-menu-btn" id="refresh-changes-btn" type="button" aria-label="Refresh" title="Refresh changes">
          ${REFRESH_ICON}
        </button>
        <input id="changes-filter-input" class="gd-filter-input" placeholder="Filter changed files" value="${escapeHtml(state.changesFilter)}" />
      </div>
      <div class="gd-changes-filter-row">
        <label class="gd-check-all">
          <input id="select-all-changes" type="checkbox" ${allSelected ? "checked" : ""} ${!filteredFiles.length ? "disabled" : ""} />
          <span>${filteredFiles.length} changed file${filteredFiles.length === 1 ? "" : "s"}</span>
        </label>
        <span class="muted" style="font-size:11px">${selectedCount > 0 ? `${selectedCount} staged` : ""}</span>
      </div>
      <div class="gd-changes-list">
        ${state.workingTree.loading ? `<div class="viewer-loading" style="padding:10px 12px">Loading…</div>` : ""}
        ${state.workingTree.error ? `<div class="viewer-error" style="margin:8px">${escapeHtml(state.workingTree.error)}</div>` : ""}
        ${!hasLocalRepo ? `<div class="gd-left-empty-minimal"><span class="muted">No repository open</span></div>` : ""}
        ${hasLocalRepo && !state.workingTree.loading && !filteredFiles.length && !state.workingTree.error
          ? `<div class="gd-left-empty-minimal"><span class="muted">Working tree is clean</span></div>`
          : ""}
        ${groupedChangedFiles(filteredFiles).map((group) => `
          <div class="change-group">
            <div class="change-group-title">${statusLabel(group.status)}</div>
            ${group.files.map((file) => `
              <button class="change-file-row ${state.workingTree.selectedPath === file.path ? "active" : ""}" data-change-path="${escapeHtml(file.path)}" type="button">
                <input class="change-file-checkbox" data-change-checkbox="${escapeHtml(file.path)}" type="checkbox" ${state.workingTree.selectedPaths.has(file.path) ? "checked" : ""} />
                <span class="change-status change-status-${escapeHtml(file.status)}">${escapeHtml(file.status[0]?.toUpperCase() || "M")}</span>
                <span class="change-file-name" title="${escapeHtml(file.originalPath ? `${file.originalPath} → ${file.path}` : file.path)}">${escapeHtml(file.path)}</span>
              </button>
            `).join("")}
          </div>
        `).join("")}
        ${gitOutput ? `<pre class="git-output gd-git-out">${escapeHtml(gitOutput)}</pre>` : ""}
      </div>
    `;
  }

  // ── Main area ───────────────────────────────────────────────────────────
  let mainHTML = "";

  if (activeView === "history") {
    const commit = state.history.selectedCommit;
    if (commit) {
      mainHTML = `
        <div class="gd-main-pad stack">
          <div class="section-header">
            <div>
              <h3 class="title">${escapeHtml(commit.title)}</h3>
              <p class="subtitle">${escapeHtml(commit.author)} · ${escapeHtml(commit.date)} · ${escapeHtml(commit.shortHash)}</p>
            </div>
            <button id="refresh-history-btn" type="button">Refresh</button>
          </div>
          <pre class="commit-message-view">${escapeHtml(commit.message)}</pre>
          <div class="changed-files-chip-row">
            ${(commit.files || []).map((file) => `<span class="changed-file-chip">${escapeHtml(file.status)} ${escapeHtml(file.path)}</span>`).join("")}
          </div>
          <div class="diff-preview diff-preview-inline">${renderDiff(commit.diff || "No diff available.")}</div>
        </div>`;
    } else {
      mainHTML = workflowEmptyStateTemplate({
        title: hasLocalRepo ? "No commit selected" : "Open a repository to view history",
        message: hasLocalRepo ? "Select a commit from the list to review its files and diff." : "History appears after you open or clone a local repository.",
        icon: "diff",
        actions: !hasLocalRepo,
      });
    }
  } else {
    const selectedChange = state.workingTree.files.find((file) => file.path === state.workingTree.selectedPath);
    mainHTML = `
      <div class="gd-main-pad stack">
        <div class="section-header">
          <div>
            <h3 class="title">${selectedChange ? escapeHtml(selectedChange.path) : hasLocalRepo ? "No local changes" : "No repository selected"}</h3>
            <p class="subtitle">${selectedChange ? `${statusLabel(selectedChange.status)} file` : hasLocalRepo ? "The working tree is clean." : "Open or clone a repository to begin."}</p>
          </div>
          <button id="refresh-changes-main-btn" type="button" ${!hasLocalRepo ? "disabled" : ""}>Refresh</button>
        </div>
        ${state.workingTree.diffLoading ? `<div class="viewer-loading">Loading diff…</div>` : ""}
        ${state.workingTree.diffError ? `<div class="viewer-error">${escapeHtml(state.workingTree.diffError)}</div>` : ""}
        ${selectedChange
          ? diffTemplate(state.workingTree.selectedDiff)
          : workflowEmptyStateTemplate({
              title: hasLocalRepo ? "You're all caught up" : "Start with a repository",
              message: hasLocalRepo
                ? "There are no uncommitted changes in this repository."
                : "Open a local repository or clone one from a Gitea-compatible server.",
              icon: hasLocalRepo ? "diff" : "repo",
              actions: !hasLocalRepo,
            })}
      </div>`;
  }

  // ── Commit panel ────────────────────────────────────────────────────────
  const hasCommitSummary = Boolean((state.commitSummary || state.commitMessage || "").trim());
  const hasSelectedFiles = state.workingTree.selectedPaths.size > 0;
  const canCommit = hasLocalRepo && hasSelectedFiles && hasCommitSummary;

  const commitAreaHTML = `
    <div class="gd-commit-area">
      <input id="commit-summary-input" class="gd-commit-summary" placeholder="Summary (required)" value="${escapeHtml(state.commitSummary || state.commitMessage)}" />
      <textarea id="commit-description-input" class="commit-desc-input" placeholder="Description (optional)">${escapeHtml(state.commitDescription)}</textarea>
      <button id="commit-btn" class="primary gd-commit-btn" type="button" ${!canCommit ? "disabled" : ""}>
        Commit to ${escapeHtml(displayBranchName)}
      </button>
    </div>`;

  // ── Assemble ─────────────────────────────────────────────────────────────
  appRoot.innerHTML = `
    <div class="layout">
      <header class="gd-toolbar ${toolbarPlatformClass}">
        ${toolbarHTML}
      </header>
      <aside class="gd-left">
        <div class="gd-left-content">
          ${leftContentHTML}
        </div>
        ${activeView === "changes" ? commitAreaHTML : ""}
      </aside>
      <main class="gd-main">
        ${mainHTML}
      </main>
    </div>
    ${modalTemplate(state)}
    ${branchDialogTemplate(state, displayBranchName)}
  `;

  disableInputAutocomplete(appRoot);
  bindDashboardEvents();
}

// ── Server form events ────────────────────────────────────────────────────────

function bindServerFormEvents(existingServer = null) {
  const testButton = document.getElementById("test-server-btn");
  const saveButton = document.getElementById("save-server-btn");
  const formCard = document.getElementById("server-form-card");
  if (!testButton || !saveButton || !formCard) return;

  const collectPayload = () => {
    const get = (name) => formCard.querySelector(`[name="${name}"]`)?.value?.trim() || "";
    return {
      id: get("id"),
      displayName: get("displayName"),
      serverUrl: get("serverUrl"),
      authMethod: get("authMethod") || "token",
      token: get("token"),
      username: get("username"),
      password: get("password"),
    };
  };

  testButton.addEventListener("click", async () => {
    const payload = collectPayload();
    try {
      const result = await testGiteaConnection(payload);
      serverTestResult = `${result.ok ? "✓ Connected" : "✗ Failed"}: ${result.message} (${result.apiBaseUrl})`;
      render();
      openServerForm(existingServer?.id || null);
    } catch (error) {
      serverTestResult = `Connection test failed: ${errorMessage(error)}`;
      render();
      openServerForm(existingServer?.id || null);
    }
  });

  saveButton.addEventListener("click", () => {
    const payload = collectPayload();
    if (!payload.displayName || !payload.serverUrl) {
      settingsNotice = "Display name and server URL are required.";
      render();
      if (existingServer) openServerForm(existingServer.id);
      return;
    }

    const state = getState();
    let nextServers = [...state.settings.servers];
    const nextId = payload.id || uid();
    const serverData = { ...payload, id: nextId };
    const existingIndex = nextServers.findIndex((item) => item.id === nextId);

    if (existingIndex >= 0) {
      nextServers[existingIndex] = serverData;
      settingsNotice = `Updated server ${payload.displayName}.`;
    } else {
      nextServers.push(serverData);
      settingsNotice = `Added server ${payload.displayName}.`;
    }

    const activeServerId = state.settings.activeServerId || nextId;
    setSettings({ ...state.settings, servers: nextServers, activeServerId });
    serverTestResult = "";
    render();
  });
}

function openServerForm(serverId = null) {
  const slot = document.getElementById("server-form-slot");
  if (!slot) return;

  const server = getState().settings.servers.find((item) => item.id === serverId) || null;
  slot.innerHTML = serverFormTemplate(server);
  disableInputAutocomplete(slot);
  bindServerFormEvents(server);
}

// ── Data loading ──────────────────────────────────────────────────────────────

async function loadRepositories() {
  const activeServer = getActiveServer();
  if (!activeServer) {
    repositories = [...mockRepos];
    currentUserLogin = "alice";
    return;
  }

  try {
    const [user, repos] = await Promise.all([
      fetchCurrentUser(activeServer),
      fetchRepositories(activeServer),
    ]);
    currentUserLogin = user.login || "";
    repositories = repos;
  } catch (error) {
    settingsNotice = `Using mock repositories. API fetch failed: ${errorMessage(error)}`;
    currentUserLogin = "alice";
    repositories = [...mockRepos];
  }
}

async function autoLoadReadme() {
  const state = getState();
  const viewer = state.viewer;
  const entry = viewer.entries.find(
    (e) => e.type === "file" && /^readme(\.md|\.txt|\.rst)?$/i.test(e.name)
  );
  viewer.readmeFile = null;
  if (!entry) return;

  try {
    if (viewer.source === "remote") {
      const activeServer = getActiveServer();
      const file = await fetchRepoContents(activeServer, viewer.repoName, entry.path, viewer.branch);
      const decoded = file.encoding === "base64"
        ? decodeBase64Content(file.content || "")
        : { content: file.content || "", isBinary: false, tooLarge: false };
      if (!decoded.isBinary && !decoded.tooLarge) {
        viewer.readmeFile = { path: entry.path, content: decoded.content };
      }
    } else {
      const file = await readLocalRepoFile(
        viewer.repoPath, viewer.branch, entry.path, state.settings.gitExecutablePath
      );
      if (!file.isBinary && !file.tooLarge) {
        viewer.readmeFile = { path: entry.path, content: file.content || "" };
      }
    }
    render();
  } catch {
    // non-fatal
  }
}

function selectLocalRepo(path, name = "") {
  const state = getState();
  state.localRepoPathInput = path;
  state.selectedRepoPath = path;
  state.selectedRepoName = name || repoNameFromPath(path);
  state.workingTree.selectedPath = "";
  state.workingTree.selectedDiff = null;
  state.history.selectedHash = "";
  state.history.selectedCommit = null;
  Object.assign(state.sync, {
    loading: false,
    error: "",
    operation: "",
    branch: "",
    upstream: "",
    upstreamRemote: "",
    defaultRemote: "",
    ahead: 0,
    behind: 0,
    hasRemote: false,
    isDetached: false,
    isUnpublished: false,
    lastUpdated: 0,
  });
  addRecentRepo(path);
}

async function refreshBranches() {
  const state = getState();
  if (!state.selectedRepoPath) {
    state.branches.items = [];
    return;
  }
  state.branches.loading = true;
  state.branches.error = "";
  render();
  try {
    state.branches.items = await listLocalRepoBranches(state.selectedRepoPath, selectedGitPath());
  } catch (error) {
    state.branches.error = `Unable to load branches: ${errorMessage(error)}`;
  } finally {
    state.branches.loading = false;
    render();
  }
}

async function refreshWorkingTree() {
  const state = getState();
  const tree = state.workingTree;
  if (!state.selectedRepoPath) {
    Object.assign(tree, { loading: false, error: "", branch: "", upstream: "", ahead: 0, behind: 0, files: [], selectedPath: "", selectedDiff: null });
    render();
    return;
  }

  tree.loading = true;
  tree.error = "";
  render();
  try {
    const status = await getWorkingTreeStatus(state.selectedRepoPath, selectedGitPath());
    const previousSelection = new Set(tree.selectedPaths);
    tree.branch = status.branch || "";
    tree.upstream = status.upstream || "";
    tree.ahead = status.ahead || 0;
    tree.behind = status.behind || 0;
    tree.files = status.files || [];
    tree.selectedPaths = new Set(
      tree.files
        .map((file) => file.path)
        .filter((path) => previousSelection.size ? previousSelection.has(path) : true)
    );
    if (!tree.files.some((file) => file.path === tree.selectedPath)) {
      tree.selectedPath = tree.files[0]?.path || "";
      tree.selectedDiff = null;
    }
    if (tree.selectedPath) {
      await loadSelectedDiff(tree.selectedPath, false);
    }
  } catch (error) {
    tree.error = `Unable to load changes: ${errorMessage(error)}`;
  } finally {
    tree.loading = false;
    render();
  }
}

async function refreshSyncState({ silent = false } = {}) {
  const state = getState();
  const sync = state.sync;
  const repoPath = state.selectedRepoPath;
  if (!repoPath) {
    Object.assign(sync, {
      loading: false,
      error: "",
      branch: "",
      upstream: "",
      upstreamRemote: "",
      defaultRemote: "",
      ahead: 0,
      behind: 0,
      hasRemote: false,
      isDetached: false,
      isUnpublished: false,
      lastUpdated: 0,
    });
    if (!silent) render();
    return;
  }

  sync.loading = true;
  sync.error = "";
  if (!silent) render();
  try {
    const status = await getRepositorySyncStatus(repoPath, selectedGitPath());
    if (getState().selectedRepoPath !== repoPath) return;
    Object.assign(sync, {
      branch: status.branch || "",
      upstream: status.upstream || "",
      upstreamRemote: status.upstreamRemote || "",
      defaultRemote: status.defaultRemote || "",
      ahead: status.ahead || 0,
      behind: status.behind || 0,
      hasRemote: Boolean(status.hasRemote),
      isDetached: Boolean(status.isDetached),
      isUnpublished: Boolean(status.isUnpublished),
      lastUpdated: Date.now(),
    });
    state.workingTree.branch = sync.branch;
    state.workingTree.upstream = sync.upstream;
    state.workingTree.ahead = sync.ahead;
    state.workingTree.behind = sync.behind;
  } catch (error) {
    if (getState().selectedRepoPath !== repoPath) return;
    sync.error = errorMessage(error);
  } finally {
    if (getState().selectedRepoPath === repoPath) {
      sync.loading = false;
      render();
    }
  }
}

async function loadSelectedDiff(path, shouldRender = true) {
  const state = getState();
  const tree = state.workingTree;
  const file = tree.files.find((item) => item.path === path);
  if (!state.selectedRepoPath || !file) return;
  tree.selectedPath = path;
  tree.diffLoading = true;
  tree.diffError = "";
  if (shouldRender) render();
  try {
    tree.selectedDiff = await getFileDiff(state.selectedRepoPath, file.path, file.status, selectedGitPath());
  } catch (error) {
    tree.selectedDiff = null;
    tree.diffError = `Unable to load diff: ${errorMessage(error)}`;
  } finally {
    tree.diffLoading = false;
    if (shouldRender) render();
  }
}

async function refreshHistory() {
  const state = getState();
  const history = state.history;
  if (!state.selectedRepoPath) {
    Object.assign(history, { loading: false, error: "", commits: [], selectedHash: "", selectedCommit: null });
    render();
    return;
  }

  history.loading = true;
  history.error = "";
  render();
  try {
    history.commits = await getCommitHistory(state.selectedRepoPath, 100, selectedGitPath());
    if (!history.selectedHash || !history.commits.some((commit) => commit.hash === history.selectedHash)) {
      history.selectedHash = history.commits[0]?.hash || "";
      history.selectedCommit = null;
    }
    if (history.selectedHash) {
      await loadCommitDetail(history.selectedHash, false);
    }
  } catch (error) {
    history.error = `Unable to load history: ${errorMessage(error)}`;
  } finally {
    history.loading = false;
    render();
  }
}

async function loadCommitDetail(hash, shouldRender = true) {
  const state = getState();
  if (!state.selectedRepoPath || !hash) return;
  state.history.selectedHash = hash;
  state.history.error = "";
  if (shouldRender) render();
  try {
    state.history.selectedCommit = await getCommitDetail(state.selectedRepoPath, hash, selectedGitPath());
  } catch (error) {
    state.history.error = `Unable to load commit: ${errorMessage(error)}`;
  } finally {
    if (shouldRender) render();
  }
}

async function refreshRepoData() {
  await Promise.all([refreshWorkingTree(), refreshSyncState({ silent: true }), refreshBranches(), refreshHistory()]);
}

async function scanForLocalRepos() {
  const state = getState();
  const rootInput = document.getElementById("local-repo-scan-root-input")?.value?.trim() || "";
  state.localRepoScanRootInput = rootInput;

  if (!getActiveServer()) {
    state.localRepoScanResults = [];
    state.localRepoScanError = "Select a Gitea server before scanning local repositories.";
    render();
    return;
  }

  const allowedRemoteUrls = serverRepoRemoteUrls();
  if (!allowedRemoteUrls.length) {
    state.localRepoScanResults = [];
    state.localRepoScanError = "No repository clone URLs loaded for the selected server. Refresh and try again.";
    render();
    return;
  }

  state.localRepoScanLoading = true;
  state.localRepoScanError = "";
  render();

  const roots = [rootInput, state.settings.defaultCloneDirectory].filter(Boolean);
  try {
    state.localRepoScanResults = await scanLocalRepos(
      [...new Set(roots)],
      allowedRemoteUrls,
      state.settings.gitExecutablePath,
      4,
      200
    );
    if (!state.localRepoScanResults.length) {
      state.localRepoScanError = "No local repositories from the selected server were found.";
    }
  } catch (error) {
    state.localRepoScanResults = [];
    state.localRepoScanError = `Repo scan failed: ${errorMessage(error)}`;
  } finally {
    state.localRepoScanLoading = false;
    render();
  }
}

async function loadViewerPath(path = "") {
  const state = getState();
  const viewer = state.viewer;
  if (!viewer.source) return;

  viewer.path = path;
  viewer.loading = true;
  viewer.error = "";
  viewer.entries = [];
  viewer.selectedFile = null;
  viewer.readmeFile = null;
  render();

  try {
    if (viewer.source === "remote") {
      const activeServer = getActiveServer();
      if (!activeServer) throw new Error("No active server is configured.");
      const contents = await fetchRepoContents(activeServer, viewer.repoName, path, viewer.branch);
      viewer.entries = normaliseRemoteEntries(contents).filter((entry) => entry.type === "dir" || entry.path !== path);
    } else {
      const entries = await listLocalRepoTree(
        viewer.repoPath,
        viewer.branch,
        path,
        state.settings.gitExecutablePath
      );
      viewer.entries = normaliseLocalEntries(entries);
    }
    viewer.loading = false;
    render();
    await autoLoadReadme();
  } catch (error) {
    viewer.error = `Unable to load repository contents: ${errorMessage(error)}`;
    viewer.loading = false;
    render();
  }
}

async function openViewerFile(path) {
  const state = getState();
  const viewer = state.viewer;
  if (!viewer.source || !path) return;

  viewer.loading = true;
  viewer.error = "";
  viewer.selectedFile = null;
  render();

  try {
    if (viewer.source === "remote") {
      const activeServer = getActiveServer();
      if (!activeServer) throw new Error("No active server is configured.");
      const file = await fetchRepoContents(activeServer, viewer.repoName, path, viewer.branch);
      const decoded = file.encoding === "base64"
        ? decodeBase64Content(file.content || "")
        : { content: file.content || "", size: file.size || 0, isBinary: false, tooLarge: false };
      viewer.selectedFile = {
        path: file.path || path,
        size: file.size || decoded.size,
        content: decoded.content,
        isBinary: decoded.isBinary,
        tooLarge: decoded.tooLarge,
      };
    } else {
      const file = await readLocalRepoFile(
        viewer.repoPath,
        viewer.branch,
        path,
        state.settings.gitExecutablePath
      );
      viewer.selectedFile = {
        path: file.path,
        size: file.size,
        content: file.content || "",
        isBinary: file.isBinary,
        tooLarge: file.tooLarge,
        previewMime: file.previewMime,
        previewBase64: file.previewBase64,
      };
    }
  } catch (error) {
    viewer.error = `Unable to preview file: ${errorMessage(error)}`;
  } finally {
    viewer.loading = false;
    render();
  }
}

async function openRemoteViewer(repo) {
  const state = getState();
  const viewer = state.viewer;
  Object.assign(viewer, {
    source: "remote",
    repoName: repo.full_name,
    repoPath: "",
    cloneUrl: repo.clone_url || "",
    defaultBranch: repo.default_branch || "",
    branch: "",
    branches: [],
    path: "",
    entries: [],
    selectedFile: null,
    loading: true,
    error: "",
  });
  activeModal = "viewer";
  render();

  try {
    const activeServer = getActiveServer();
    if (!activeServer) throw new Error("No active server is configured.");
    const branches = await fetchRepoBranches(activeServer, repo.full_name);
    const branchNames = branches.map((branch) => branch.name).filter(Boolean);
    const selectedBranch = repo.default_branch || branchNames[0] || "";
    viewer.branches = branchNames.map((name) => ({ name, current: name === selectedBranch }));
    viewer.branch = selectedBranch;
    await loadViewerPath("");
  } catch (error) {
    viewer.loading = false;
    viewer.error = `Unable to open repository viewer: ${errorMessage(error)}`;
    render();
  }
}

async function openLocalViewer() {
  const state = getState();
  const value = state.selectedRepoPath || state.localRepoPathInput;
  if (!value) {
    gitOutput = "Select or enter a local repository path first.";
    render();
    return;
  }

  const viewer = state.viewer;
  Object.assign(viewer, {
    source: "local",
    repoName: state.selectedRepoName || value.split(/[/\\]/).filter(Boolean).pop() || value,
    repoPath: value,
    cloneUrl: "",
    defaultBranch: "",
    branch: "",
    branches: [],
    path: "",
    entries: [],
    selectedFile: null,
    loading: true,
    error: "",
  });
  activeModal = "viewer";
  render();

  try {
    const branches = await listLocalRepoBranches(value, state.settings.gitExecutablePath);
    const selectedBranch = branches.find((branch) => branch.current)?.name || branches[0]?.name || "";
    viewer.branches = branches;
    viewer.branch = selectedBranch;
    await loadViewerPath("");
  } catch (error) {
    viewer.loading = false;
    viewer.error = `Unable to open local viewer: ${errorMessage(error)}`;
    render();
  }
}

async function refreshInstalledIdes() {
  const state = getState();
  state.installedIdeScanLoading = true;
  state.installedIdeScanError = "";
  render();

  try {
    state.installedIdes = await scanInstalledIdes();
  } catch (error) {
    state.installedIdes = [];
    state.installedIdeScanError = `Unable to scan installed IDEs: ${errorMessage(error)}`;
  } finally {
    state.installedIdeScanLoading = false;
    render();
  }
}

async function openSelectedRepoInFileExplorer() {
  const state = getState();
  if (!state.selectedRepoPath) {
    gitOutput = "Select or enter a local repository path first.";
    render();
    return;
  }

  try {
    await openInFileExplorer(state.selectedRepoPath);
    gitOutput = `Opened in File Explorer:\n${state.selectedRepoPath}`;
  } catch (error) {
    gitOutput = `Open in File Explorer failed: ${errorMessage(error)}`;
  } finally {
    utilityMenuOpen = false;
    render();
  }
}

async function openSelectedRepoInCodeEditor() {
  const state = getState();
  if (!state.selectedRepoPath) {
    gitOutput = "Select or enter a local repository path first.";
    render();
    return;
  }

  const editorCommand = state.settings.externalEditorPath?.trim() || "code";
  try {
    await openInExternalEditor(state.selectedRepoPath, editorCommand);
    gitOutput = `Opened in Code Editor with "${editorCommand}":\n${state.selectedRepoPath}`;
  } catch (error) {
    gitOutput = `Open in Code Editor failed: ${errorMessage(error)}`;
  } finally {
    utilityMenuOpen = false;
    render();
  }
}

async function runRepoCommand(actionName, runner, operation = "") {
  const state = getState();
  if (!state.selectedRepoPath) {
    gitOutput = "Select or enter a local repository path first.";
    render();
    return;
  }

  state.sync.operation = operation;
  state.sync.error = "";
  render();
  try {
    const result = await runner();
    gitOutput = `${actionName}: ${result.command}\n\n${result.stdout || "(no stdout)"}\n${result.stderr ? `\n${result.stderr}` : ""}`;
  } catch (error) {
    const message = errorMessage(error);
    gitOutput = `${actionName} failed: ${message}`;
    state.sync.error = message;
  } finally {
    state.sync.operation = "";
  }
  render();
}

// ── Event bindings ────────────────────────────────────────────────────────────

function bindDashboardEvents() {
  const state = getState();

  // View toggle (Changes / History)
  document.querySelectorAll("[data-view]").forEach((btn) => {
    btn.addEventListener("click", () => {
      const view = btn.dataset.view;
      if (view === activeView) return;
      activeView = view;
      render();
      if (activeView === "changes") refreshWorkingTree();
      if (activeView === "history") refreshHistory();
    });
  });

  // Open modal buttons (data-open-modal anywhere in the page)
  document.querySelectorAll("[data-open-modal]").forEach((btn) => {
    btn.addEventListener("click", () => {
      activeModal = btn.dataset.openModal;
      utilityMenuOpen = false;
      render();
      if (activeModal === "settings" && !state.installedIdeScanLoading && !state.installedIdes.length) {
        refreshInstalledIdes();
      }
    });
  });

  // Utility menu toggle
  document.getElementById("utility-menu-btn")?.addEventListener("click", (event) => {
    event.stopPropagation();
    utilityMenuOpen = !utilityMenuOpen;
    render();
  });

  document.getElementById("open-file-explorer-btn")?.addEventListener("click", openSelectedRepoInFileExplorer);
  document.getElementById("open-file-explorer-menu-btn")?.addEventListener("click", openSelectedRepoInFileExplorer);
  document.getElementById("open-code-editor-btn")?.addEventListener("click", openSelectedRepoInCodeEditor);
  document.getElementById("open-code-editor-menu-btn")?.addEventListener("click", openSelectedRepoInCodeEditor);
  document.querySelectorAll("[data-window-action]").forEach((button) => {
    button.addEventListener("click", () => handleWindowAction(button.dataset.windowAction || ""));
  });
  document.querySelector(".gd-toolbar-drag-space")?.addEventListener("mousedown", startWindowDrag);
  document.querySelector(".gd-toolbar-drag-space")?.addEventListener("dblclick", () => handleWindowAction("maximize"));

  // Modal close
  document.getElementById("modal-close-btn")?.addEventListener("click", () => {
    activeModal = "";
    render();
  });

  document.getElementById("modal-backdrop")?.addEventListener("click", (event) => {
    if (event.target === event.currentTarget) {
      activeModal = "";
      render();
    }
  });

  // Contextual sync button
  document.getElementById("git-sync-action-btn")?.addEventListener("click", () => {
    const action = document.getElementById("git-sync-action-btn")?.dataset.syncAction || "fetch";
    if (action === "pull") {
      runRepoCommand("Pull", () => runGitPull(state.selectedRepoPath, selectedGitPath()), "pull").then(refreshRepoData);
    } else if (action === "push") {
      runRepoCommand("Push", () => runGitPush(state.selectedRepoPath, selectedGitPath()), "push").then(refreshRepoData);
    } else if (action === "publish") {
      runRepoCommand(
        "Publish",
        () => runGitPublishBranch(state.selectedRepoPath, state.sync.defaultRemote || "origin", selectedGitPath()),
        "publish"
      ).then(refreshRepoData);
    } else if (action === "sync") {
      runRepoCommand("Sync", () => runGitSync(state.selectedRepoPath, selectedGitPath()), "sync").then(refreshRepoData);
    } else {
      runRepoCommand("Fetch", () => runGitFetch(state.selectedRepoPath, selectedGitPath()), "fetch").then(refreshRepoData);
    }
  });

  // Changes filter
  document.getElementById("changes-filter-input")?.addEventListener("input", (event) => {
    state.changesFilter = event.target.value;
    render();
  });

  // History filter
  document.getElementById("history-filter-input")?.addEventListener("input", (event) => {
    state.historyFilter = event.target.value;
    render();
  });

  // Repo search (in modal)
  document.getElementById("repo-search-input")?.addEventListener("input", (event) => {
    state.repoSearch = event.target.value;
    render();
  });

  // Repository modal tabs
  document.querySelectorAll("[data-repos-tab]").forEach((btn) => {
    btn.addEventListener("click", () => {
      activeReposTab = btn.dataset.reposTab || "local";
      render();
    });
  });

  // Refresh repos (in modal)
  document.getElementById("refresh-repos-btn")?.addEventListener("click", async () => {
    await loadRepositories();
    render();
  });

  // Owner filter pills
  document.querySelectorAll("[data-owner-filter]").forEach((btn) => {
    btn.addEventListener("click", () => {
      repoOwnerFilter = btn.dataset.ownerFilter;
      render();
    });
  });

  // View repo (opens file viewer modal)
  document.querySelectorAll(".view-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      const repo = repositories.find((item) => item.full_name === button.dataset.repoName);
      if (repo) openRemoteViewer(repo);
    });
  });

  // Clone repo (prefill clone modal)
  document.querySelectorAll(".clone-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      state.cloneUrlInput = button.dataset.cloneUrl || "";
      activeModal = "clone";
      render();
    });
  });

  // Scan local repos
  document.getElementById("scan-local-repos-btn")?.addEventListener("click", () => {
    scanForLocalRepos();
  });

  // Use a scanned repo
  document.querySelectorAll(".use-scanned-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      selectLocalRepo(button.dataset.repoPath || "", button.dataset.repoName || "");
      activeModal = "";
      activeView = "changes";
      render();
      refreshRepoData();
    });
  });

  // View files of a scanned repo
  document.querySelectorAll(".view-scanned-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      selectLocalRepo(button.dataset.repoPath || "", button.dataset.repoName || "");
      openLocalViewer();
    });
  });

  // Open recent repo
  document.querySelectorAll(".open-recent-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      selectLocalRepo(button.dataset.recentRepoPath || "");
      activeModal = "";
      activeView = "changes";
      render();
      refreshRepoData();
    });
  });

  // Remove recent repo
  document.querySelectorAll(".remove-recent-repo-btn").forEach((button) => {
    button.addEventListener("click", () => {
      const path = button.dataset.recentRepoPath || "";
      updateSettings({
        recentRepositories: state.settings.recentRepositories.filter((item) => item !== path),
        lastSelectedRepoPath: state.settings.lastSelectedRepoPath === path ? "" : state.settings.lastSelectedRepoPath,
      });
      if (state.selectedRepoPath === path) {
        state.selectedRepoPath = "";
        state.selectedRepoName = "";
      }
      render();
    });
  });

  // Open local repo by path (repos modal)
  document.getElementById("open-local-repo-btn")?.addEventListener("click", () => {
    const value = document.getElementById("local-repo-path-input")?.value?.trim() || "";
    if (!value) return;
    selectLocalRepo(value);
    activeModal = "";
    activeView = "changes";
    render();
    refreshRepoData();
  });

  // Select all changes
  document.getElementById("select-all-changes")?.addEventListener("change", (event) => {
    const visiblePaths = state.workingTree.files
      .filter((file) => file.path.toLowerCase().includes(state.changesFilter.trim().toLowerCase()))
      .map((file) => file.path);
    if (event.target.checked) {
      visiblePaths.forEach((path) => state.workingTree.selectedPaths.add(path));
    } else {
      visiblePaths.forEach((path) => state.workingTree.selectedPaths.delete(path));
    }
    render();
  });

  // File checkboxes
  document.querySelectorAll(".change-file-checkbox").forEach((checkbox) => {
    checkbox.addEventListener("click", (event) => event.stopPropagation());
    checkbox.addEventListener("change", (event) => {
      const path = checkbox.dataset.changeCheckbox;
      if (event.target.checked) {
        state.workingTree.selectedPaths.add(path);
      } else {
        state.workingTree.selectedPaths.delete(path);
      }
      render();
    });
  });

  // Change file row click (load diff)
  document.querySelectorAll(".change-file-row").forEach((button) => {
    button.addEventListener("click", () => {
      loadSelectedDiff(button.dataset.changePath || "");
    });
  });

  // Commit summary input
  document.getElementById("commit-summary-input")?.addEventListener("input", (event) => {
    state.commitSummary = event.target.value;
    state.commitMessage = event.target.value;
    // Re-render to update button disabled state
    const btn = document.getElementById("commit-btn");
    if (btn) {
      const hasSelectedFiles = state.workingTree.selectedPaths.size > 0;
      const hasRepo = Boolean(state.selectedRepoPath);
      const hasSummary = Boolean(event.target.value.trim());
      btn.disabled = !(hasRepo && hasSelectedFiles && hasSummary);
    }
  });

  // Commit description input
  document.getElementById("commit-description-input")?.addEventListener("input", (event) => {
    state.commitDescription = event.target.value;
  });

  // Commit button
  document.getElementById("commit-btn")?.addEventListener("click", async () => {
    state.commitSummary = document.getElementById("commit-summary-input")?.value?.trim() || "";
    state.commitDescription = document.getElementById("commit-description-input")?.value || "";
    const selectedPaths = [...state.workingTree.selectedPaths];
    if (!state.commitSummary) {
      gitOutput = "Commit summary is required.";
      render();
      return;
    }
    if (!selectedPaths.length) {
      gitOutput = "Select at least one changed file before committing.";
      render();
      return;
    }
    try {
      const result = await commitChanges(
        state.selectedRepoPath,
        selectedPaths,
        state.commitSummary,
        state.commitDescription,
        selectedGitPath()
      );
      gitOutput = `${result.command}\n\n${result.stdout || "Commit created."}\n${result.stderr || ""}`;
      state.commitSummary = "";
      state.commitMessage = "";
      state.commitDescription = "";
      await refreshRepoData();
    } catch (error) {
      gitOutput = `Commit failed: ${errorMessage(error)}`;
      render();
    }
  });

  // Branch menu button
  document.getElementById("branch-menu-btn")?.addEventListener("click", () => {
    state.branches.menuOpen = !state.branches.menuOpen;
    render();
  });

  // Branch menu items
  document.querySelectorAll(".branch-menu-item").forEach((button) => {
    button.addEventListener("click", async () => {
      const branch = button.dataset.branchName || "";
      if (!branch || branch === state.workingTree.branch) {
        state.branches.menuOpen = false;
        render();
        return;
      }
      const selectedBranch = state.branches.items.find((item) => item.name === branch);
      const displayName = selectedBranch ? branchDisplayName(selectedBranch) : branch;
      state.branches.switchingTo = displayName;
      render();
      try {
        const result = await checkoutBranch(state.selectedRepoPath, branch, selectedGitPath());
        gitOutput = `${result.command}\n\n${result.stdout || `Switched to ${displayName}.`}\n${result.stderr || ""}`;
        state.branches.menuOpen = false;
        await refreshRepoData();
      } catch (error) {
        gitOutput = `Branch switch failed: ${errorMessage(error)}`;
        state.branches.menuOpen = false;
        render();
      } finally {
        state.branches.switchingTo = "";
        render();
      }
    });
  });

  // Branch menu actions (create/rename/delete dialog)
  document.querySelectorAll(".branch-menu-action").forEach((button) => {
    button.addEventListener("click", () => {
      openBranchDialog(button.dataset.branchAction, button.dataset.branchName || "");
    });
  });

  // Branch dialog
  document.getElementById("branch-dialog-cancel")?.addEventListener("click", () => closeBranchDialog());

  document.getElementById("branch-dialog-input")?.addEventListener("input", (event) => {
    state.branches.dialog.value = event.target.value;
  });

  document.getElementById("branch-dialog-confirm")?.addEventListener("click", async () => {
    const dialog = state.branches.dialog;
    const value = document.getElementById("branch-dialog-input")?.value?.trim() || "";
    if (!value) {
      dialog.error = dialog.mode === "delete" ? "Choose a branch to delete." : "Branch name is required.";
      render();
      return;
    }
    try {
      const runner = dialog.mode === "create"
        ? () => createBranch(state.selectedRepoPath, value, selectedGitPath())
        : dialog.mode === "rename"
          ? () => renameBranch(state.selectedRepoPath, dialog.target, value, selectedGitPath())
          : () => deleteBranch(state.selectedRepoPath, value, false, selectedGitPath());
      const result = await runner();
      gitOutput = `${result.command}\n\n${result.stdout || "Branch updated."}\n${result.stderr || ""}`;
      Object.assign(dialog, { mode: "", target: "", value: "", error: "" });
      await refreshRepoData();
    } catch (error) {
      dialog.error = errorMessage(error);
      render();
    }
  });

  // Commit items in history
  document.querySelectorAll(".commit-item").forEach((button) => {
    button.addEventListener("click", () => loadCommitDetail(button.dataset.commitHash || ""));
  });

  // Refresh buttons
  document.getElementById("refresh-changes-btn")?.addEventListener("click", () => Promise.all([refreshWorkingTree(), refreshSyncState({ silent: true })]));
  document.getElementById("refresh-changes-main-btn")?.addEventListener("click", () => Promise.all([refreshWorkingTree(), refreshSyncState({ silent: true })]));
  document.getElementById("refresh-history-btn")?.addEventListener("click", () => refreshHistory());

  // Clone modal handlers
  document.getElementById("clone-server-select")?.addEventListener("change", async (event) => {
    updateSettings({ activeServerId: event.target.value || null });
    await loadRepositories();
    render();
  });

  document.getElementById("clone-repo-select")?.addEventListener("change", (event) => {
    state.cloneUrlInput = event.target.value;
    render();
  });

  document.getElementById("clone-destination-browse-btn")?.addEventListener("click", async () => {
    state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
    let selectedDirectory = "";
    try {
      selectedDirectory = await browseDirectory(state.settings.defaultCloneDirectory || "");
    } catch (error) {
      gitOutput = `Could not open folder picker: ${errorMessage(error)}`;
      render();
      return;
    }

    if (!selectedDirectory) return;
    const repoName = repoNameFromUrl(state.cloneUrlInput);
    state.cloneDestinationInput = repoName ? joinDirectoryPath(selectedDirectory, repoName) : selectedDirectory;
    render();
  });

  document.getElementById("clone-btn")?.addEventListener("click", async () => {
    state.cloneUrlInput = document.getElementById("clone-url-input")?.value?.trim() || "";
    state.cloneDestinationInput = document.getElementById("clone-destination-input")?.value?.trim() || "";

    if (!state.cloneUrlInput || !state.cloneDestinationInput) {
      gitOutput = "Clone URL and destination path are required.";
      render();
      return;
    }

    try {
      const result = await runGitClone(
        state.cloneUrlInput,
        state.cloneDestinationInput,
        state.settings.gitExecutablePath
      );
      gitOutput = `${result.command}\n\n${result.stdout || "Clone completed."}\n${result.stderr || ""}`;
      selectLocalRepo(state.cloneDestinationInput);
      activeModal = "";
      activeView = "changes";
      await refreshRepoData();
    } catch (error) {
      gitOutput = `Clone failed: ${errorMessage(error)}`;
    }
    render();
  });

  // Settings modal handlers
  document.getElementById("external-editor-select")?.addEventListener("change", (event) => {
    document.getElementById("custom-editor-row")?.classList.toggle("hidden", event.target.value !== CUSTOM_EDITOR_VALUE);
  });
  document.getElementById("rescan-editors-btn")?.addEventListener("click", () => refreshInstalledIdes());
  document.getElementById("custom-editor-browse-btn")?.addEventListener("click", async () => {
    const currentValue = document.getElementById("custom-editor-input")?.value?.trim() || "";
    let selectedApplication = "";
    try {
      selectedApplication = await browseApplication(currentValue);
    } catch (error) {
      settingsNotice = `Could not open application picker: ${errorMessage(error)}`;
      render();
      return;
    }

    if (!selectedApplication) return;
    const input = document.getElementById("custom-editor-input");
    if (input) input.value = selectedApplication;
  });

  document.getElementById("save-basic-settings-btn")?.addEventListener("click", () => {
    const editorSelection = document.getElementById("external-editor-select")?.value || DEFAULT_EDITOR_VALUE;
    const customEditorPath = document.getElementById("custom-editor-input")?.value?.trim() || "";
    const externalEditorPath =
      editorSelection === DEFAULT_EDITOR_VALUE
        ? ""
        : editorSelection === CUSTOM_EDITOR_VALUE
          ? customEditorPath
          : editorSelection;

    updateSettings({
      theme: document.getElementById("theme-select")?.value || "dark",
      gitExecutablePath: document.getElementById("git-path-input")?.value?.trim() || "",
      defaultCloneDirectory: document.getElementById("default-clone-dir-input")?.value?.trim() || "",
      externalEditorPath,
      activeServerId: document.getElementById("default-server-select")?.value || null,
      autoFetchOnRepoOpen: Boolean(document.getElementById("auto-fetch-checkbox")?.checked),
    });
    applyTheme();
    settingsNotice = "Saved.";
    render();
  });

  // Servers modal handlers
  document.getElementById("add-server-btn")?.addEventListener("click", () => openServerForm());

  document.querySelectorAll(".set-default-server-btn").forEach((button) => {
    button.addEventListener("click", async () => {
      updateSettings({ activeServerId: button.dataset.id });
      settingsNotice = "Active server updated.";
      await loadRepositories();
      render();
    });
  });

  document.querySelectorAll(".delete-server-btn").forEach((button) => {
    button.addEventListener("click", () => {
      const nextServers = getState().settings.servers.filter((server) => server.id !== button.dataset.id);
      const nextActive =
        getState().settings.activeServerId === button.dataset.id
          ? nextServers[0]?.id || null
          : getState().settings.activeServerId;
      setSettings({ ...getState().settings, servers: nextServers, activeServerId: nextActive });
      settingsNotice = "Server removed.";
      render();
    });
  });

  document.querySelectorAll(".edit-server-btn").forEach((button) => {
    button.addEventListener("click", () => openServerForm(button.dataset.id));
  });

  // Viewer handlers (inside viewer modal)
  document.getElementById("viewer-branch-select")?.addEventListener("change", (event) => {
    state.viewer.branch = event.target.value;
    loadViewerPath("");
  });

  document.getElementById("viewer-refresh-btn")?.addEventListener("click", () => {
    loadViewerPath(state.viewer.path);
  });

  document.querySelectorAll(".viewer-crumb-btn").forEach((button) => {
    button.addEventListener("click", () => {
      loadViewerPath(button.dataset.viewerPath || "");
    });
  });

  document.querySelectorAll(".viewer-row").forEach((button) => {
    button.addEventListener("click", () => {
      const path = button.dataset.entryPath || "";
      if (button.dataset.entryType === "dir") {
        loadViewerPath(path);
      } else {
        openViewerFile(path);
      }
    });
  });
}

function render() {
  applyTheme();
  applyPlatformChrome();
  dashboardView();
}

window.addEventListener("DOMContentLoaded", async () => {
  applyTheme();
  applyPlatformChrome();
  await loadRepositories();
  if (getState().selectedRepoPath) {
    await refreshRepoData();
    if (getState().settings.autoFetchOnRepoOpen) {
      runRepoCommand("Fetch", () => runGitFetch(getState().selectedRepoPath, selectedGitPath()), "fetch").then(refreshRepoData);
    }
  }
  window.setInterval(() => {
    const state = getState();
    if (!state.selectedRepoPath || state.sync.operation || state.sync.loading) return;
    refreshSyncState({ silent: true });
  }, 30000);
  render();
});
