/**
 * Reusable data cache with fetch deduplication, invalidation, and subscribers.
 */

import { fetchWithAuth } from './api.ts';

export type ExtractDataFn<T> = (json: any) => T;
export type SubscriberFn<T> = (data: T) => void;

export interface DataCacheOpts<T> {
    endpoint: string;
    extractData: ExtractDataFn<T>;
    defaultValue?: T;
}

export class DataCache<T = any> {
    private _endpoint: string;
    private _extractData: ExtractDataFn<T>;
    private _defaultValue: T;
    private _data: T;
    private _loading: boolean;
    private _promise: Promise<T> | null;
    private _subscribers: SubscriberFn<T>[];
    private _fresh: boolean;

    /**
     * @param opts.endpoint - API path (e.g. '/picture-sources')
     * @param opts.extractData - Extract array from response JSON
     * @param opts.defaultValue - Initial/fallback value (default: [])
     */
    constructor({ endpoint, extractData, defaultValue = [] as unknown as T }: DataCacheOpts<T>) {
        this._endpoint = endpoint;
        this._extractData = extractData;
        this._defaultValue = defaultValue;
        this._data = defaultValue;
        this._loading = false;
        this._promise = null;
        this._subscribers = [];
        this._fresh = false;  // true after first successful fetch, cleared on invalidate
    }

    get data(): T { return this._data; }
    get loading(): boolean { return this._loading; }

    /**
     * Fetch from API. Deduplicates concurrent calls.
     * Returns cached data immediately if already fetched and not invalidated.
     */
    async fetch({ force = false } = {}): Promise<T> {
        if (!force && this._fresh) return this._data;
        if (this._promise) return this._promise;
        this._loading = true;
        this._promise = this._doFetch();
        try {
            return await this._promise;
        } finally {
            this._promise = null;
            this._loading = false;
        }
    }

    async _doFetch(): Promise<T> {
        try {
            const resp = await fetchWithAuth(this._endpoint);
            if (!resp.ok) {
                console.error(`[DataCache] ${this._endpoint}: HTTP ${resp.status}`);
                return this._data;
            }
            const json = await resp.json();
            this._data = this._extractData(json);
            this._fresh = true;
            this._notify();
            return this._data;
        } catch (err: any) {
            if (err.isAuth) return this._data;
            console.error(`Cache fetch ${this._endpoint}:`, err);
            return this._data;
        }
    }

    /** Mark cache as stale; next fetch() will re-request. */
    invalidate(): void {
        this._fresh = false;
    }

    /** Manually set cache value (e.g. after a create/update call). */
    update(value: T): void {
        this._data = value;
        this._fresh = true;
        this._notify();
    }

    subscribe(fn: SubscriberFn<T>): void { this._subscribers.push(fn); }
    unsubscribe(fn: SubscriberFn<T>): void { this._subscribers = this._subscribers.filter(f => f !== fn); }

    _notify(): void {
        for (const fn of this._subscribers) fn(this._data);
    }
}
