var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _Scheduler_instances, _Scheduler_actions, _Scheduler_dispatcher, _Scheduler_calculateInterval, _Scheduler_setAction, _Scheduler_runScheduledAction, _Scheduler_invokeActorWithAction, _Scheduler_cancelAction;
import { Empty, GetActionResponse, ScheduledAction, ScheduledActionResponse, SchedulerHandlerDefinition, Strings, } from '@devvit/protos';
import { Header } from '@devvit/shared-types/Header.js';
import { StringUtil } from '@devvit/shared-types/StringUtil.js';
import parser from 'cron-parser';
import { v4 as UUIDv4 } from 'uuid';
import { EnvelopeClientBuilder } from '../common/envelope/EnvelopeClientBuilder.js';
import { Log } from '../lib/loggers/Log.js';
/**
 * The scheduler schedules future actions and invokes them
 */
export class Scheduler {
    constructor(dispatcher) {
        _Scheduler_instances.add(this);
        _Scheduler_actions.set(this, void 0);
        _Scheduler_dispatcher.set(this, void 0);
        __classPrivateFieldSet(this, _Scheduler_actions, {}, "f");
        __classPrivateFieldSet(this, _Scheduler_dispatcher, dispatcher, "f");
    }
    async List(request) {
        const actions = Object.entries(__classPrivateFieldGet(this, _Scheduler_actions, "f"))
            .map(([id, action]) => GetActionResponse.fromPartial({
            id,
            request: action.request,
        }))
            .filter((action) => {
            let date = action.request?.when;
            if (!date) {
                const tab = __classPrivateFieldGet(this, _Scheduler_actions, "f")[action.id].crontab;
                if (tab) {
                    date = parser.parseExpression(tab).next().toDate();
                }
            }
            if (!date) {
                return false;
            }
            return ((!request.after || date >= request.after) && (!request.before || date <= request.before));
        });
        return { actions };
    }
    // RPC
    // TODO: support time-zones - by default it assumes the timezone of the worker
    /**
     * Schedule an action with a future Timestamp or Crontab string
     */
    async Schedule(request, metadata) {
        if (!request.cron && request.when == null) {
            throw new Error('Must include timestamp or crontab with schedule request');
        }
        const actorHostname = getActorFromMetadata(metadata);
        if (!actorHostname) {
            throw new Error('Cannot determine actor to schedule action for');
        }
        const actionMetadata = constructActionRequestMetadata(metadata);
        const id = __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_setAction).call(this, request, actorHostname, actionMetadata);
        return ScheduledActionResponse.fromPartial({ id });
    }
    /**
     * Cancel a scheduled action with a given action id.
     * Caller must have the same hostname as the initiator.
     */
    async Cancel(request, metadata) {
        if (!request.id) {
            throw new Error('Must include id to cancel scheduled action');
        }
        const action = __classPrivateFieldGet(this, _Scheduler_actions, "f")[request.id];
        if (action == null) {
            return Empty.fromPartial({}); // no-op
        }
        // Check envelope src. Can't cancel other's actions
        if (action.actorHostname !== getActorFromMetadata(metadata)) {
            throw new Error('Unable to cancel action.');
        }
        __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_cancelAction).call(this, request.id, action);
        return Empty.fromPartial({});
    }
}
_Scheduler_actions = new WeakMap(), _Scheduler_dispatcher = new WeakMap(), _Scheduler_instances = new WeakSet(), _Scheduler_calculateInterval = function _Scheduler_calculateInterval(when) {
    let toScheduleDate;
    if (typeof when === 'object') {
        toScheduleDate = when;
    }
    else {
        try {
            const cron = parser.parseExpression(when);
            toScheduleDate = cron.next();
        }
        catch (err) {
            throw new Error('Invalid crontab', { cause: err });
        }
    }
    return Math.max(0, toScheduleDate.getTime() - Date.now());
}, _Scheduler_setAction = function _Scheduler_setAction(request, actorHostname, metadata) {
    const actionId = UUIDv4();
    const interval = __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_calculateInterval).call(this, request.when || request.cron);
    const timeoutRef = globalThis.setTimeout(() => __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_runScheduledAction).call(this, actionId), interval);
    const scheduledAction = {
        actorHostname,
        timeoutRef,
        data: request.action || ScheduledAction.fromPartial({}),
        crontab: request.cron,
        metadata: metadata,
        request,
    };
    __classPrivateFieldGet(this, _Scheduler_actions, "f")[actionId] = scheduledAction;
    return actionId;
}, _Scheduler_runScheduledAction = function _Scheduler_runScheduledAction(actionId) {
    // get & delete action from hash
    const action = __classPrivateFieldGet(this, _Scheduler_actions, "f")[actionId];
    delete __classPrivateFieldGet(this, _Scheduler_actions, "f")[actionId];
    // this approach won't scale
    // bc a failure between this and the last step won't
    // reschedule an action that should be recurring
    if (action.crontab) {
        const nextInterval = __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_calculateInterval).call(this, action.crontab);
        __classPrivateFieldGet(this, _Scheduler_actions, "f")[actionId] = {
            ...action,
            timeoutRef: globalThis.setTimeout(() => __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_runScheduledAction).call(this, actionId), nextInterval),
        };
    }
    // Send the envelope. #invokeActorWithAction handles rejections.
    __classPrivateFieldGet(this, _Scheduler_instances, "m", _Scheduler_invokeActorWithAction).call(this, action.actorHostname, action.data, action.metadata);
}, _Scheduler_invokeActorWithAction = 
/**
 * Calls HandleScheduledAction on the actor that saved the action
 */
async function _Scheduler_invokeActorWithAction(actorHostname, data, metadata) {
    const client = EnvelopeClientBuilder.build(actorHostname, [
        SchedulerHandlerDefinition,
    ]);
    __classPrivateFieldGet(this, _Scheduler_dispatcher, "f").bind(client);
    try {
        await client.HandleScheduledAction(data, metadata);
    }
    catch (err) {
        Log.error(`Error invoking scheduled action: ${StringUtil.caughtToString(err)}`);
    }
    __classPrivateFieldGet(this, _Scheduler_dispatcher, "f").unbind(client);
}, _Scheduler_cancelAction = function _Scheduler_cancelAction(actionId, action) {
    globalThis.clearTimeout(action.timeoutRef);
    delete __classPrivateFieldGet(this, _Scheduler_actions, "f")[actionId];
};
/**
 * Get the actor hostname from metadata
 */
function getActorFromMetadata(metadata) {
    return metadata?.[Header.Caller]?.values[0];
}
/**
 * Creates a metadata object with auth token to be passed to scheduledActionHandler
 */
function constructActionRequestMetadata(metadata) {
    const app = metadata?.[Header.App]?.values[0];
    const authToken = metadata?.[Header.R2Auth]?.values[0];
    const endpoint = metadata?.[Header.R2Host]?.values[0];
    const subreddit = metadata?.[Header.Subreddit]?.values[0];
    const appUser = metadata?.[Header.AppUser]?.values[0];
    const actionMetadata = {};
    if (app) {
        actionMetadata[Header.App] = Strings.fromPartial({ values: [app] });
    }
    if (authToken) {
        actionMetadata[Header.R2Auth] = Strings.fromPartial({ values: [authToken] });
    }
    if (endpoint) {
        actionMetadata[Header.R2Host] = Strings.fromPartial({ values: [endpoint] });
    }
    if (subreddit) {
        actionMetadata[Header.Subreddit] = Strings.fromPartial({ values: [subreddit] });
    }
    if (appUser) {
        actionMetadata[Header.AppUser] = Strings.fromPartial({ values: [appUser] });
    }
    return actionMetadata;
}
