import type {
  SessionResponse,
  RichDiffFilesResponse,
  RichFileDiffResponse,
  AgentInfo,
  ProfileInfo,
  BrowseResponse,
  GroupInfo,
  ProjectInfo,
  DockerStatusResponse,
  CreateSessionRequest,
} from "./types";

// GET a JSON endpoint; returns null on non-2xx or network/parse errors.
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T | null> {
  try {
    const res = await fetch(url, init);
    if (!res.ok) return null;
    return (await res.json()) as T;
  } catch {
    return null;
  }
}

// --- Sessions ---

export interface SessionsEnvelope {
  sessions: SessionResponse[];
  workspace_ordering: string[];
}

export function fetchSessions(): Promise<SessionsEnvelope | null> {
  return fetchJson<SessionsEnvelope>("/api/sessions");
}

export async function updateWorkspaceOrdering(order: string[]): Promise<boolean> {
  try {
    const res = await fetch("/api/workspace-ordering", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ order }),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export interface EnsureSessionResult {
  ok: boolean;
  status?: "alive" | "restarted";
  error?: string;
  message?: string;
}

export async function ensureSession(
  id: string,
  signal?: AbortSignal,
): Promise<EnsureSessionResult> {
  try {
    const res = await fetch(`/api/sessions/${id}/ensure`, {
      method: "POST",
      signal,
    });
    const body = await res.json().catch(() => ({}));
    if (!res.ok) {
      return {
        ok: false,
        error: typeof body.error === "string" ? body.error : undefined,
        message:
          typeof body.message === "string"
            ? body.message
            : `Server error (${res.status})`,
      };
    }
    return {
      ok: true,
      status: body.status as "alive" | "restarted" | undefined,
    };
  } catch (e) {
    if ((e as { name?: string }).name === "AbortError") {
      return { ok: false, error: "aborted" };
    }
    return {
      ok: false,
      message: e instanceof Error ? e.message : "Network error",
    };
  }
}

export async function ensureTerminal(
  id: string,
  container = false,
): Promise<boolean> {
  const path = container ? "container-terminal" : "terminal";
  try {
    const res = await fetch(`/api/sessions/${id}/${path}`, {
      method: "POST",
    });
    return res.ok;
  } catch {
    return false;
  }
}

export function getSessionDiffFiles(
  id: string,
): Promise<RichDiffFilesResponse | null> {
  return fetchJson<RichDiffFilesResponse>(`/api/sessions/${id}/diff/files`);
}

export function getSessionFileDiff(
  id: string,
  filePath: string,
  repoName?: string,
): Promise<RichFileDiffResponse | null> {
  const params = new URLSearchParams({ path: filePath });
  if (repoName) params.set("repo", repoName);
  return fetchJson<RichFileDiffResponse>(
    `/api/sessions/${id}/diff/file?${params.toString()}`,
  );
}

// --- Settings ---

export interface SettingsResponse {
  theme?: {
    idle_decay_minutes?: number;
  };
  [key: string]: unknown;
}

export function fetchSettings(profile?: string): Promise<SettingsResponse | null> {
  const params = profile ? `?profile=${encodeURIComponent(profile)}` : "";
  return fetchJson<SettingsResponse>(`/api/settings${params}`);
}

export async function updateSettings(
  updates: Record<string, unknown>,
): Promise<boolean> {
  try {
    const res = await fetch("/api/settings", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(updates),
    });
    return res.ok;
  } catch {
    return false;
  }
}

// --- Profile management ---

export async function createProfile(name: string): Promise<boolean> {
  try {
    const res = await fetch("/api/profiles", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export async function deleteProfile(name: string): Promise<boolean> {
  try {
    const res = await fetch(`/api/profiles/${encodeURIComponent(name)}`, {
      method: "DELETE",
    });
    return res.ok;
  } catch {
    return false;
  }
}

export async function renameProfile(
  name: string,
  newName: string,
): Promise<boolean> {
  try {
    const res = await fetch(
      `/api/profiles/${encodeURIComponent(name)}/rename`,
      {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ new_name: newName }),
      },
    );
    return res.ok;
  } catch {
    return false;
  }
}

export async function setDefaultProfile(name: string): Promise<boolean> {
  try {
    const res = await fetch("/api/default-profile", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name }),
    });
    return res.ok;
  } catch {
    return false;
  }
}

export function getProfileSettings(
  name: string,
): Promise<Record<string, unknown> | null> {
  return fetchJson<Record<string, unknown>>(
    `/api/profiles/${encodeURIComponent(name)}/settings`,
  );
}

export async function updateProfileSettings(
  name: string,
  updates: Record<string, unknown>,
): Promise<boolean> {
  try {
    const res = await fetch(
      `/api/profiles/${encodeURIComponent(name)}/settings`,
      {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(updates),
      },
    );
    return res.ok;
  } catch {
    return false;
  }
}

// --- Themes & Sounds ---

import type { ResolvedTheme } from "./theme";

export async function fetchThemes(): Promise<string[]> {
  return (await fetchJson<string[]>("/api/themes")) ?? [];
}

/** Fetch the resolved theme projection (web CSS vars, terminal CSS
 *  vars, syntax highlighter selection) for a named theme. The server
 *  falls back to Empire for unknown names; check `source` to detect. */
export function fetchResolvedTheme(
  name: string,
): Promise<ResolvedTheme | null> {
  return fetchJson<ResolvedTheme>(
    `/api/themes/${encodeURIComponent(name)}`,
  );
}

/** Fetch the resolved theme for the active profile's current
 *  selection. Server reads from profile_config so per-profile overrides
 *  land in the right place. */
export function fetchCurrentTheme(): Promise<ResolvedTheme | null> {
  return fetchJson<ResolvedTheme>("/api/theme/current");
}

export async function fetchSounds(): Promise<string[]> {
  return (await fetchJson<string[]>("/api/sounds")) ?? [];
}

/** Fetch a sound file as a Blob so the cockpit's browser-side approval
 *  player can hand a blob URL to `new Audio(...)`. The fetch path runs
 *  through `fetchInterceptor.ts`, which injects `Authorization: Bearer`
 *  on every request; an `<audio src="...">` element does not, so a
 *  blob round-trip is necessary in PWA mode. See #1038. */
export async function fetchSoundBlob(name: string): Promise<Blob | null> {
  try {
    const res = await fetch(`/api/sounds/file/${encodeURIComponent(name)}`);
    if (!res.ok) return null;
    return await res.blob();
  } catch {
    return null;
  }
}

// --- About / server info ---

export interface ServerAbout {
  version: string;
  auth_required: boolean;
  passphrase_enabled: boolean;
  /** Resolved `--auth` mode. `"token"` means the URL token gates
   *  requests; `"passphrase"` means the passphrase login wall is the
   *  only human gate; `"none"` means no authentication at all. The
   *  Security panel renders an accurate label off this instead of
   *  guessing "--no-auth" from `auth_required === false`. */
  auth_mode: "token" | "passphrase" | "none";
  read_only: boolean;
  behind_tunnel: boolean;
  profile: string;
  /** Live value of the cockpit master switch (`config.cockpit.enabled`).
   *  Toggleable from the web settings via PATCH /api/cockpit/master.
   *  When true, new sessions for ACP-capable tools default to cockpit
   *  mode; when false, every new session is tmux. */
  cockpit_master_enabled: boolean;
  /** Resolved `cockpit.show_tool_durations` from the active profile's
   *  config. Drives the per-tool elapsed-time label in the cockpit
   *  web UI; cross-device since it lives in config.toml. */
  cockpit_show_tool_durations: boolean;
  /** Resolved `cockpit.queue_drain_mode` from the active profile's
   *  config. Selects how the composer drains client-side queued
   *  follow-up prompts on Stopped: `combined` (default) joins them
   *  with blank lines into a single prompt; `serial` fires one entry
   *  at a time. See #1031. */
  cockpit_queue_drain_mode: "combined" | "serial";
  /** Resolved `cockpit.max_concurrent_resumes` from the active
   *  profile's config. Upper bound on parallel cockpit worker
   *  spawns/attaches the reconciler runs on `aoe serve` cold start.
   *  See #1088. */
  cockpit_max_concurrent_resumes: number;
  /** Resolved `cockpit.force_end_turn_threshold_secs` from the active
   *  profile's config. Seconds of streaming inactivity after which
   *  the cockpit web UI offers a "Force end turn" button. See #1100. */
  cockpit_force_end_turn_threshold_secs: number;
  /** Resolved `cockpit.replay_events` from the active profile's
   *  config. Per-session retention cap on the cockpit event log;
   *  0 means unlimited. Mirrored onto the in-memory activity buffer
   *  so the rendered transcript matches the user's chosen ceiling
   *  instead of clipping at a hard-coded frontend constant. See #1111. */
  cockpit_replay_events: number;
  build_flavor: "debug" | "release"; // `"debug"` => debug_assertions; drives topbar DEV badge. See #1055.
}

export async function setCockpitMaster(
  enabled: boolean,
): Promise<{ master_enabled: boolean } | null> {
  try {
    const res = await fetch("/api/cockpit/master", {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ enabled }),
    });
    if (!res.ok) return null;
    return await res.json();
  } catch {
    return null;
  }
}

export function fetchAbout(): Promise<ServerAbout | null> {
  return fetchJson<ServerAbout>("/api/about");
}

/** Runtime helper around `ServerAbout.build_flavor`. See #1055. */
export function isDebugBuild(about: ServerAbout | null | undefined): boolean {
  if (!about) return false;
  return about.build_flavor === "debug";
}
export interface UpdateStatus {
  check_enabled: boolean;
  current_version: string;
  latest_version: string | null;
  update_available: boolean;
  release_url: string | null;
  web_poll_interval_minutes: number;
  error: string | null;
}

export function fetchUpdateStatus(): Promise<UpdateStatus | null> {
  return fetchJson<UpdateStatus>("/api/system/update-status");
}

// --- Branches ---

export interface BranchInfo {
  name: string;
  is_current: boolean;
  remote_only?: boolean;
}

/** Lists branches for a repo path. When `includeRemote` is true the
 *  response includes branches that only exist on the remote (with
 *  `remote_only: true`); selecting one bases the new worktree off the
 *  remote tip. See #948. */
export function fetchBranches(
  path: string,
  includeRemote = false,
): Promise<BranchInfo[] | null> {
  const params = new URLSearchParams({ path });
  if (includeRemote) params.set("include_remote", "true");
  return fetchJson<BranchInfo[]>(`/api/git/branches?${params.toString()}`);
}

// --- Cockpit context primer ---

export interface ContextPrimerResponse {
  primer: string;
  included_event_count: number;
  included_turn_count: number;
  truncated: boolean;
  max_chars: number;
  /** When the recap was built from a session that ended in a non-
   *  success terminal (rate-limit park or AgentStartupError), the
   *  user's most recent UserPromptSent never reached the agent. The
   *  backend pops it from the primer body and surfaces it here so the
   *  recovery UI can drop it back into the composer as the user's
   *  pending request. See #1281 / #1282. */
  unprocessed_prompt?: string | null;
}

// --- Cockpit ACP registry ---

export interface CockpitAgentInfo {
  name: string;
  description: string;
  command: string;
}

/** List ACP registry entries the cockpit supervisor knows about.
 *  Distinct from `/api/agents` (session-tool agents for the wizard);
 *  this is the *cockpit* registry used by the rate-limit recovery
 *  modal to populate the handoff target list. See #1282. */
export async function fetchCockpitAgents(): Promise<CockpitAgentInfo[]> {
  return (await fetchJson<CockpitAgentInfo[]>("/api/cockpit/agents")) ?? [];
}

// --- Cockpit switch agent ---

export interface SwitchAgentResponse {
  session_id: string;
  agent: string;
  /** Highest seq BEFORE AgentSwitched was emitted. Pass to
   *  fetchContextPrimer so the recap excludes the handoff event. */
  before_seq: number;
  /** Seq assigned to the AgentSwitched event. The frontend awaits the
   *  reducer reaching this seq before prefilling so the divider and
   *  composer prefill arrive in order. */
  switch_seq: number;
  status: string;
}

/** Hand off a cockpit session from its current ACP backend to
 *  `target` (registry key, e.g. "codex"). Backend stops the old
 *  worker, spawns the new one, persists the agent change, and emits
 *  an AgentSwitched event. On failure (unknown target, spawn error)
 *  the instance is left untouched. See #1282. */
export async function switchCockpitAgent(
  sessionId: string,
  target: string,
  model?: string | null,
): Promise<SwitchAgentResponse | null> {
  const body: { target: string; model?: string } = { target };
  if (model) body.model = model;
  return fetchJson<SwitchAgentResponse>(
    `/api/sessions/${encodeURIComponent(sessionId)}/cockpit/switch-agent`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    },
  );
}

/** Fetch a markdown primer built from events `seq < beforeSeq`. Used
 *  after a `session/load` failure: the agent's model context is empty
 *  but the transcript is intact in SQLite, so the user can opt in to
 *  pre-filling the composer with a compact recap. See #1004. */
export function fetchContextPrimer(
  sessionId: string,
  beforeSeq: number,
  signal?: AbortSignal,
): Promise<ContextPrimerResponse | null> {
  const params = new URLSearchParams({ before_seq: String(beforeSeq) });
  return fetchJson<ContextPrimerResponse>(
    `/api/sessions/${encodeURIComponent(sessionId)}/cockpit/context-primer?${params.toString()}`,
    signal ? { signal } : undefined,
  );
}

// --- Devices ---

export interface DeviceInfo {
  ip: string;
  user_agent: string;
  first_seen: string;
  last_seen: string;
  request_count: number;
}

export function fetchDevices(): Promise<DeviceInfo[] | null> {
  return fetchJson<DeviceInfo[]>("/api/devices");
}

// --- Wizard APIs ---

export async function fetchAgents(): Promise<AgentInfo[]> {
  return (await fetchJson<AgentInfo[]>("/api/agents")) ?? [];
}

export async function fetchProfiles(): Promise<ProfileInfo[]> {
  return (await fetchJson<ProfileInfo[]>("/api/profiles")) ?? [];
}

export async function getHomePath(): Promise<string | null> {
  const data = await fetchJson<{ path?: string }>("/api/filesystem/home");
  return data?.path ?? null;
}

export async function browseFilesystem(
  path: string,
  limit?: number,
): Promise<BrowseResponse & { ok: boolean }> {
  const params = new URLSearchParams({ path });
  if (limit != null) params.set("limit", String(limit));
  const data = await fetchJson<BrowseResponse>(`/api/filesystem/browse?${params}`);
  if (!data) return { entries: [], has_more: false, ok: false };
  return { ...data, ok: true };
}

export async function fetchGroups(): Promise<GroupInfo[]> {
  return (await fetchJson<GroupInfo[]>("/api/groups")) ?? [];
}

export async function fetchProjects(scope?: "global" | "profile"): Promise<ProjectInfo[]> {
  const url = scope ? `/api/projects?scope=${scope}` : "/api/projects";
  return (await fetchJson<ProjectInfo[]>(url)) ?? [];
}

export async function createProject(body: {
  path: string;
  name?: string;
  scope?: "global" | "profile";
  allow_override?: boolean;
}): Promise<{ ok: boolean; error?: string; project?: ProjectInfo }> {
  try {
    const res = await fetch("/api/projects", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const text = await res.text();
      try {
        const data = JSON.parse(text);
        return { ok: false, error: data.message || `Server error (${res.status})` };
      } catch {
        return { ok: false, error: text || `Server error (${res.status})` };
      }
    }
    const project = (await res.json()) as ProjectInfo;
    return { ok: true, project };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : String(e) };
  }
}

export async function deleteProject(
  name: string,
  scope: "global" | "profile",
): Promise<{ ok: boolean; error?: string }> {
  try {
    const res = await fetch(
      `/api/projects/${encodeURIComponent(name)}?scope=${scope}`,
      { method: "DELETE" },
    );
    if (!res.ok) {
      const text = await res.text();
      try {
        const data = JSON.parse(text);
        return { ok: false, error: data.message || `Server error (${res.status})` };
      } catch {
        return { ok: false, error: text || `Server error (${res.status})` };
      }
    }
    return { ok: true };
  } catch (e) {
    return { ok: false, error: e instanceof Error ? e.message : String(e) };
  }
}

export async function fetchDockerStatus(): Promise<DockerStatusResponse> {
  return (
    (await fetchJson<DockerStatusResponse>("/api/docker/status")) ?? {
      available: false,
      runtime: null,
    }
  );
}

export async function createSession(
  body: CreateSessionRequest,
): Promise<{ ok: boolean; error?: string; session?: SessionResponse }> {
  try {
    const res = await fetch("/api/sessions", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (!res.ok) {
      const text = await res.text();
      try {
        const data = JSON.parse(text);
        return {
          ok: false,
          error: data.message || `Server error (${res.status})`,
        };
      } catch {
        return {
          ok: false,
          error: `Server error (${res.status}): ${text.slice(0, 200)}`,
        };
      }
    }
    const data = await res.json();
    return { ok: true, session: data };
  } catch (e) {
    return {
      ok: false,
      error: `Network error: ${e instanceof Error ? e.message : "connection failed"}`,
    };
  }
}

// --- Clone ---

export async function cloneRepo(
  url: string,
  opts?: { destination?: string; shallow?: boolean },
): Promise<{ ok: boolean; path?: string; error?: string }> {
  try {
    const body: Record<string, unknown> = { url };
    if (opts?.destination) body.destination = opts.destination;
    if (opts?.shallow) body.shallow = true;
    const res = await fetch("/api/git/clone", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      return {
        ok: false,
        error: data.message || `Clone failed (${res.status})`,
      };
    }
    return { ok: true, path: data.path };
  } catch (e) {
    return {
      ok: false,
      error: `Network error: ${e instanceof Error ? e.message : "connection failed"}`,
    };
  }
}

// --- Login ---

export interface LoginStatus {
  required: boolean;
  authenticated: boolean;
  /** Whether the session currently sits inside the 15-minute step-up
   *  window. Sensitive routes (terminal attach, cockpit prompt /
   *  approval / file mutations) only execute while this is true.
   *  See #1131. */
  elevated: boolean;
  /** Seconds remaining on the current elevation window, or null when
   *  not elevated. */
  elevated_until_secs: number | null;
}

export async function loginStatus(): Promise<LoginStatus> {
  return (
    (await fetchJson<LoginStatus>("/api/login/status")) ?? {
      required: false,
      authenticated: true,
      elevated: true,
      elevated_until_secs: null,
    }
  );
}

/** Verify the auth token via a session-exempt endpoint (`/api/login/status`).
 *  Returning `true` means the token authenticated; the caller still has to
 *  consult `loginStatus()` to decide between the main app and LoginPage.
 *  Used by the token entry page so a valid-token-but-needs-passphrase paste
 *  is accepted instead of being misread as a token rejection. */
export async function verifyToken(): Promise<boolean> {
  try {
    const res = await fetch("/api/login/status");
    return res.ok;
  } catch {
    return false;
  }
}

export async function login(
  passphrase: string,
): Promise<{ ok: boolean; error?: string }> {
  let deviceBindingSecret: string;
  try {
    // Imported lazily to keep this module's load cost small; the
    // helper itself is sync. Generates on first call.
    const { getOrCreateDeviceBindingSecret } = await import(
      "./deviceBinding"
    );
    deviceBindingSecret = getOrCreateDeviceBindingSecret();
  } catch (err) {
    return {
      ok: false,
      error:
        err instanceof Error
          ? err.message
          : "Could not create device binding for this browser",
    };
  }
  try {
    const res = await fetch("/api/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        passphrase,
        device_binding_secret: deviceBindingSecret,
      }),
    });
    if (res.ok) return { ok: true };
    const data = await res.json().catch(() => null);
    return {
      ok: false,
      error: data?.message ?? `Login failed (${res.status})`,
    };
  } catch {
    return { ok: false, error: "Network error" };
  }
}

/**
 * Re-verify the passphrase to open a fresh 15-minute elevation
 * window. Required before the cockpit/terminal can perform
 * SSH-equivalent actions when the prior window has lapsed. See
 * #1131.
 *
 * Attaches the device-binding header explicitly rather than relying
 * on the global fetch interceptor; auth-sensitive endpoints should
 * not depend on monkey-patching to carry their second factor.
 */
export async function elevateLogin(
  passphrase: string,
): Promise<{ ok: boolean; error?: string; elevated_until_secs?: number }> {
  let bindingSecret: string;
  try {
    const { getOrCreateDeviceBindingSecret } = await import(
      "./deviceBinding"
    );
    bindingSecret = getOrCreateDeviceBindingSecret();
  } catch (err) {
    return {
      ok: false,
      error:
        err instanceof Error
          ? err.message
          : "Could not access device binding for this browser",
    };
  }
  try {
    const res = await fetch("/api/login/elevate", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Aoe-Device-Binding": bindingSecret,
      },
      body: JSON.stringify({ passphrase }),
    });
    if (res.ok) {
      const data = (await res.json().catch(() => null)) as {
        elevated_until_secs?: number;
      } | null;
      return {
        ok: true,
        elevated_until_secs: data?.elevated_until_secs,
      };
    }
    const data = await res.json().catch(() => null);
    return {
      ok: false,
      error: data?.message ?? `Elevation failed (${res.status})`,
    };
  } catch {
    return { ok: false, error: "Network error" };
  }
}

export async function logout(): Promise<void> {
  try {
    await fetch("/api/logout", { method: "POST" });
  } catch {
    // Best effort
  } finally {
    // Drop the per-device binding secret so a future login generates
    // a fresh one alongside the new session cookie. Without this, an
    // attacker who later obtains a stale localStorage snapshot still
    // holds a valid binding for the next session created on this
    // browser. See #1131.
    try {
      const { clearDeviceBindingSecret } = await import("./deviceBinding");
      clearDeviceBindingSecret();
    } catch {
      // ignore
    }
    // Drop the in-memory approval-sound caches so a future user on the
    // same tab does not see the previous user's settings snapshot or
    // hear their cached blob.
    try {
      const { clearApprovalSoundCache } = await import(
        "../hooks/useApprovalSound"
      );
      clearApprovalSoundCache();
    } catch {
      // ignore
    }
  }
}

export async function renameSession(
  id: string,
  title: string,
): Promise<boolean> {
  try {
    const res = await fetch(`/api/sessions/${id}`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ title }),
    });
    return res.ok;
  } catch {
    return false;
  }
}

/** Three-preset helper for the sidebar context menu:
 *  - "off":     set all three overrides to false (silence this session)
 *  - "default": clear all three overrides (inherit server defaults)
 *  - "all":     set all three overrides to true (notify on any event)
 *  Sends all three fields in one PATCH to avoid multi-request ordering. */
export async function setSessionNotifications(
  id: string,
  preset: "off" | "default" | "all",
): Promise<boolean> {
  const value =
    preset === "off" ? false : preset === "all" ? true : null;
  try {
    const res = await fetch(`/api/sessions/${id}/notifications`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        notify_on_waiting: value,
        notify_on_idle: value,
        notify_on_error: value,
      }),
    });
    return res.ok;
  } catch {
    return false;
  }
}

/** Set the per-session diff-base override. Pass `null` to clear the
 *  override and fall back to the profile default / auto-detection.
 *  See #970. */
export async function setSessionDiffBase(
  id: string,
  baseBranch: string | null,
): Promise<SessionResponse | null> {
  try {
    const res = await fetch(`/api/sessions/${id}/diff-base`, {
      method: "PATCH",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ base_branch: baseBranch }),
    });
    if (!res.ok) return null;
    return (await res.json()) as SessionResponse;
  } catch {
    return null;
  }
}

export interface DeleteSessionOptions {
  delete_worktree?: boolean;
  delete_branch?: boolean;
  delete_sandbox?: boolean;
  force_delete?: boolean;
}

export interface DeleteSessionResult {
  ok: boolean;
  error?: string;
}

export async function deleteSession(
  id: string,
  options: DeleteSessionOptions = {},
): Promise<DeleteSessionResult> {
  try {
    const res = await fetch(`/api/sessions/${id}`, {
      method: "DELETE",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(options),
    });
    if (!res.ok) {
      const data = await res.json().catch(() => ({}));
      return {
        ok: false,
        error: data.message || `Server error (${res.status})`,
      };
    }
    return { ok: true };
  } catch (e) {
    return {
      ok: false,
      error: `Network error: ${e instanceof Error ? e.message : "connection failed"}`,
    };
  }
}
