// `SessionStore` is the single source of truth for the device
// session state across every authenticated UI surface. It owns the
// lifecycle of the WebCrypto keypair (loaded or generated through
// the `KeyStore`), the persisted `device_session_id` (read/written
// through the `Cache`), and the high-level status that the root
// layout uses to gate routing.
//
// The store runs in two stages: `init()` loads the persisted
// keypair and session id, sanity-checks WebCrypto Ed25519 support,
// and settles `status` into one of `unsupported`, `anonymous`, or
// `authenticated`. Callers (the login form, the lobby) drive the
// rest through `signIn` and `signOut`.
//
// `signOut("revoked")` is a separate code path because gateway-side
// session revocation closes the SubscribeEvents stream
// asynchronously; the watcher in `lib/revocation-watcher.ts` calls
// it without user interaction. The post-condition is the same as
// `signOut("user")` — keypair regenerated, session id wiped,
// status returned to `anonymous` — so the layout's existing
// `anonymous → /login` redirect handles both reasons uniformly.

import type {
	Cache,
	DeviceKeypair,
	KeyStore,
	StoreLoader,
} from "../platform/store/index";
import { loadStore } from "../platform/store/index";
import {
	clearDeviceSession,
	loadDeviceSession,
	setDeviceSessionId,
} from "../api/session";

export type SessionStatus =
	| "loading"
	| "unsupported"
	| "anonymous"
	| "authenticated";

export class SessionStore {
	status: SessionStatus = $state("loading");
	keypair: DeviceKeypair | null = $state(null);
	deviceSessionId: string | null = $state(null);

	private initPromise: Promise<void> | null = null;
	private keyStore: KeyStore | null = null;
	private cache: Cache | null = null;
	private supportProbe: () => Promise<boolean> = defaultSupportProbe;
	private storeLoader: StoreLoader = loadStore;

	/**
	 * init loads the persisted keypair and device-session id, runs a
	 * one-time WebCrypto Ed25519 sanity check, and settles `status`.
	 * Calling it multiple times is safe — the first call drives the
	 * actual work; subsequent calls await the same promise.
	 */
	init(): Promise<void> {
		if (this.initPromise === null) {
			this.initPromise = this.doInit();
		}
		return this.initPromise;
	}

	/**
	 * signIn persists the device-session id returned by the
	 * confirm-email-code response and flips the status to
	 * `authenticated`. The keypair already lives in the store from
	 * `init()`.
	 */
	async signIn(deviceSessionId: string): Promise<void> {
		if (this.cache === null) {
			throw new Error("session store: signIn called before init");
		}
		await setDeviceSessionId(this.cache, deviceSessionId);
		this.deviceSessionId = deviceSessionId;
		this.status = "authenticated";
	}

	/**
	 * signOut wipes the keypair and the persisted device-session id,
	 * generates a fresh keypair so the next login does not reuse the
	 * revoked public key, and returns the status to `anonymous`. The
	 * `reason` is recorded in console output for telemetry but does
	 * not change the post-state — both user-driven logout and
	 * gateway-driven revocation land the user back on `/login`.
	 */
	async signOut(reason: "user" | "revoked"): Promise<void> {
		if (this.keyStore === null || this.cache === null) {
			throw new Error("session store: signOut called before init");
		}
		await clearDeviceSession(this.keyStore, this.cache);
		const fresh = await loadDeviceSession(this.keyStore, this.cache);
		this.keypair = fresh.keypair;
		this.deviceSessionId = null;
		this.status = "anonymous";
		if (reason === "revoked") {
			console.info("session store: device session revoked by gateway");
		}
	}

	/**
	 * setSupportProbeForTests overrides the WebCrypto Ed25519 probe.
	 * Production code calls the real `crypto.subtle.generateKey`; tests
	 * can swap in a deterministic stub.
	 */
	setSupportProbeForTests(probe: () => Promise<boolean>): void {
		this.supportProbe = probe;
	}

	/**
	 * setStoreLoaderForTests overrides the storage adapter resolver.
	 * Production code calls `loadStore()` from `platform/store`; tests
	 * inject a per-test `KeyStore` + `Cache` pair backed by a unique
	 * IndexedDB name so cases stay independent.
	 */
	setStoreLoaderForTests(loader: StoreLoader): void {
		this.storeLoader = loader;
	}

	/**
	 * resetForTests forgets all persisted state on the instance so a
	 * subsequent `init()` runs from scratch. Production code never
	 * calls this; it exists only for the Vitest harness.
	 */
	resetForTests(): void {
		this.status = "loading";
		this.keypair = null;
		this.deviceSessionId = null;
		this.initPromise = null;
		this.keyStore = null;
		this.cache = null;
		this.supportProbe = defaultSupportProbe;
		this.storeLoader = loadStore;
	}

	private async doInit(): Promise<void> {
		const supported = await this.supportProbe();
		if (!supported) {
			this.status = "unsupported";
			return;
		}
		const { keyStore, cache } = await this.storeLoader();
		this.keyStore = keyStore;
		this.cache = cache;
		const loaded = await loadDeviceSession(keyStore, cache);
		this.keypair = loaded.keypair;
		this.deviceSessionId = loaded.deviceSessionId;
		this.status =
			loaded.deviceSessionId === null ? "anonymous" : "authenticated";
	}
}

async function defaultSupportProbe(): Promise<boolean> {
	if (
		typeof globalThis.crypto !== "object" ||
		typeof globalThis.crypto.subtle !== "object"
	) {
		return false;
	}
	try {
		await globalThis.crypto.subtle.generateKey(
			{ name: "Ed25519" } as KeyAlgorithm,
			false,
			["sign", "verify"],
		);
		return true;
	} catch {
		return false;
	}
}

export const session = new SessionStore();
