//

import { assert } from "../assert.js";
import { returnZero } from "../function/returnZero.js";
import { current_time_in_seconds } from "../time/current_time_in_seconds.js";
import { Cache } from "./Cache.js";

/**
 * @template R
 */
class Record {
    /**
     * @template R
     * @param {R} value
     * @param {number} time
     */
    constructor(value, time) {
        this.value = value;
        this.time = time;
        this.failed = false;
        this.weight = 0;
    }
}

/**
 * In seconds
 * @readonly
 * @type {number}
 */
const DEFAULT_TIME_TO_LIVE = Infinity;

/**
 * @template T
 * @param {Record<T>} record
 * @returns {number}
 */
function record_get_value_weight(record) {
    return record.weight;
}

/**
 * Asynchronous cache capable of resolving its own values by keys
 * Modelled on Guava's LoadingCache concept
 * @template K
 * @template V
 * @class
 *
 * @author Alex Goldring
 * @copyright Company Named Limited (c) 2026
 */
export class LoadingCache {
    /**
     * @type {Cache<K,Record<Promise<V>>>}
     */
    #internal
    /**
     * Time until records are marked as invalid, invalid records are not served back and trigger re-loading of the value
     * In seconds
     * @type {number}
     */
    #timeToLive = DEFAULT_TIME_TO_LIVE;

    /**
     * @type {function(key:K):Promise<V>}
     */
    #load

    /**
     *
     * @type {boolean}
     */
    #policyRetryFailed = true;
    /**
     *
     * @type {function(V): number}
     */
    #value_weigher;

    /**
     * @see {@link Cache} for more details on what each parameter means
     * @param [maxWeight]
     * @param [keyWeigher]
     * @param [valueWeigher]
     * @param [keyHashFunction]
     * @param [keyEqualityFunction]
     * @param [capacity]
     * @param {number} [timeToLive] in seconds, default is 10 seconds
     * @param {function(key:K):Promise<V>} load
     * @param {boolean} [retryFailed]
     */
    constructor({
                    maxWeight,
                    keyWeigher,
                    valueWeigher = returnZero,
                    keyHashFunction,
                    keyEqualityFunction,
                    capacity,
                    timeToLive = DEFAULT_TIME_TO_LIVE,
                    load,
                    retryFailed = true
                }) {

        assert.isFunction(load, 'load');

        this.#internal = new Cache({
            maxWeight,
            keyWeigher,
            valueWeigher: record_get_value_weight,
            keyHashFunction,
            keyEqualityFunction,
            capacity,
        });

        this.#timeToLive = timeToLive;
        this.#load = load;
        this.#policyRetryFailed = retryFailed;
        this.#value_weigher = valueWeigher;
    }

    /**
     *
     * @param {K} key
     * @returns {boolean}
     */
    invalidate(key) {
        return this.#internal.remove(key);
    }

    /**
     *
     */
    clear() {
        this.#internal.clear();
    }

    /**
     *
     * @param {K} key
     * @param {number} timestamp
     * @return {Record<Promise<V>>}
     */
    #load_value(key, timestamp) {

        let promise;

        try {
            promise = this.#load(key);
        } catch (e) {
            promise = Promise.reject(e);
        }

        const record = new Record(promise, timestamp);

        this.#internal.put(key, record);

        promise.then(
            (value) => {
                // re-score value based on actual data
                record.weight = this.#value_weigher(value);
                this.#internal.updateElementWeight(key);
            },
            () => {
                // mark as failure
                record.failed = true;
            }
        );

        return record;
    }

    /**
     * Load new value for the key (happens asynchronously)
     * @param {K} key
     * @returns {Promise<V>}
     */
    refresh(key) {
        const record = this.#load_value(key, current_time_in_seconds());

        return record.value;
    }

    /**
     * Directly insert value into the cache
     * @param {K} key
     * @param {V} value
     */
    put(key, value) {
        this.#internal.put(key, new Record(Promise.resolve(value), current_time_in_seconds()));
    }

    /**
     *
     * @param {K} key
     * @returns {boolean}
     */
    contains(key) {
        return this.#internal.contains(key);
    }

    /**
     *
     * @param {K} key
     * @return {Promise<V>}
     */
    async get(key) {
        const currentTime = current_time_in_seconds();

        /**
         *
         * @type {Record<Promise<V>>}
         */
        let record = this.#internal.get(key);

        if (record === null
            || (record.time + this.#timeToLive) < currentTime  // timeout
            || (record.failed && this.#policyRetryFailed) // load failed and we're configured to retry
        ) {

            // record needs to be loaded
            record = this.#load_value(key, currentTime);

        }


        return record.value;
    }
}
