/**
 * Platform adapter (§4.9) — one interface, three implementations (web, PWA,
 * Tauri). Web and PWA share the same implementation here; the Tauri shell
 * (apps/desktop) supplies its own via `window.__TAURI__` detection and
 * overrides at bundle-init time. Keeping the interface in apps/web (not a
 * lib) is deliberate: it is consumed only at the app's composition root.
 */
export interface SecureStore {
  get(key: string): Promise<string | null>;
  set(key: string, value: string): Promise<void>;
  remove(key: string): Promise<void>;
}

export interface NotifyOptions {
  /** Replaces an earlier notification with the same tag rather than stacking. */
  tag?: string;
  /**
   * In-app path to open when the notification is clicked, e.g.
   * `/channel?workspaceId=1&channelId=2`. Replaces the old
   * workspaceId/channelId pair, which could only ever describe a message —
   * a task, a call and a meeting all need somewhere different to land.
   */
  route?: string;
  /** Groups related notifications in the OS notification centre. */
  group?: string;
  /** Being rung: bypasses "don't disturb me, I'm looking at the app". */
  urgent?: boolean;
  /** Post without a sound (the caller already played one). */
  silent?: boolean;
}

export interface Notifier {
  notify(title: string, body: string, options?: NotifyOptions): Promise<void>;
}

export interface BadgeSetter {
  setBadge(count: number): Promise<void>;
  clearBadge(): Promise<void>;
}

export interface DeepLinks {
  onOpen(handler: (url: string) => void): () => void;
}

export interface PlatformAdapter {
  secureStorage: SecureStore;
  notifications: Notifier;
  badge: BadgeSetter;
  deepLink: DeepLinks;
}

class IndexedDbSecureStore implements SecureStore {
  private db(): Promise<IDBDatabase> {
    return new Promise((resolve, reject) => {
      const req = indexedDB.open("slackwsh-secure", 1);
      req.onupgradeneeded = () => req.result.createObjectStore("kv");
      req.onsuccess = () => resolve(req.result);
      req.onerror = () => reject(req.error);
    });
  }

  async get(key: string): Promise<string | null> {
    const db = await this.db();
    return new Promise((resolve, reject) => {
      const tx = db.transaction("kv", "readonly");
      const req = tx.objectStore("kv").get(key);
      req.onsuccess = () => resolve((req.result as string | undefined) ?? null);
      req.onerror = () => reject(req.error);
    });
  }

  async set(key: string, value: string): Promise<void> {
    const db = await this.db();
    return new Promise((resolve, reject) => {
      const tx = db.transaction("kv", "readwrite");
      tx.objectStore("kv").put(value, key);
      // Wait for the transaction to commit — resolving on the request's
      // onsuccess alone can race a hard navigation/refresh and drop the write.
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
      tx.onabort = () => reject(tx.error ?? new Error("idb transaction aborted"));
    });
  }

  async remove(key: string): Promise<void> {
    const db = await this.db();
    return new Promise((resolve, reject) => {
      const tx = db.transaction("kv", "readwrite");
      tx.objectStore("kv").delete(key);
      tx.oncomplete = () => resolve();
      tx.onerror = () => reject(tx.error);
      tx.onabort = () => reject(tx.error ?? new Error("idb transaction aborted"));
    });
  }
}

class WebNotifier implements Notifier {
  async notify(title: string, body: string, options?: NotifyOptions): Promise<void> {
    if (typeof Notification === "undefined") return;
    if (Notification.permission === "default") await Notification.requestPermission();
    if (Notification.permission !== "granted") return;
    const n = new Notification(title, {
      body,
      tag: options?.tag,
      silent: options?.silent,
    } as NotificationOptions);
    const route = options?.route;
    if (route) {
      n.onclick = () => {
        window.focus();
        // Routed through the same window event as the desktop path, so both
        // navigate client-side and the behaviour can't diverge.
        requestRoute(route);
      };
    }
  }
}

class WebBadgeSetter implements BadgeSetter {
  async setBadge(count: number): Promise<void> {
    const nav = navigator as Navigator & { setAppBadge?: (n: number) => Promise<void> };
    await nav.setAppBadge?.(count);
  }
  async clearBadge(): Promise<void> {
    const nav = navigator as Navigator & { clearAppBadge?: () => Promise<void> };
    await nav.clearAppBadge?.();
  }
}

class HistoryDeepLinks implements DeepLinks {
  onOpen(handler: (url: string) => void): () => void {
    const listener = () => handler(window.location.href);
    window.addEventListener("popstate", listener);
    return () => window.removeEventListener("popstate", listener);
  }
}

export function createWebPlatformAdapter(): PlatformAdapter {
  return {
    secureStorage: new IndexedDbSecureStore(),
    notifications: new WebNotifier(),
    badge: new WebBadgeSetter(),
    deepLink: new HistoryDeepLinks(),
  };
}

// ---- Tauri (Windows, macOS — ADR-009) --------------------------------

/** Feature 11.9: the OS keychain via the Rust `keyring` crate, wrapped as
 * tauri::command functions (apps/desktop/src-tauri/src/lib.rs) — not
 * IndexedDB, which is what the web/PWA build uses for the same interface. */
class KeychainSecureStore implements SecureStore {
  async get(key: string): Promise<string | null> {
    const { invoke } = await import("@tauri-apps/api/core");
    const value = await invoke<string | null>("keychain_get", { account: key });
    return value ?? null;
  }
  async set(key: string, value: string): Promise<void> {
    const { invoke } = await import("@tauri-apps/api/core");
    await invoke("keychain_set", { account: key, value });
  }
  async remove(key: string): Promise<void> {
    const { invoke } = await import("@tauri-apps/api/core");
    await invoke("keychain_delete", { account: key });
  }
}

/** Feature 7.2: native OS notifications with click-to-deep-link, via
 * tauri-plugin-notification rather than the Web Notification API (which
 * also technically works inside the webview, but the plugin integrates
 * with the OS notification center properly — action buttons, persistence). */
/** Event name the Rust side emits when a toast is clicked (see notify_toast in
 * apps/desktop/src-tauri/src/lib.rs). */
const NOTIFICATION_ACTIVATED_EVENT = "slackwsh://notification-activated";

/** Window event a component listens for to navigate client-side. A hard
 * `location.assign` cannot be used inside the shell: the frontend is a static
 * export, so `/channel` would have to resolve to `channel.html`, which Tauri's
 * asset protocol does not do. */
export const NOTIFICATION_ROUTE_EVENT = "slackwsh:notification-route";

function requestRoute(route: string | null | undefined) {
  if (!route) return;
  window.dispatchEvent(new CustomEvent(NOTIFICATION_ROUTE_EVENT, { detail: { route } }));
}

class TauriNotifier implements Notifier {
  /** Bound once per process, not per notification — a listener per toast would
   * leak one for every message that ever arrives. */
  private static activationBound = false;
  private static permissionPrimed = false;

  /** Ask the OS once early so the first real alert isn't silently dropped. */
  static async ensurePermission(): Promise<void> {
    if (TauriNotifier.permissionPrimed) return;
    TauriNotifier.permissionPrimed = true;
    try {
      const { isPermissionGranted, requestPermission } = await import("@tauri-apps/plugin-notification");
      let granted = await isPermissionGranted();
      if (!granted) granted = (await requestPermission()) === "granted";
      if (granted) return;
    } catch {
      // Fall through to the web Notification API (works in some webviews).
    }
    if (typeof Notification !== "undefined" && Notification.permission === "default") {
      await Notification.requestPermission().catch(() => "denied");
    }
  }

  private static async bindActivation() {
    if (TauriNotifier.activationBound) return;
    TauriNotifier.activationBound = true;
    try {
      const { listen } = await import("@tauri-apps/api/event");
      await listen<string | null>(NOTIFICATION_ACTIVATED_EVENT, (event) => {
        requestRoute(event.payload);
      });
    } catch {
      // Without activation the toast still posts; the tray icon remains the
      // way back into the window.
    }
  }

  async notify(title: string, body: string, options?: NotifyOptions): Promise<void> {
    void TauriNotifier.bindActivation();
    await TauriNotifier.ensurePermission();

    // Windows first: our own command is the only path that produces a toast
    // attributed to this app *and* delivers clicks. tauri-plugin-notification
    // does neither on Windows — it withholds the AppUserModelID for any build
    // running out of target/ (so toasts read "Windows PowerShell") and calls
    // show() without an activation handler.
    try {
      const { invoke } = await import("@tauri-apps/api/core");
      await invoke("notify_toast", { title, body, route: options?.route ?? null });
      return;
    } catch {
      // Not Windows, or the command is missing from an older shell binary.
    }

    try {
      const { isPermissionGranted, requestPermission, sendNotification } = await import(
        "@tauri-apps/plugin-notification"
      );
      let granted = await isPermissionGranted();
      if (!granted) granted = (await requestPermission()) === "granted";
      if (granted) {
        sendNotification({
          title,
          body,
          group: options?.group,
          extra: options?.route ? { route: options.route } : undefined,
        });
        return;
      }
    } catch {
      // Plugin unavailable — try the webview Notification API below.
    }

    // Last resort (macOS / older shells): Web Notification API inside the webview.
    if (typeof Notification === "undefined") return;
    if (Notification.permission === "default") await Notification.requestPermission();
    if (Notification.permission !== "granted") return;
    const n = new Notification(title, {
      body,
      tag: options?.tag,
      silent: options?.silent,
    } as NotificationOptions);
    const route = options?.route;
    if (route) {
      n.onclick = () => {
        window.focus();
        requestRoute(route);
      };
    }
  }
}

/**
 * Feature 11.2 — the unread count on the OS surfaces that carry one: the tray
 * tooltip everywhere, a Windows taskbar overlay, a macOS dock badge. The work
 * happens in Rust (`set_unread` in apps/desktop/src-tauri/src/lib.rs) because
 * overlay icons and dock badges are window APIs with no JS binding.
 *
 * The window-title prefix this used to do on its own is kept as the fallback
 * for when the command isn't there — an older shell binary paired with a newer
 * web bundle, which one build for web/PWA/desktop (ADR-005) makes possible.
 */
class TauriTrayBadgeSetter implements BadgeSetter {
  private baseTitle = "connectHUB";

  async setBadge(count: number): Promise<void> {
    try {
      const { invoke } = await import("@tauri-apps/api/core");
      await invoke("set_unread", { count: Math.max(0, Math.trunc(count)) });
    } catch {
      const { getCurrentWindow } = await import("@tauri-apps/api/window");
      await getCurrentWindow()
        .setTitle(count > 0 ? `(${count}) ${this.baseTitle}` : this.baseTitle)
        .catch(() => undefined);
    }
  }
  async clearBadge(): Promise<void> {
    await this.setBadge(0);
  }
}

/** Feature 11.4: `slackwsh://` deep links routed to a channel/message,
 * via tauri-plugin-deep-link — registered as a URL scheme in
 * tauri.conf.json's `plugins.deep-link.desktop.schemes`. */
class TauriDeepLinks implements DeepLinks {
  onOpen(handler: (url: string) => void): () => void {
    let unlisten: (() => void) | undefined;
    let cancelled = false;

    import("@tauri-apps/plugin-deep-link").then(async ({ onOpenUrl, getCurrent }) => {
      if (cancelled) return;
      const current = await getCurrent();
      if (current?.[0]) handler(current[0]);
      unlisten = await onOpenUrl((urls) => {
        if (urls[0]) handler(urls[0]);
      });
    });

    return () => {
      cancelled = true;
      unlisten?.();
    };
  }
}

export function createTauriPlatformAdapter(): PlatformAdapter {
  return {
    secureStorage: new KeychainSecureStore(),
    notifications: new TauriNotifier(),
    badge: new TauriTrayBadgeSetter(),
    deepLink: new TauriDeepLinks(),
  };
}

/** The actual composition-root decision point (§4.9's "one interface,
 * three implementations"): detect the Tauri webview at runtime rather than
 * building a separate bundle, since ADR-005/ADR-009 both commit to one
 * build for web, PWA, and desktop. */
export function createPlatformAdapter(): PlatformAdapter {
  const isTauri = typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
  return isTauri ? createTauriPlatformAdapter() : createWebPlatformAdapter();
}

/** Prime OS notification permission in the desktop shell (Windows + macOS).
 * No-op on the plain web build — browsers only allow prompting from a gesture. */
export async function ensureDesktopNotificationPermission(): Promise<void> {
  if (typeof window === "undefined" || !("__TAURI_INTERNALS__" in window)) return;
  await TauriNotifier.ensurePermission();
}

/** Phase 3 alpha — check the updater endpoint. Failures are silent: unsigned
 * local builds and a missing releases bucket must not block the app. */
export async function checkDesktopUpdates(): Promise<void> {
  if (typeof window === "undefined" || !("__TAURI_INTERNALS__" in window)) return;
  try {
    const { check } = await import("@tauri-apps/plugin-updater");
    const update = await check();
    if (!update) return;
    await update.downloadAndInstall();
    const { relaunch } = await import("@tauri-apps/plugin-process");
    await relaunch();
  } catch {
    // No artefact, bad pubkey, or unsigned build.
  }
}
