// Single source of truth for "where am I running" so UI code (menus, window
// chrome, file-control features) can branch without sprinkling window checks.
//
// Two runtimes: Tauri (desktop) and plain web. The Tauri bridge is assembled
// here from @tauri-apps/api so the rest of the renderer sees one stable call
// surface regardless of which runtime is underneath.
import { getPin } from './data/serverPins';
import { CLIENT_VERSION, CLIENT_VERSION_HEADER } from './version';

export const isTauri =
  typeof window !== 'undefined' && Boolean(window.__TAURI_INTERNALS__);

let bridge;
// Hoisted out of the `if (isTauri)` block below so hydrateSharedState() (also
// module-level) can invoke Rust commands directly, without needing its own
// copy of the bridge object.
let invoke;

if (isTauri) {
  const core = await import('@tauri-apps/api/core');
  invoke = core.invoke;
  const { Channel } = core;
  const { open } = await import('@tauri-apps/plugin-dialog');
  // API requests must NOT use the webview's fetch: WKWebView (macOS) and
  // WebView2 (Windows) treat the tauri:// origin as secure and block plain
  // http:// requests to any non-localhost host as mixed content — which is
  // exactly how a self-hosted server on a LAN or tailnet is addressed. The
  // plugin executes requests in Rust, same fetch signature.
  const { fetch: nativeFetch } = await import('@tauri-apps/plugin-http');
  // Aliased: the plugin's `openPath` would otherwise shadow-collide with the
  // module-level `openPath` export below.
  const { openPath: pluginOpenPath, revealItemInDir } = await import(
    '@tauri-apps/plugin-opener'
  );
  const { getCurrentWebview } = await import('@tauri-apps/api/webview');
  // Custom window chrome (Windows): the native titlebar is removed in lib.rs, so
  // MenuBar.jsx drives minimize/maximize/close through this window handle.
  const { getCurrentWindow } = await import('@tauri-apps/api/window');
  const appWindow = getCurrentWindow();

  // Tauri exposes 'macos' | 'windows' | 'linux' at build time; the UI expects
  // Node-style 'darwin' | 'win32' | 'linux'.
  const tauriPlatform = import.meta.env.TAURI_ENV_PLATFORM ?? '';
  const nodeStyle = tauriPlatform.includes('darwin') || tauriPlatform.includes('macos')
    ? 'darwin'
    : tauriPlatform.includes('windows')
      ? 'win32'
      : 'linux';

  // Callers (some fire-and-forget) rely on an { ok:false, ... } object rather
  // than a thrown/rejected promise. Tauri's invoke() rejects on serde/command
  // errors, so wrap each invoke-backed call to normalize rejections back into
  // that resolved-failure shape.
  const safe = (fallback) => (promise) =>
    promise.catch((e) => ({ ok: false, error: String(e), ...fallback }));

  // Transfer commands take a required IPC channel for progress events
  // ({ kind, path, done, total, bytesDone, bytesTotal }); a caller without a
  // callback still gets a (no-op) channel because Rust's command signature
  // requires one.
  const progressChannel = (onProgress) => {
    const channel = new Channel();
    channel.onmessage = typeof onProgress === 'function' ? onProgress : () => {};
    return channel;
  };

  // All API traffic runs through Rust with fingerprint pinning (TOFU).
  // https URL + no stored pin, or a cert that doesn't match, is REFUSED in
  // Rust — never silently accepted. Plain-http dev servers bypass pinning.
  const pinnedApiFetch = async (url, { method = 'GET', headers = {}, body, binary } = {}) => {
    const bytes =
      body === undefined || body === null
        ? null
        : typeof body === 'string'
          ? Array.from(new TextEncoder().encode(body))
          : Array.from(new Uint8Array(body));
    const resp = await invoke('pinned_fetch', {
      method, url, headers, body: bytes, pin: getPin(url), binary: binary === true,
    });
    return {
      ok: resp.status >= 200 && resp.status < 300,
      status: resp.status,
      json: async () => JSON.parse(resp.body || '{}'),
      text: async () => resp.body,
    };
  };

  bridge = {
    isTauri: true, // "has a native desktop bridge"
    platform: nodeStyle,
    // The webview's own fetch is kept only as `nativeFetch` above for
    // reference (mixed-content note); all API traffic runs pinned through
    // Rust — see pinnedApiFetch.
    apiFetch: pinnedApiFetch,
    getDeviceName: () => invoke('get_device_name').catch(() => 'this-device'),
    // Trust-on-first-use: fetch the server's live TLS certificate (unpinned —
    // this call happens precisely because there's no pin yet) so the trust
    // prompt can show its fingerprint for out-of-band verification. Resolves
    // to { fingerprint, subject, not_before, not_after }; rejects if the host
    // is unreachable.
    fetchServerCertificate: (host) => invoke('fetch_server_certificate', { host }),
    selectWorkspaceFolder: async () => {
      try {
        const picked = await open({
          title: 'Choose Workspace Folder',
          directory: true,
          canCreateDirectories: true,
        });
        return typeof picked === 'string' ? picked : null;
      } catch {
        return null;
      }
    },
    readWorkspaceTree: (folderPath, options) =>
      safe({})(invoke('read_workspace_tree', { folderPath, options: options ?? null })),
    pullWorkspaceFiles: (payload, onProgress, pin) =>
      safe({ changed: [], created: [], dirsCreated: [], errors: [] })(
        invoke('pull_workspace_files', {
          payload,
          onProgress: progressChannel(onProgress),
          pin: pin ?? null,
        })
      ),
    setWorkspaceWritable: (payload) =>
      safe({ failed: [] })(invoke('set_workspace_writable', { payload })),
    deleteWorkspaceFiles: (payload) =>
      safe({ failed: [] })(invoke('delete_workspace_files', { payload })),
    uploadWorkspaceFiles: (payload, onProgress, pin) =>
      safe({ results: [] })(
        invoke('upload_workspace_files', {
          payload,
          onProgress: progressChannel(onProgress),
          pin: pin ?? null,
        })
      ),
    // The desktop console runs the shared cambium grammar in-process (no
    // /api/command). Resolves to { ok, output, changed }; a rejected invoke
    // (infra failure) is normalized to that same shape so the panel never sees
    // a throw.
    runConsoleCommand: (line, ctx) =>
      invoke('run_console_command', { line, ctx }).catch((e) => ({
        ok: false,
        output: String(e),
        changed: [],
      })),
    // The GUI's generalized command path (Task 2/18b): same shared grammar +
    // executor as runConsoleCommand, but resolving to the full structured
    // CommandOutputDto ({ ok, code, human, data, changed }) and carrying a
    // real progress channel, so a GUI-issued pull/submit can drive the
    // toolbar progress strip. A rejected invoke (infra failure) is
    // normalized to that same shape so callers never see a throw.
    runCommandStructured: (line, ctx, onProgress) =>
      invoke('run_command_structured', { line, ctx, onProgress: progressChannel(onProgress) }).catch(
        (e) => ({ ok: false, code: 1, human: String(e), data: null, changed: [] })
      ),
    // FileTree's status column, computed by the same cambium-core engine
    // `cambium status` uses. Resolves to a `ComputeStatusResponse` or null on
    // any failure — useFileControl's engine effect treats null as "keep the
    // previous statuses" rather than clobbering them with an empty result.
    computeStatus: (inputs) => invoke('compute_status', { inputs }).catch(() => null),
    loadClientState: (key) => invoke('load_client_state', { key }).catch(() => undefined),
    saveClientState: (key, value) =>
      invoke('save_client_state', { key, value }).then(() => true).catch(() => false),
    // Shared workspaces.json (device-wide, not per-key like client-state
    // above): { formatVersion, activeProject, workspaces: { id: { name,
    // folder } } }. Always resolves to a valid JSON string (Rust creates the
    // default file on first read) — see ConnectionContext's native mirror.
    workspacesLoad: () => invoke('workspaces_load').catch(() => null),
    workspacesSave: (value) =>
      invoke('workspaces_save', { value }).then(() => true).catch(() => false),
    // Local encrypted credential storage for "remember me".
    saveSecret: (account, value) => invoke('save_secret', { account, value }),
    loadSecret: (account) => invoke('load_secret', { account }),
    deleteSecret: (account) => invoke('delete_secret', { account }),
    openPath: (path) =>
      pluginOpenPath(path).then(() => ({ ok: true })).catch((e) => ({ ok: false, error: String(e) })),
    revealInFileManager: (path) =>
      revealItemInDir(path).then(() => ({ ok: true })).catch((e) => ({ ok: false, error: String(e) })),
    importWorkspacePaths: (payload) =>
      safe({ copied: [], collisions: [], skipped: [], errors: [] })(
        invoke('import_workspace_paths', { payload })
      ),
    createWorkspaceFolder: (payload) =>
      safe({})(invoke('create_workspace_folder', { payload })),
    // Native drag-drop: the webview suppresses HTML5 file drops in Tauri, and
    // only this event carries real OS paths. Positions arrive physical; the UI
    // hit-tests with elementFromPoint, which wants CSS pixels.
    onFileDrop: (callback) =>
      getCurrentWebview().onDragDropEvent((event) => {
        const { type, paths, position } = event.payload;
        const scale = window.devicePixelRatio || 1;
        callback({
          type,
          paths,
          x: position ? position.x / scale : undefined,
          y: position ? position.y / scale : undefined,
        });
      }),
    // Custom window controls for the frameless Windows chrome. onResized
    // resolves to an unlisten fn (Tauri event handle).
    windowControls: {
      minimize: () => appWindow.minimize(),
      toggleMaximize: () => appWindow.toggleMaximize(),
      close: () => appWindow.close(),
      isMaximized: () => appWindow.isMaximized(),
      onResized: (cb) => appWindow.onResized(cb),
    },
    // No native menu in the Tauri shell.
    onCommand: undefined,
  };
}

// Stable, never-shown ID for this install. Devices act as unique workspaces:
// a file checked out from your laptop can't be edited from your desktop even
// though both are the same user. The server tracks (deviceId, name) pairs per
// user, and lock ownership is scoped to this ID. On desktop this now lives in
// the shared Cambium device.json (so a future CLI reports the same identity);
// this file's own localStorage id — if one already existed from before this
// migration — is sent as the fallback so an existing install's id carries
// over into the shared file rather than minting a new one, and is kept in
// sync afterward purely so a later web build of the same profile still finds
// something reasonable. On web there's no shared file at all: the
// localStorage id is the whole story, exactly as before.
const DEVICE_ID_KEY = 'cbm-device-id-v1'; // matches ConnectionContext.jsx's (retired) constant

async function hydrateSharedState() {
  if (isTauri) {
    try {
      let fallbackId = null;
      try {
        fallbackId = localStorage.getItem(DEVICE_ID_KEY);
      } catch {
        /* no fallback available; Rust mints a fresh id */
      }
      const identity = await invoke('get_device_identity', {
        fallbackId: fallbackId || null,
        fallbackName: null,
      });
      try {
        localStorage.setItem(DEVICE_ID_KEY, identity.deviceId);
      } catch {
        /* not fatal — the shared file is the real source of truth now */
      }
      return identity.deviceId;
    } catch {
      // Fall through to the web-style local id below (native call failed).
    }
  }
  try {
    let id = localStorage.getItem(DEVICE_ID_KEY);
    if (!id) {
      id = crypto.randomUUID();
      localStorage.setItem(DEVICE_ID_KEY, id);
    }
    return id;
  } catch {
    return 'device-unknown';
  }
}

// Resolved once at module load (platform.js already top-level-awaits the
// Tauri bridge above, so synchronous consumers — e.g. ConnectionContext's
// `deviceId` field — keep working exactly as when this was a local useMemo).
export const deviceId = await hydrateSharedState();

export const isWeb = !isTauri;

// fetch for server API calls: Rust-side on desktop (immune to the webview's
// mixed-content policy), window.fetch on web. Same call signature as fetch.
const rawApiFetch = bridge?.apiFetch ?? ((...args) => window.fetch(...args));

// Every server request identifies its client version — the server gates each
// one on it and answers 426 when the majors differ (see ./version). Stamped
// here rather than in RcApi.request because this is the real choke point:
// avatarCache downloads avatars through apiFetch directly, and a request that
// skipped the header would simply be refused.
//
// Caller-supplied headers win, so a test or a future caller can still send a
// different version deliberately.
export function apiFetch(url, options = {}) {
  return rawApiFetch(url, {
    ...options,
    headers: { [CLIENT_VERSION_HEADER]: CLIENT_VERSION, ...options.headers },
  });
}

// Frameless-window controls (minimize/maximize/close/isMaximized/onResized).
// Null on web/macOS-callers; the menu bar only reads it when isWindows.
export const windowControls = bridge?.windowControls ?? null;

// 'darwin' | 'win32' | 'linux'. Null on web.
export const platform = bridge?.platform ?? null;
export const isMac = platform === 'darwin';
export const isWindows = platform === 'win32';
export const isLinux = platform === 'linux';

// Native menu -> renderer command bus. No-op on web so callers stay simple.
export function onNativeCommand(callback) {
  return bridge?.onCommand?.(callback) ?? (() => {});
}

// The machine's hostname, used as the default device name for new accounts.
export function getDeviceName() {
  return bridge?.getDeviceName?.() ?? Promise.resolve('this-device');
}

// The server's live TLS certificate, for the trust-on-first-use prompt:
// { fingerprint, subject, not_before, not_after }. Web has no native TLS
// introspection (and no pinning story at all — see apiFetch above), so it
// resolves to null there; ConnectionContext treats that as "nothing to show,
// skip the prompt" rather than an error. Can reject (host unreachable) on
// desktop — callers should catch.
export function fetchServerCertificate(host) {
  return bridge?.fetchServerCertificate?.(host) ?? Promise.resolve(null);
}

// Open the native folder picker. Resolves to an absolute path, or null if the
// user cancelled (or we're on web, where there is no filesystem access).
export function selectWorkspaceFolder() {
  return bridge?.selectWorkspaceFolder?.() ?? Promise.resolve(null);
}

// Read a local folder recursively for the workspace file tree.
// Resolves to { ok, children, modifiedMs } or { ok: false, error }.
// The listing is always complete — any size, any depth.
// options.hashes adds sha256 + writable per file (for sync status).
export function readWorkspaceTree(folderPath, options) {
  return (
    bridge?.readWorkspaceTree?.(folderPath, options) ??
    Promise.resolve({
      ok: false,
      error: 'Browsing local folders requires the desktop app.',
    })
  );
}

// Download depot files into the workspace (written read-only). payload:
// { folderPath, files: [{ path, url, hash, sizeBytes }], headers, archiveUrl }.
// onProgress (optional) receives { kind, path, done, total, bytesDone,
// bytesTotal } per landed file. pin: the server's trust-on-first-use
// fingerprint (see data/serverPins), forwarded to Rust so the transfer client
// is built pinned same as apiFetch. Resolves to { ok, changed, created,
// dirsCreated, errors }.
export function pullWorkspaceFiles(payload, onProgress, pin) {
  return (
    bridge?.pullWorkspaceFiles?.(payload, onProgress, pin) ??
    Promise.resolve({ ok: false, error: 'Syncing files requires the desktop app.' })
  );
}

// Toggle the local read-only bit — writable on checkout, read-only on revert.
// payload: { folderPath, paths, writable }. Resolves to { ok, failed }.
export function setWorkspaceWritable(payload) {
  return (
    bridge?.setWorkspaceWritable?.(payload) ??
    Promise.resolve({ ok: false, failed: [] })
  );
}

// Remove workspace files (marking for delete deletes the local copy).
// payload: { folderPath, paths }. Resolves to { ok, failed }.
export function deleteWorkspaceFiles(payload) {
  return (
    bridge?.deleteWorkspaceFiles?.(payload) ??
    Promise.resolve({ ok: false, failed: [] })
  );
}

// Stream workspace files to the server's staging area ahead of a submit.
// payload: { folderPath, files: [{ path, url, sizeBytes }], headers }.
// onProgress as in pullWorkspaceFiles; pin as in pullWorkspaceFiles.
// Resolves to { ok, results: [{ path, hash?, sizeBytes?, error? }] }.
export function uploadWorkspaceFiles(payload, onProgress, pin) {
  return (
    bridge?.uploadWorkspaceFiles?.(payload, onProgress, pin) ??
    Promise.resolve({ ok: false, results: [], error: 'Submitting requires the desktop app.' })
  );
}

// Run a console command line through the shared cambium-core executor (the
// same grammar the `cambium` CLI uses). ctx carries the GUI's current session
// and project/workspace selection. Resolves to { ok, output, changed }; the
// web build has no native executor, so it reports that.
export function runConsoleCommand(line, ctx) {
  return (
    bridge?.runConsoleCommand?.(line, ctx) ??
    Promise.resolve({ ok: false, output: 'The console requires the desktop app.', changed: [] })
  );
}

// Run a console command line through the shared executor and resolve to the
// full structured CommandOutputDto ({ ok, code, human, data, changed }) —
// fileCommand.js's dispatchFileCommand builds every GUI file-control verb on
// this. onProgress (optional) receives transfer-progress events the same
// shape pullWorkspaceFiles/uploadWorkspaceFiles report. The web build has no
// native executor, so it reports that.
export function runCommandStructured(line, ctx, onProgress) {
  return (
    bridge?.runCommandStructured?.(line, ctx, onProgress) ??
    Promise.resolve({
      ok: false,
      code: 1,
      human: 'This command requires the desktop app.',
      data: null,
      changed: [],
    })
  );
}

// FileTree's status column: the cambium-core engine flattens the depot tree
// + workspace listing the GUI already fetched, runs compute_statuses, and
// answers in the `cambium status --json` vocabulary. Resolves to null on web
// (no native executor) or any native failure — never rejects.
export function computeStatus(inputs) {
  return bridge?.computeStatus?.(inputs) ?? Promise.resolve(null);
}

// Native per-key JSON persistence for state that outgrows localStorage
// (e.g. the synced-hash manifest on big workspaces). Resolves to the stored
// string, null if never saved, or undefined when there is no native store
// (web) / the read failed.
export function loadClientState(key) {
  return bridge?.loadClientState?.(key) ?? Promise.resolve(undefined);
}

// Resolves true when persisted natively; false on web or write failure.
export function saveClientState(key, value) {
  return bridge?.saveClientState?.(key, value) ?? Promise.resolve(false);
}

// The shared workspaces.json (native mirror source for ConnectionContext's
// WORKSPACES_KEY). Resolves to the raw JSON string, or null on web / a failed
// read (there is no "never saved" distinct from "default file" here — Rust
// always returns a valid default when the file doesn't exist yet).
export function loadWorkspacesNative() {
  return bridge?.workspacesLoad?.() ?? Promise.resolve(null);
}

// Resolves true when persisted natively; false on web or write failure.
export function saveWorkspacesNative(value) {
  return bridge?.workspacesSave?.(value) ?? Promise.resolve(false);
}

// Local encrypted credential storage, keyed by an opaque `account` string (see
// ConnectionContext). There is no config directory to write in a browser, so
// the web build resolves to no-ops rather than
// stashing the password anywhere.
export function saveSecret(account, value) {
  return bridge?.saveSecret?.(account, value) ?? Promise.resolve();
}

export function loadSecret(account) {
  return bridge?.loadSecret?.(account) ?? Promise.resolve(null);
}

export function deleteSecret(account) {
  return bridge?.deleteSecret?.(account) ?? Promise.resolve();
}

// Open a file with the OS default application. { ok:false } on web.
export function openPath(path) {
  return bridge?.openPath?.(path) ?? Promise.resolve({ ok: false, error: 'Desktop app only.' });
}

// Select a file/folder in the OS file manager. { ok:false } on web.
export function revealInFileManager(path) {
  return (
    bridge?.revealInFileManager?.(path) ?? Promise.resolve({ ok: false, error: 'Desktop app only.' })
  );
}

// The menu label for revealInFileManager, per platform.
export const fileManagerLabel = isMac
  ? 'Reveal in Finder'
  : isWindows
    ? 'Show in Explorer'
    : 'Show in File Manager';

// Copy OS paths into the workspace (drag-and-drop). See workspace.rs.
export function importWorkspacePaths(payload) {
  return (
    bridge?.importWorkspacePaths?.(payload) ??
    Promise.resolve({ ok: false, copied: [], collisions: [], skipped: [], errors: [] })
  );
}

// Create a folder inside the workspace. { ok:false } on web. See workspace.rs.
export function createWorkspaceFolder(payload) {
  return (
    bridge?.createWorkspaceFolder?.(payload) ??
    Promise.resolve({ ok: false, error: 'Desktop app only.' })
  );
}

// Subscribe to native file drag-drop. Resolves to an unlisten fn; never fires on web.
export function onFileDrop(callback) {
  return bridge?.onFileDrop?.(callback) ?? Promise.resolve(() => {});
}
