<?php
// prevent direct access

use classes\backup_store;
use classes\economic;
use classes\xlvask;
use classes\slack as Slack;
use classes\email as Email;
use classes\gatewayapi as GatewayAPI;
use goals\classes\goals_criteria;
use goals\services\goals_progress_alert_renderer;
use goals\helpers\goals_criteria_progress_alert_destination as Dest;
use goals\helpers\goals_criteria_progress_alert_frequency as Freq;
use objects\department_goals_o;
use objects\departments_o;
use objects\bookings_o;
use objects\logs_o;
use objects\users_o;

if (!defined('WD')) {
    exit;
}

// Define the warning function
function warn($message): void
{
    echo "\n\033[33m$message\033[0m\n";
}

$response_cron = [];

// Define the cron tasks
$cron_tasks = [
//    'CheckUnfulfilledBookings' => [
//        'interval' => 86400, // 24 hours
//        'last_run' => 0,
//        'next_run' => 0,
//        'time' => '15:00',
//        'function' => 'checkUnfulfilledBookings',
//    ],
//    'SyncBookings' => [
//        'interval' => 60, // 1 minute
//        'last_run' => 0,
//        'next_run' => 0,
//        'function' => 'syncBookings',
//    ],
    'SyncLogs' => [
        'interval' => 300, // 5 minutes
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'syncLogsToDatabase',
    ],
    'SyncUserEconomicCustomerDiscounts' => [
        'interval' => 180, // 3 minutes
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'SyncUserEconomicCustomerDiscounts',
    ],
    'SyncUserEconomicCustomerDetails' => [
        'interval' => 43200, // 12 hours
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'SyncUserEconomicCustomerDetails',
    ],
    'backup' => [
        'interval' => 43200, // 12 hours
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'backup',
    ],
    'SyncEconomicInvoiceStatus' => [
        'interval' => 120, // 2 minutes
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'SyncEconomicInvoiceStatus',
    ],
    'SyncXLVaskModuleCron' => [
        'interval' => 3600, // 1 hour
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'SyncXLVaskModuleCron',
    ],
    'GoalsProgressAlertsCron' => [
        'interval' => 60, // check every minute
        'last_run' => 0,
        'next_run' => 0,
        'function' => 'GoalsProgressAlertsCron',
    ],
];

function checkUnfulfilledBookings(): void
{
    // This is deactivated for now, as it is not wanted.
    // I'm saving this for later, as it is a good idea to have this in place.
    return;
    $bookings_o = new bookings_o();
    $bookings_o->checkUnfulfilledBookings();
}

function syncBookings(): void
{
    $bookings_o = new bookings_o();
    $response_cron[] = $bookings_o->syncBookings();
}

function syncLogsToDatabase(): void
{
    $logs_o = new logs_o();
    $logs_o->syncLogsToDatabase();
}

function backup(): void
{
    try {
        $backup = new backup_store();
        $backup->createBackup();
    } catch (Exception $e) {
        warn('Backup failed: ' . $e->getMessage());
    }
}

function SyncUserEconomicCustomerDiscounts(): void
{
    $users_o = new users_o();
    $users_o->clearAllUsersEconomicCustomerDiscountsFromCache();
}

function SyncUserEconomicCustomerDetails(): void
{
    $users_o = new users_o();
    $users_o->clearAllUsersEconomicCustomerDetailsFromCache();
    //$users_o->syncAllUsersEconomicCustomerDetails();
}

/**
 * @throws Exception
 */
function SyncEconomicInvoiceStatus(): void
{
    $economic = new economic();
    try {
        $economic->getTasks()->runCheckErrors();
    } catch (Exception $e) {
        // Do nothing, this is automatically running
    }
    try {
        $economic->getTasks()->runCheckDrafts();
    } catch (Exception $e) {
        // Do nothing, this is automatically running
    }
}

function SyncXLVaskModuleCron(): void
{
    $xlvask = new xlvask();
    try {
        if ($xlvask->config->enabled->isTrue()) {
            $xlvask->getTasks()->runCronTasks();
        }
    } catch (Exception $e) {
        // Do nothing, this is automatically running
    }
}

/**
 * GoalsProgressAlertsCron
 *
 * Iterates all active department goals and, based on each goal's criteria, sends progress alerts
 * according to frequency, selected weekdays, and an optional time-of-day with timezone.
 *
 * Scheduling rules (defaults when not fully specified):
 * - Frequency NONE: skip.
 * - DAILY: send once per calendar day at the configured time (or at :00 of the current minute if no time provided).
 * - WEEKLY: send once per selected weekday(s) at the configured time (if none selected, Monday).
 * - MONTHLY: send once on the 1st of each month at the configured time (00:00 if not provided).
 * - CHANGED: send when the computed progress value changes since the last check (debounced to 1 hour).
 *
 * Destinations:
 * - SLACK: send to each department's configured webhook; if none, fallback to default Slack webhook.
 * - EMAIL/SMS: currently require explicit recipients in manual route; automatic cron will skip and log a warning.
 */
function GoalsProgressAlertsCron(): void
{
    // List all goals (not deleted)
    $goals = new department_goals_o();
    $rows = $goals->listObjects(function ($row) {
        return (int)$row['id'];
    });

    if (!is_array($rows) || count($rows) === 0) {
        return;
    }

    $nowUtc = new DateTimeImmutable('now', new DateTimeZone('UTC'));

    foreach ($rows as $goalId) {
        try {
            $goal = (new department_goals_o())->select($goalId);
            if (!$goal->exists()) { continue; }
            // Skip soft-deleted
            if (method_exists($goal, 'deleted_at') && !empty((string)$goal->deleted_at->value())) { continue; }

            $criteriaArray = (array)$goal->criteria->value();
            // Build criteria object
            $criteria = goals_criteria::fromJson(json_encode($criteriaArray));
            $criteria->validateAndSanitize();

            $frequency = $criteria->progress_alert_frequency ?? Freq::NONE;
            if ($frequency === Freq::NONE) { continue; }

            // Check if due
            $dueInfo = goalsProgressAlertDue($criteria, $nowUtc);

            // Special handling for CHANGED: only proceed if progress value changed since last send
            if (($frequency === Freq::CHANGED)) {
                $currentProgress = goals_criteria::calculateProgressFromArray($criteriaArray);
                $lastProgressKey = 'goal_alert_last_progress:' . $goalId;
                $lastProgress = redis->get($lastProgressKey) ?? null;
                if ($lastProgress !== null && (string)$lastProgress === (string)$currentProgress) {
                    continue; // no change
                }
                // For CHANGED, dedupe by progress value so each distinct value sends once
                $dueInfo['slot'] = 'changed-' . md5((string)$currentProgress);
                $dueInfo['ttl'] = 86400 * 30; // keep dedupe for a while
            } else {
                if (!$dueInfo['due']) { continue; }
            }

            // Deduplicate per goal per slot via Redis
            $slotKey = $dueInfo['slot'];
            $redisKey = 'goal_alert_sent:' . $goalId . ':' . $slotKey;
            $already = redis->get($redisKey) ?? null;
            if ($already) { continue; }

            // Render message
            $message = goals_progress_alert_renderer::render($criteria);

            // Dispatch according to destination
            $destination = $criteria->progress_alert_destination ?? Dest::SLACK;
            switch ($destination) {
                case Dest::SLACK:
                    $departments = (array)$goal->departments->value();
                    $sentToDept = false;
                    if (count($departments) > 0) {
                        foreach ($departments as $deptId) {
                            if (!is_numeric($deptId)) { continue; }
                            $dept = (new departments_o())->select((int)$deptId);
                            if (!$dept->exists()) { continue; }
                            $webhook = (string)$dept->slack_webhook->value();
                            if (empty($webhook)) { continue; }
                            (new Slack())->send_webhook_message((string)goals_progress_alert_renderer::render($criteria, $dept), $webhook);
                            $sentToDept = true;
                        }
                    }
                    if (!$sentToDept) {
                        // Fallback to default webhook
                        (new Slack())->send_message($message);
                    }
                    break;
                case Dest::EMAIL:
                    // No recipient context in goal for automated cron
                    warn('GoalsProgressAlertsCron: EMAIL destination requires explicit recipients; skipping goal #' . $goalId);
                    break;
                case Dest::SMS:
                    // No recipient context in goal for automated cron
                    warn('GoalsProgressAlertsCron: SMS destination requires explicit recipients; skipping goal #' . $goalId);
                    break;
                default:
                    // Unsupported or NONE
                    warn('GoalsProgressAlertsCron: Unsupported destination for goal #' . $goalId);
            }

            // Mark slot as sent (expire in a reasonable window)
            $ttl = $dueInfo['ttl'];
            redis->set('' . $redisKey, 1);
            if (method_exists(redis, 'expire')) {
                redis->expire($redisKey, $ttl);
            }

            // Track last progress for CHANGED
            if ($frequency === Freq::CHANGED) {
                $progressVal = goals_criteria::calculateProgressFromArray($criteriaArray);
                redis->set('goal_alert_last_progress:' . $goalId, (string)$progressVal);
                if (method_exists(redis, 'expire')) {
                    redis->expire('goal_alert_last_progress:' . $goalId, 86400 * 30);
                }
            }
        } catch (Throwable $e) {
            warn('GoalsProgressAlertsCron error for goal #' . $goalId . ': ' . $e->getMessage());
        }
    }
}

/**
 * Decide if an alert is due for the given criteria at the provided UTC time.
 * Returns ['due' => bool, 'slot' => string, 'ttl' => int]
 */
function goalsProgressAlertDue(goals_criteria $criteria, DateTimeImmutable $nowUtc): array
{
    $frequency = $criteria->progress_alert_frequency ?? Freq::NONE;

    // Determine alert time in a timezone (defaults to UTC current minute)
    $timeStr = $criteria->progress_alert_time_of_day;
    $alertMinuteUtc = null; // DateTimeImmutable normalized to the scheduled minute in UTC
    $slot = '';
    $ttl = 3600; // default TTL window for deduplication

    $weekdays = $criteria->progress_alert_weekdays ?? [];
    $weekdayNames = array_map(fn($e) => ($e instanceof UnitEnum ? $e->name : (string)$e), $weekdays);

    // Helper to build DateTime at today with provided HH:MM in given offset
    $buildTodayAt = function (string $hhmmTz) use ($nowUtc): ?DateTimeImmutable {
        // Convert "HH:MMZ" or with offset to a concrete time today
        if (!preg_match('/^([01]\d|2[0-3]):([0-5]\d)(Z|[+-](?:[01]\d|2[0-3]):?[0-5]\d)$/', $hhmmTz)) {
            return null;
        }
        // Extract parts
        [$h, $m, $tz] = [substr($hhmmTz,0,2), substr($hhmmTz,3,2), substr($hhmmTz,5)];
        $tzStr = $tz;
        if ($tzStr === 'Z') { $tzStr = '+00:00'; }
        if (preg_match('/^[+-]\d{4}$/', $tzStr)) {
            // Normalize +HHMM
            $tzStr = substr($tzStr,0,3) . ':' . substr($tzStr,3,2);
        }
        $localTz = new DateTimeZone($tzStr);
        $todayLocal = new DateTimeImmutable('now', $localTz);
        $scheduledLocal = $todayLocal->setTime((int)$h, (int)$m, 0, 0);
        // Convert to UTC
        return $scheduledLocal->setTimezone(new DateTimeZone('UTC'));
    };

    // Build scheduled time today (or fallback to current minute)
    if (is_string($timeStr) && $timeStr !== '') {
        $alertMinuteUtc = $buildTodayAt($timeStr);
    }
    if (!$alertMinuteUtc) {
        // Fallback: use current minute
        $alertMinuteUtc = $nowUtc->setTime((int)$nowUtc->format('H'), (int)$nowUtc->format('i'), 0, 0);
    }

    $nowMinute = $nowUtc->setTime((int)$nowUtc->format('H'), (int)$nowUtc->format('i'), 0, 0);

    switch ($frequency) {
        case Freq::DAILY:
            $slot = $nowUtc->format('Y-m-d');
            $ttl = 86400; // 1 day
            // If weekdays specified, restrict to those
            if (!empty($weekdayNames)) {
                $phpWeekday = strtoupper($nowUtc->format('l'));
                if (!in_array($phpWeekday, $weekdayNames, true)) { return ['due' => false, 'slot' => $slot, 'ttl' => $ttl]; }
            }
            return ['due' => ($nowMinute == $alertMinuteUtc), 'slot' => $slot, 'ttl' => $ttl];
        case Freq::WEEKLY:
            $slot = $nowUtc->format('o-W') . (empty($weekdayNames) ? '-MONDAY' : '-' . strtoupper($nowUtc->format('l')));
            $ttl = 86400 * 7;
            // Default to Monday if none specified
            if (empty($weekdayNames)) {
                $isMonday = ($nowUtc->format('N') === '1');
                return ['due' => $isMonday && ($nowMinute == $alertMinuteUtc), 'slot' => $slot, 'ttl' => $ttl];
            }
            $todayName = strtoupper($nowUtc->format('l'));
            if (!in_array($todayName, $weekdayNames, true)) { return ['due' => false, 'slot' => $slot, 'ttl' => $ttl]; }
            return ['due' => ($nowMinute == $alertMinuteUtc), 'slot' => $slot, 'ttl' => $ttl];
        case Freq::MONTHLY:
            $slot = $nowUtc->format('Y-m');
            $ttl = 86400 * 31;
            $isFirstOfMonth = ($nowUtc->format('j') === '1');
            return ['due' => $isFirstOfMonth && ($nowMinute == $alertMinuteUtc), 'slot' => $slot, 'ttl' => $ttl];
        case Freq::CHANGED:
            // Debounce to 1 hour per change
            $ttl = 3600;
            $slot = $nowUtc->format('Y-m-d-H');
            $lastProgress = redis->get('goal_alert_last_progress:' . ($criteria->label ?? '')) ?? null; // We'll override with goal id upstream
            // Always mark due here; upstream dedupe per goal id + hour slot applies, but we also check change upstream
            return ['due' => true, 'slot' => $slot, 'ttl' => $ttl];
        default:
            return ['due' => false, 'slot' => '', 'ttl' => 3600];
    }
}

foreach ( $cron_tasks as $task => $data ) {
    $lastRun = redis->get_last_crond_run($task) === null ? 0 : redis->get_last_crond_run($task);
    $nextRun = $lastRun + $data['interval'];
    $cron_tasks[$task]['last_run'] = $lastRun;
    $cron_tasks[$task]['next_run'] = $nextRun;
    if ($nextRun <= time()) {
        $data['function']();
        redis->set_last_crond_run($task, time());
    } else {
        $response_cron[] = $task . ' is not due to run yet, next run is at ' . date('Y-m-d H:i:s', $nextRun) . ' (' . ($nextRun - time()) . ' seconds)';
    }
}