<cfsetting showdebugoutput="false" requesttimeout="60">
<cfsetting enablecfoutputonly="true">
<cfcontent type="application/json; charset=utf-8" reset="true">

<cfscript>
// Process all due scheduled tasks
// Called by cron every minute (or manually for testing)
// Creates Tasks entries for any ScheduledTaskDefinitions that are due
// Public endpoint - no auth required (should be restricted by IP in production)

function apiAbort(required struct payload) {
    writeOutput(serializeJSON(payload));
    abort;
}

// Calculate next run time from cron expression
function calculateNextRun(required string cronExpression) {
    var parts = listToArray(cronExpression, " ");
    if (arrayLen(parts) != 5) {
        return dateAdd("d", 1, now());
    }

    var cronMinute = parts[1];
    var cronHour = parts[2];
    var cronDay = parts[3];
    var cronMonth = parts[4];
    var cronWeekday = parts[5];

    // Start from current time + 1 minute
    var checkDate = dateAdd("n", 1, now());
    checkDate = createDateTime(year(checkDate), month(checkDate), day(checkDate), hour(checkDate), minute(checkDate), 0);

    var maxIterations = 400 * 24 * 60; // 400 days in minutes
    var iterations = 0;

    while (iterations < maxIterations) {
        var matchMinute = (cronMinute == "*" || (isNumeric(cronMinute) && minute(checkDate) == int(cronMinute)));
        var matchHour = (cronHour == "*" || (isNumeric(cronHour) && hour(checkDate) == int(cronHour)));
        var matchDay = (cronDay == "*" || (isNumeric(cronDay) && day(checkDate) == int(cronDay)));
        var matchMonth = (cronMonth == "*" || (isNumeric(cronMonth) && month(checkDate) == int(cronMonth)));

        var dow = dayOfWeek(checkDate) - 1; // Convert to 0-based (0=Sunday)
        var matchWeekday = (cronWeekday == "*");
        if (!matchWeekday) {
            if (find("-", cronWeekday)) {
                var range = listToArray(cronWeekday, "-");
                if (arrayLen(range) == 2 && isNumeric(range[1]) && isNumeric(range[2])) {
                    matchWeekday = (dow >= int(range[1]) && dow <= int(range[2]));
                }
            } else if (isNumeric(cronWeekday)) {
                matchWeekday = (dow == int(cronWeekday));
            }
        }

        if (matchMinute && matchHour && matchDay && matchMonth && matchWeekday) {
            return checkDate;
        }

        checkDate = dateAdd("n", 1, checkDate);
        iterations++;
    }

    return dateAdd("d", 1, now());
}

try {
    // Find all active scheduled tasks that are due
    dueTasks = queryExecute("
        SELECT
            ID AS ScheduledTaskID,
            BusinessID as BusinessID,
            TaskCategoryID as CategoryID,
            Title as Title,
            Details as Details,
            CronExpression as CronExpression,
            COALESCE(ScheduleType, 'cron') as ScheduleType,
            IntervalMinutes as IntervalMinutes
        FROM ScheduledTaskDefinitions
        WHERE IsActive = 1
          AND NextRunOn <= NOW()
    ", {}, { datasource: "payfrit" });

    createdTasks = [];

    for (task in dueTasks) {
        // Create the actual task (ClaimedByUserID=0 means unclaimed/pending)
        queryExecute("
            INSERT INTO Tasks (
                BusinessID, CategoryID, TaskTypeID,
                Title, Details, CreatedOn, ClaimedByUserID
            ) VALUES (
                :businessID, :categoryID, :typeID,
                :title, :details, NOW(), 0
            )
        ", {
            businessID: { value: task.BusinessID, cfsqltype: "cf_sql_integer" },
            categoryID: { value: task.CategoryID, cfsqltype: "cf_sql_integer", null: isNull(task.CategoryID) },
            typeID: { value: 0, cfsqltype: "cf_sql_integer" },
            title: { value: task.Title, cfsqltype: "cf_sql_varchar" },
            details: { value: task.Details, cfsqltype: "cf_sql_longvarchar", null: isNull(task.Details) }
        }, { datasource: "payfrit" });

        qNew = queryExecute("SELECT LAST_INSERT_ID() as newID", [], { datasource: "payfrit" });

        // Calculate next run based on schedule type
        if (task.ScheduleType == "interval_after_completion" && !isNull(task.IntervalMinutes) && task.IntervalMinutes > 0) {
            // After-completion interval: don't schedule next run until task is completed
            // Set to far future (effectively paused until task completion triggers recalculation)
            nextRun = javaCast("null", "");
        } else if (task.ScheduleType == "interval" && !isNull(task.IntervalMinutes) && task.IntervalMinutes > 0) {
            // Fixed interval: next run = NOW + interval minutes
            nextRun = dateAdd("n", task.IntervalMinutes, now());
        } else {
            // Cron-based: use cron parser
            nextRun = calculateNextRun(task.CronExpression);
        }

        queryExecute("
            UPDATE ScheduledTaskDefinitions SET
                LastRunOn = NOW(),
                NextRunOn = :nextRun
            WHERE ID = :id
        ", {
            nextRun: { value: nextRun, cfsqltype: "cf_sql_timestamp", null: isNull(nextRun) },
            id: { value: task.ID, cfsqltype: "cf_sql_integer" }
        }, { datasource: "payfrit" });

        arrayAppend(createdTasks, {
            "ScheduledTaskID": task.ID,
            "TaskID": qNew.newID,
            "BusinessID": task.BusinessID,
            "Title": task.Title
        });
    }

    apiAbort({
        "OK": true,
        "MESSAGE": "Processed #arrayLen(createdTasks)# scheduled task(s)",
        "CREATED_TASKS": createdTasks,
        "CHECKED_COUNT": dueTasks.recordCount,
        "RAN_AT": dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss")
    });

} catch (any e) {
    apiAbort({
        "OK": false,
        "ERROR": "server_error",
        "MESSAGE": e.message
    });
}
</cfscript>
