export interface TTLCache<K, V> {
    get(key: K): V | undefined;
    has(key: K): boolean;
    set(key: K, value: V, options?: { at?: number }): V;
    delete(key: K): boolean;
    clear(): void;
    prune(): number;
    keys(): K[];
    entries(): [K, V][];
    readonly size: number;
}

interface TTLCacheOptions {
    ttlMs?: number | (() => number);
    maxSize?: number;
    now?: () => number;
}

export function createTTLCache<K = unknown, V = unknown>({
    ttlMs = 0,
    maxSize = 100,
    now = () => Date.now(),
}: TTLCacheOptions = {}): TTLCache<K, V> {
    const map = new Map<K, { value: V; at: number }>();

    function _resolveNow(): number {
        try {
            const value = Number(now());
            return Number.isFinite(value) ? value : Date.now();
        } catch {
            return Date.now();
        }
    }

    function _resolveTTL(): number {
        try {
            const value = typeof ttlMs === "function" ? Number(ttlMs()) : Number(ttlMs);
            return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
        } catch {
            return 0;
        }
    }

    function _resolveMaxSize(): number {
        try {
            const value = Number(maxSize);
            return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
        } catch {
            return 1;
        }
    }

    function _isExpired(entry: { value: V; at: number } | undefined, nowValue: number, ttlValue: number): boolean {
        if (!entry) return true;
        if (!(ttlValue > 0)) return false;
        return nowValue - Number(entry.at || 0) > ttlValue;
    }

    function _pruneExpired(nowValue = _resolveNow(), ttlValue = _resolveTTL()): void {
        if (!(ttlValue > 0)) return;
        for (const [key, entry] of map.entries()) {
            if (_isExpired(entry, nowValue, ttlValue)) {
                map.delete(key);
            }
        }
    }

    function _evictOverflow(): void {
        const limit = _resolveMaxSize();
        while (map.size > limit) {
            const oldestKey = map.keys().next().value;
            if (oldestKey === undefined) break;
            map.delete(oldestKey);
        }
    }

    function _touch(key: K, entry: { value: V; at: number }): void {
        map.delete(key);
        map.set(key, entry);
    }

    return {
        get(key) {
            const nowValue = _resolveNow();
            const ttlValue = _resolveTTL();
            _pruneExpired(nowValue, ttlValue);
            const entry = map.get(key);
            if (!entry) return undefined;
            if (_isExpired(entry, nowValue, ttlValue)) {
                map.delete(key);
                return undefined;
            }
            return entry.value;
        },

        has(key) {
            return this.get(key) !== undefined;
        },

        set(key, value, options = {}) {
            const entryAtRaw = Number(options?.at);
            const entryAt = Number.isFinite(entryAtRaw) ? entryAtRaw : _resolveNow();
            const entry = { value, at: entryAt };
            if (map.has(key)) {
                _touch(key, entry);
            } else {
                map.set(key, entry);
            }
            _pruneExpired(_resolveNow(), _resolveTTL());
            _evictOverflow();
            return value;
        },

        delete(key) {
            return map.delete(key);
        },

        clear() {
            map.clear();
        },

        prune() {
            _pruneExpired(_resolveNow(), _resolveTTL());
            _evictOverflow();
            return map.size;
        },

        keys() {
            this.prune();
            return Array.from(map.keys());
        },

        entries() {
            this.prune();
            return Array.from(map.entries()).map(([key, entry]) => [key, entry.value]);
        },

        get size() {
            this.prune();
            return map.size;
        },
    };
}
