<?php
/**
 * Menu scheduling (date, time of day, days of week)
 *
 * @package Maca_Menu_List
 */

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

/**
 * Capitalize first letter (UTF-8 safe, e.g. måndag → Måndag).
 */
function maca_menulist_capitalize_label($text) {
    $text = (string) $text;
    if ($text === '') {
        return $text;
    }

    if (function_exists('mb_substr') && function_exists('mb_strtoupper')) {
        return mb_strtoupper(mb_substr($text, 0, 1, 'UTF-8'), 'UTF-8') . mb_substr($text, 1, null, 'UTF-8');
    }

    return ucfirst($text);
}

/**
 * Weekday labels (0 = Sunday, matching wp_date('w') / current_time('w')).
 * Uses WordPress locale so names follow site language (e.g. sv_SE).
 */
function maca_menulist_weekday_labels() {
    global $wp_locale;

    $fallback = array(
        0 => __('Sunday', 'maca-restu-pro'),
        1 => __('Monday', 'maca-restu-pro'),
        2 => __('Tuesday', 'maca-restu-pro'),
        3 => __('Wednesday', 'maca-restu-pro'),
        4 => __('Thursday', 'maca-restu-pro'),
        5 => __('Friday', 'maca-restu-pro'),
        6 => __('Saturday', 'maca-restu-pro'),
    );

    $labels = array();
    for ($day = 0; $day <= 6; $day++) {
        if (isset($wp_locale) && is_object($wp_locale)) {
            $label = $wp_locale->get_weekday($day);
        } else {
            $label = $fallback[$day];
        }
        $labels[$day] = maca_menulist_capitalize_label($label);
    }

    return $labels;
}

/**
 * Weekday index order for UI lists (Monday first by default).
 * Values are 0 (Sunday) … 6 (Saturday), matching wp_date('w').
 *
 * @param bool $monday_first Start week on Monday.
 * @return array<int, int>
 */
function maca_menulist_weekday_day_order($monday_first = true) {
    if ($monday_first) {
        return array(1, 2, 3, 4, 5, 6, 0);
    }

    return array(0, 1, 2, 3, 4, 5, 6);
}

/**
 * Sort weekday indices for display (Monday first by default).
 *
 * @param array<int, int> $days Weekday indices.
 * @param bool            $monday_first Start week on Monday.
 * @return array<int, int>
 */
function maca_menulist_sort_weekdays_for_display($days, $monday_first = true) {
    if (empty($days)) {
        return array();
    }

    $order = array_flip(maca_menulist_weekday_day_order($monday_first));
    $days = array_values(array_unique(array_map('intval', $days)));

    usort(
        $days,
        static function ($a, $b) use ($order) {
            $pos_a = isset($order[ $a ]) ? $order[ $a ] : 999;
            $pos_b = isset($order[ $b ]) ? $order[ $b ] : 999;

            if ($pos_a === $pos_b) {
                return $a <=> $b;
            }

            return $pos_a <=> $pos_b;
        }
    );

    return $days;
}

/**
 * Weekday labels keyed by day index, in display order (Monday first by default).
 *
 * @param bool $monday_first Start week on Monday.
 * @return array<int, string>
 */
function maca_menulist_weekday_labels_ordered($monday_first = true) {
    $labels = maca_menulist_weekday_labels();
    $ordered = array();

    foreach (maca_menulist_weekday_day_order($monday_first) as $day) {
        if (isset($labels[ $day ])) {
            $ordered[ $day ] = $labels[ $day ];
        }
    }

    return $ordered;
}

/**
 * Short weekday labels (Mon–Sun) keyed by day index, Monday first.
 *
 * @param bool $monday_first Start week on Monday.
 * @return array<int, string>
 */
function maca_menulist_weekday_short_labels_ordered($monday_first = true) {
    $short = array(
        0 => __('Sun', 'maca-restu-pro'),
        1 => __('Mon', 'maca-restu-pro'),
        2 => __('Tue', 'maca-restu-pro'),
        3 => __('Wed', 'maca-restu-pro'),
        4 => __('Thu', 'maca-restu-pro'),
        5 => __('Fri', 'maca-restu-pro'),
        6 => __('Sat', 'maca-restu-pro'),
    );

    $ordered = array();

    foreach (maca_menulist_weekday_day_order($monday_first) as $day) {
        if (isset($short[ $day ])) {
            $ordered[ $day ] = $short[ $day ];
        }
    }

    return $ordered;
}

/**
 * Normalize time string to HH:MM:SS for MySQL TIME.
 */
function maca_menulist_normalize_time($time) {
    if ($time === null || $time === '') {
        return null;
    }

    $time = trim(sanitize_text_field((string) $time));

    if (preg_match('/^(\d{2}:\d{2}(?::\d{2})?)/', $time, $matches)) {
        $time = $matches[1];
    }

    if (preg_match('/^\d{2}:\d{2}$/', $time)) {
        return $time . ':00';
    }

    if (preg_match('/^\d{2}:\d{2}:\d{2}$/', $time)) {
        return $time;
    }

    return null;
}

/**
 * Parse days_of_week DB value to int array.
 */
function maca_menulist_parse_days_of_week($days_of_week) {
    if ($days_of_week === null || $days_of_week === '') {
        return array();
    }

    $days = array_map('intval', explode(',', $days_of_week));
    $days = array_filter($days, function ($day) {
        return $day >= 0 && $day <= 6;
    });

    return array_values(array_unique($days));
}

/**
 * Format days for storage.
 */
function maca_menulist_format_days_of_week($days) {
    if (empty($days) || !is_array($days)) {
        return null;
    }

    $days = array_map('intval', $days);
    $days = array_filter($days, function ($day) {
        return $day >= 0 && $day <= 6;
    });
    sort($days);

    if (empty($days)) {
        return null;
    }

    return implode(',', $days);
}

/**
 * Check if current time is within a daily time range (supports overnight).
 */
function maca_menulist_time_in_range($current_time, $time_start, $time_end) {
    $current_time = maca_menulist_normalize_time($current_time) ?? '00:00:00';
    $start = maca_menulist_normalize_time($time_start) ?? '00:00:00';
    $end = maca_menulist_normalize_time($time_end) ?? '23:59:59';

    if ($start <= $end) {
        return ($current_time >= $start && $current_time <= $end);
    }

    return ($current_time >= $start || $current_time <= $end);
}

/**
 * Push a site-local MySQL datetime onto the schedule evaluation stack (admin preview).
 *
 * @param string $mysql_datetime MySQL datetime (Y-m-d H:i:s).
 * @return void
 */
function maca_menulist_push_schedule_eval_mysql($mysql_datetime) {
    if (!isset($GLOBALS['maca_menulist_schedule_eval_stack'])) {
        $GLOBALS['maca_menulist_schedule_eval_stack'] = array();
    }

    $GLOBALS['maca_menulist_schedule_eval_stack'][] = (string) $mysql_datetime;
}

/**
 * Pop the innermost schedule evaluation datetime from the stack.
 *
 * @return void
 */
function maca_menulist_pop_schedule_eval_mysql() {
    if (!empty($GLOBALS['maca_menulist_schedule_eval_stack'])) {
        array_pop($GLOBALS['maca_menulist_schedule_eval_stack']);
    }
}

/**
 * Site-local MySQL datetime used for schedule comparisons.
 *
 * @param int|null $timestamp Optional Unix timestamp from preview UI.
 * @return string
 */
function maca_menulist_get_schedule_eval_mysql($timestamp = null) {
    if ($timestamp !== null) {
        if (function_exists('maca_menulist_unix_to_site_mysql')) {
            return maca_menulist_unix_to_site_mysql((int) $timestamp);
        }

        return wp_date('Y-m-d H:i:s', (int) $timestamp);
    }

    if (!empty($GLOBALS['maca_menulist_schedule_eval_stack'])) {
        return (string) end($GLOBALS['maca_menulist_schedule_eval_stack']);
    }

    return maca_menulist_wp_now_mysql();
}

/**
 * Unix timestamp for schedule weekday/time checks.
 *
 * @param int|null $timestamp Optional preview timestamp.
 * @return int
 */
function maca_menulist_schedule_eval_timestamp($timestamp = null) {
    if ($timestamp !== null) {
        return (int) $timestamp;
    }

    $mysql = maca_menulist_get_schedule_eval_mysql();
    $parsed = maca_menulist_wp_mysql_to_timestamp($mysql);

    return $parsed === false ? time() : (int) $parsed;
}

/**
 * Normalize stored schedule datetimes (MySQL or datetime-local) for string comparison.
 *
 * @param string|null $value Stored datetime value.
 * @return string Empty when unset.
 */
function maca_menulist_normalize_schedule_stored_datetime($value) {
    $value = trim((string) $value);
    if ($value === '' || strpos($value, '0000-00-00') === 0) {
        return '';
    }

    if (strpos($value, 'T') !== false && function_exists('maca_menulist_datetime_local_to_mysql')) {
        $mysql = maca_menulist_datetime_local_to_mysql($value);
        if ($mysql !== null) {
            return $mysql;
        }

        if (preg_match('/^(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}(?::\d{2})?)$/', $value, $matches)) {
            $mysql = maca_menulist_datetime_local_to_mysql($matches[1] . 'T' . substr($matches[2], 0, 5));
            if ($mysql !== null) {
                return $mysql;
            }
        }
    }

    if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $value)) {
        return $value . ':00';
    }

    return $value;
}

/**
 * Parse a stored schedule boundary to Unix timestamp (site timezone).
 *
 * Date-only values use start-of-day for starts and end-of-day for ends.
 *
 * @param string|null $value Stored datetime.
 * @param string      $bound start|end.
 * @return int|false
 */
function maca_menulist_schedule_bound_to_timestamp($value, $bound = 'start') {
    $value = maca_menulist_normalize_schedule_stored_datetime($value);
    if ($value === '') {
        return false;
    }

    $bound = $bound === 'end' ? 'end' : 'start';

    if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
        $value .= ($bound === 'end') ? ' 23:59:59' : ' 00:00:00';
    } elseif (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $value)) {
        $value .= ':00';
    }

    return maca_menulist_wp_mysql_to_timestamp($value);
}

/**
 * Whether a menu is enabled and within its start/end date range.
 */
function maca_menulist_menu_matches_date_range($menu, $timestamp = null) {
    if (!$menu || empty($menu->is_active)) {
        return false;
    }

    $mysql_now = maca_menulist_get_schedule_eval_mysql($timestamp);
    $now_ts = maca_menulist_wp_mysql_to_timestamp($mysql_now);
    if ($now_ts === false) {
        return false;
    }

    $start_ts = maca_menulist_schedule_bound_to_timestamp($menu->start_date ?? '', 'start');
    if ($start_ts !== false && $now_ts < $start_ts) {
        return false;
    }

    $end_ts = maca_menulist_schedule_bound_to_timestamp($menu->end_date ?? '', 'end');
    if ($end_ts !== false && $now_ts > $end_ts) {
        return false;
    }

    return true;
}

/**
 * Whether a menu matches the full schedule (date, weekday, time) at the given moment.
 */
function maca_menulist_menu_matches_schedule($menu, $timestamp = null) {
    if (!maca_menulist_menu_matches_date_range($menu, $timestamp)) {
        return false;
    }

    $mysql_now = maca_menulist_get_schedule_eval_mysql($timestamp);
    $eval_ts = maca_menulist_wp_mysql_to_timestamp($mysql_now);
    if ($eval_ts === false) {
        return false;
    }

    $allowed_days = maca_menulist_parse_days_of_week($menu->days_of_week ?? '');
    if (!empty($allowed_days)) {
        $current_day = (int) wp_date('w', $eval_ts);
        if (!in_array($current_day, $allowed_days, true)) {
            return false;
        }
    }

    $time_start = maca_menulist_normalize_time($menu->time_start ?? '');
    $time_end = maca_menulist_normalize_time($menu->time_end ?? '');

    if ($time_start !== null || $time_end !== null) {
        $current_time = strlen($mysql_now) >= 19 ? substr($mysql_now, 11, 8) : wp_date('H:i:s', $eval_ts);
        if (!maca_menulist_time_in_range($current_time, $time_start, $time_end)) {
            return false;
        }
    }

    return true;
}

/**
 * Whether a scheduled item is visible on the website menu (active + start/end date only).
 *
 * Weekday and daily time rules are ignored on the website; QR uses the full schedule.
 *
 * @param object   $menu      Menu, campaign, or dish category row.
 * @param int|null $timestamp Optional Unix timestamp in site timezone context.
 * @return bool
 */
function maca_menulist_menu_matches_website_schedule($menu, $timestamp = null) {
    return maca_menulist_menu_matches_date_range($menu, $timestamp);
}

/**
 * Fetch candidate menus for a category (enabled, within date range).
 */
function maca_menulist_get_candidate_menus_for_category($category_id, $timestamp = null) {
    $category_id = intval($category_id);
    if ($category_id <= 0) {
        return array();
    }

    $timestamp = $timestamp === null ? current_time('timestamp') : (int) $timestamp;
    $mysql_now = wp_date('Y-m-d H:i:s', $timestamp);

    $menus = maca_menulist_db_get_candidate_menus($category_id, $mysql_now);

    if (empty($menus)) {
        return array();
    }

    $published = array();
    foreach ($menus as $menu) {
        if (maca_menulist_menu_matches_date_range($menu, $timestamp)) {
            $published[] = maca_menulist_normalize_menu($menu);
        }
    }

    return $published;
}

/**
 * All published menus for a category (website – ignores time/weekday rules).
 */
function maca_menulist_get_published_menus_for_category($category_id, $timestamp = null) {
    return maca_menulist_get_candidate_menus_for_category($category_id, $timestamp);
}

/**
 * Menu that matches the full schedule right now (for QR / live service).
 */
function maca_menulist_get_active_menu_for_category($category_id, $timestamp = null) {
    $menus = maca_menulist_get_candidate_menus_for_category($category_id, $timestamp);

    if (empty($menus)) {
        return null;
    }

    $timestamp = $timestamp === null ? current_time('timestamp') : (int) $timestamp;

    foreach ($menus as $menu) {
        if (maca_menulist_menu_matches_schedule($menu, $timestamp)) {
            return $menu;
        }
    }

    return null;
}

/**
 * Menu for website/shortcode: live menu if scheduled now, otherwise newest published menu.
 */
function maca_menulist_get_menu_for_website($category_id, $timestamp = null) {
    $live_menu = maca_menulist_get_active_menu_for_category($category_id, $timestamp);
    if ($live_menu) {
        return $live_menu;
    }

    $menus = maca_menulist_get_published_menus_for_category($category_id, $timestamp);

    return !empty($menus) ? $menus[0] : null;
}

/**
 * Filter categories that have a menu matching the full schedule (QR).
 */
function maca_menulist_filter_categories_with_active_menus($categories, $timestamp = null) {
    if (empty($categories)) {
        return array();
    }

    $filtered = array();
    foreach ($categories as $category) {
        if (maca_menulist_get_active_menu_for_category($category->id, $timestamp)) {
            $filtered[] = $category;
        }
    }

    return $filtered;
}

/**
 * Filter categories that have at least one published menu (website/shortcode).
 */
function maca_menulist_filter_categories_with_published_menus($categories, $timestamp = null) {
    if (empty($categories)) {
        return array();
    }

    $filtered = array();
    foreach ($categories as $category) {
        if (!empty(maca_menulist_get_published_menus_for_category($category->id, $timestamp))) {
            $filtered[] = $category;
        }
    }

    return $filtered;
}

/**
 * Schedule note for website when menu is shown outside live hours.
 */
function maca_menulist_get_website_schedule_note($menu) {
    if (!$menu || maca_menulist_menu_matches_schedule($menu)) {
        return '';
    }

    if (!maca_menulist_show_schedule_notes()) {
        return '';
    }

    return sprintf(
        /* translators: %s: schedule summary */
        __('Serving hours: %s (not live right now)', 'maca-restu-pro'),
        maca_menulist_format_schedule_summary($menu)
    );
}

/**
 * Short weekday labels for compact admin summaries.
 *
 * @return array<int, string>
 */
function maca_menulist_weekday_abbrev_labels() {
    global $wp_locale;

    $labels = maca_menulist_weekday_labels();
    $abbrevs = array();

    for ($day = 0; $day <= 6; $day++) {
        if (isset($wp_locale) && is_object($wp_locale)) {
            $name = $wp_locale->get_weekday($day);
            $abbrevs[$day] = maca_menulist_capitalize_label($wp_locale->get_weekday_abbrev($name));
        } else {
            $abbrevs[$day] = substr($labels[$day], 0, 3);
        }
    }

    return $abbrevs;
}

/**
 * Format weekday numbers as compact ranges (e.g. Mon–Fri).
 *
 * @param array<int, int> $days Day numbers 0–6.
 * @return string
 */
function maca_menulist_format_weekday_ranges($days) {
    if (empty($days)) {
        return '';
    }

    $days = array_values(array_unique(array_map('intval', $days)));
    if (function_exists('maca_menulist_sort_weekdays_for_display')) {
        $days = maca_menulist_sort_weekdays_for_display($days);
    } else {
        sort($days);
    }

    $abbrevs = maca_menulist_weekday_abbrev_labels();
    $ranges = array();
    $start = $days[0];
    $prev = $days[0];
    $count = count($days);

    for ($i = 1; $i < $count; $i++) {
        if ($days[$i] === $prev + 1) {
            $prev = $days[$i];
            continue;
        }

        if ($start === $prev) {
            $ranges[] = $abbrevs[$start];
        } else {
            $ranges[] = $abbrevs[$start] . '–' . $abbrevs[$prev];
        }

        $start = $days[$i];
        $prev = $days[$i];
    }

    if ($start === $prev) {
        $ranges[] = $abbrevs[$start];
    } else {
        $ranges[] = $abbrevs[$start] . '–' . $abbrevs[$prev];
    }

    return implode(', ', $ranges);
}

/**
 * Compact schedule summary for admin lists (e.g. Mon–Fri 11:00–14:00).
 *
 * @param object|null $menu Menu row.
 * @return string
 */
function maca_menulist_format_schedule_summary_compact($menu) {
    if (!$menu) {
        return '';
    }

    $parts = array();

    if (!empty($menu->start_date) || !empty($menu->end_date)) {
        $date_format = get_option('date_format');
        $range = array();

        if (!empty($menu->start_date)) {
            $range[] = date_i18n($date_format, strtotime((string) $menu->start_date));
        }
        if (!empty($menu->end_date)) {
            $range[] = date_i18n($date_format, strtotime((string) $menu->end_date));
        }

        if (count($range) === 2) {
            $parts[] = $range[0] . '–' . $range[1];
        } elseif (!empty($range)) {
            $parts[] = (!empty($menu->start_date) ? __('From', 'maca-restu-pro') : __('Until', 'maca-restu-pro')) . ' ' . $range[0];
        }
    }

    $schedule_bits = array();
    $days = maca_menulist_parse_days_of_week($menu->days_of_week ?? '');
    $day_part = maca_menulist_format_weekday_ranges($days);

    if (!empty($menu->time_start) || !empty($menu->time_end)) {
        $start = !empty($menu->time_start) ? substr((string) $menu->time_start, 0, 5) : '00:00';
        $end = !empty($menu->time_end) ? substr((string) $menu->time_end, 0, 5) : '24:00';
        $time_part = $start . '–' . $end;
        $schedule_bits[] = $day_part !== '' ? $day_part . ' ' . $time_part : $time_part;
    } elseif ($day_part !== '') {
        $schedule_bits[] = $day_part;
    }

    if (!empty($schedule_bits)) {
        $parts[] = implode(', ', $schedule_bits);
    }

    if (empty($parts)) {
        return __('Always', 'maca-restu-pro');
    }

    return implode(' · ', $parts);
}

/**
 * Schedule cell content for the admin menu list.
 *
 * @param object|null $menu Menu row.
 * @return array{status: string, line: string}
 */
function maca_menulist_get_admin_menu_schedule_display($menu) {
    $status = maca_menulist_get_menu_status($menu);
    $schedule = maca_menulist_format_schedule_summary_compact($menu);
    $always_label = __('Always', 'maca-restu-pro');

    if ($status === 'active') {
        return array(
            'status' => $status,
            'line' => $schedule,
        );
    }

    if ($status === 'scheduled' && !empty($menu->start_date)) {
        $prefix = sprintf(
            /* translators: %s: start date */
            __('Starts %s', 'maca-restu-pro'),
            date_i18n(get_option('date_format'), strtotime((string) $menu->start_date))
        );
    } elseif ($status === 'inactive') {
        $prefix = __('Inactive', 'maca-restu-pro');
    } elseif ($status === 'expired') {
        $prefix = __('Expired', 'maca-restu-pro');
    } elseif ($status === 'outside_hours') {
        $prefix = __('Outside schedule', 'maca-restu-pro');
    } else {
        $prefix = '';
    }

    if ($prefix === '') {
        return array(
            'status' => $status,
            'line' => $schedule,
        );
    }

    if ($schedule === $always_label) {
        return array(
            'status' => $status,
            'line' => $prefix,
        );
    }

    return array(
        'status' => $status,
        'line' => $prefix . ' · ' . $schedule,
    );
}

/**
 * Human-readable schedule summary for admin lists.
 */
function maca_menulist_format_schedule_summary($menu) {
    $parts = array();

    if (!empty($menu->start_date) || !empty($menu->end_date)) {
        $range = array();
        if (!empty($menu->start_date)) {
            $range[] = __('From', 'maca-restu-pro') . ' ' . $menu->start_date;
        }
        if (!empty($menu->end_date)) {
            $range[] = __('To', 'maca-restu-pro') . ' ' . $menu->end_date;
        }
        $parts[] = implode(' ', $range);
    }

    if (!empty($menu->time_start) || !empty($menu->time_end)) {
        $start = !empty($menu->time_start) ? substr($menu->time_start, 0, 5) : '00:00';
        $end = !empty($menu->time_end) ? substr($menu->time_end, 0, 5) : '24:00';
        $parts[] = sprintf(
            /* translators: 1: start time, 2: end time */
            __('Daily %1$s–%2$s', 'maca-restu-pro'),
            $start,
            $end
        );
    }

    $days = maca_menulist_sort_weekdays_for_display(
        maca_menulist_parse_days_of_week($menu->days_of_week ?? '')
    );
    if (!empty($days)) {
        $labels = maca_menulist_weekday_labels();
        $day_names = array();
        foreach ($days as $day) {
            if (isset($labels[$day])) {
                $day_names[] = $labels[$day];
            }
        }
        if (!empty($day_names)) {
            $parts[] = implode(', ', $day_names);
        }
    }

    if (empty($parts)) {
        return __('Always', 'maca-restu-pro');
    }

    return implode(' · ', $parts);
}

/**
 * Format a MySQL datetime for HTML datetime-local inputs.
 *
 * @param string|null $mysql_datetime Stored datetime.
 * @return string
 */
function maca_menulist_format_datetime_local_value($mysql_datetime) {
    if ($mysql_datetime === null || $mysql_datetime === '') {
        return '';
    }

    return str_replace(' ', 'T', substr((string) $mysql_datetime, 0, 16));
}

/**
 * Admin status label for a menu.
 */
function maca_menulist_get_menu_status($menu, $timestamp = null) {
    if (!$menu || empty($menu->is_active)) {
        return 'inactive';
    }

    $mysql_now = maca_menulist_get_schedule_eval_mysql($timestamp);
    $now_ts = maca_menulist_wp_mysql_to_timestamp($mysql_now);
    if ($now_ts === false) {
        return 'inactive';
    }

    $start_ts = maca_menulist_schedule_bound_to_timestamp($menu->start_date ?? '', 'start');
    if ($start_ts !== false && $now_ts < $start_ts) {
        return 'scheduled';
    }

    $end_ts = maca_menulist_schedule_bound_to_timestamp($menu->end_date ?? '', 'end');
    if ($end_ts !== false && $now_ts > $end_ts) {
        return 'expired';
    }

    if (maca_menulist_menu_matches_schedule($menu, $timestamp)) {
        return 'active';
    }

    return 'outside_hours';
}

/**
 * Parse schedule fields from an admin POST request.
 *
 * @return array<string, string|null>
 */
function maca_menulist_parse_schedule_from_request() {
    // phpcs:disable WordPress.Security.NonceVerification.Missing -- Caller verifies nonce.
    $start_raw = !empty($_POST['start_date']) ? sanitize_text_field(wp_unslash($_POST['start_date'])) : '';
    $end_raw = !empty($_POST['end_date']) ? sanitize_text_field(wp_unslash($_POST['end_date'])) : '';
    $start_date = $start_raw !== '' && function_exists('maca_menulist_datetime_local_to_mysql')
        ? maca_menulist_datetime_local_to_mysql($start_raw)
        : ($start_raw !== '' ? $start_raw : null);
    $end_date = $end_raw !== '' && function_exists('maca_menulist_datetime_local_to_mysql')
        ? maca_menulist_datetime_local_to_mysql($end_raw)
        : ($end_raw !== '' ? $end_raw : null);
    $time_start = maca_menulist_normalize_time(isset($_POST['time_start']) ? sanitize_text_field(wp_unslash($_POST['time_start'])) : '');
    $time_end = maca_menulist_normalize_time(isset($_POST['time_end']) ? sanitize_text_field(wp_unslash($_POST['time_end'])) : '');
    $days_raw = isset($_POST['days_of_week']) ? array_map('sanitize_text_field', (array) wp_unslash($_POST['days_of_week'])) : array();
    // phpcs:enable WordPress.Security.NonceVerification.Missing

    return array(
        'start_date' => $start_date,
        'end_date' => $end_date,
        'time_start' => $time_start,
        'time_end' => $time_end,
        'days_of_week' => maca_menulist_format_days_of_week($days_raw),
    );
}

/**
 * Whether a dish category is visible on the QR menu right now (active + schedule).
 *
 * @param object   $category  Dish category row object.
 * @param int|null $timestamp Optional Unix timestamp in site timezone context.
 * @return bool
 */
function maca_menulist_dish_category_is_visible_on_qr($category, $timestamp = null) {
    if (!$category || !maca_menulist_dish_category_is_active($category)) {
        return false;
    }

    return maca_menulist_menu_matches_schedule($category, $timestamp);
}

/**
 * Whether a dish category is visible on the website menu (active + date range only).
 *
 * @param object   $category  Dish category row object.
 * @param int|null $timestamp Optional Unix timestamp in site timezone context.
 * @return bool
 */
function maca_menulist_dish_category_is_visible_on_website($category, $timestamp = null) {
    if (!$category || !maca_menulist_dish_category_is_active($category)) {
        return false;
    }

    return maca_menulist_menu_matches_website_schedule($category, $timestamp);
}

/**
 * Admin/debug label for why a dish category is hidden at a moment.
 *
 * @param object   $category    Dish category row.
 * @param int|null $timestamp   Preview timestamp.
 * @param bool     $qr_schedule QR uses full schedule; website uses date range only.
 * @return string
 */
function maca_menulist_explain_dish_category_visibility($category, $timestamp = null, $qr_schedule = false) {
    if (!$category || !maca_menulist_dish_category_is_active($category)) {
        return 'inactive';
    }

    if (!maca_menulist_menu_matches_date_range($category, $timestamp)) {
        $status = maca_menulist_get_menu_status($category, $timestamp);
        if (in_array($status, array('scheduled', 'expired'), true)) {
            return $status;
        }

        return 'date_range';
    }

    if ($qr_schedule && !maca_menulist_menu_matches_schedule($category, $timestamp)) {
        $status = maca_menulist_get_menu_status($category, $timestamp);

        return $status === 'active' ? 'outside_hours' : $status;
    }

    return 'visible';
}

/**
 * Human-readable label for dish category visibility reason.
 *
 * @param string $reason Reason code from maca_menulist_explain_dish_category_visibility().
 * @return string
 */
function maca_menulist_dish_category_visibility_reason_label($reason) {
    $labels = array(
        'visible' => __('Visible', 'maca-restu-pro'),
        'inactive' => __('Inactive', 'maca-restu-pro'),
        'scheduled' => __('Scheduled (start date in the future)', 'maca-restu-pro'),
        'expired' => __('Expired (end date passed)', 'maca-restu-pro'),
        'date_range' => __('Outside start/end dates', 'maca-restu-pro'),
        'outside_hours' => __('Outside weekday or serving time', 'maca-restu-pro'),
        'no_dishes' => __('No active dishes in category', 'maca-restu-pro'),
        'exclusive_campaign' => __('Hidden by exclusive campaign mode', 'maca-restu-pro'),
    );

    return isset($labels[ $reason ]) ? $labels[ $reason ] : $reason;
}

/**
 * Whether a dish is visible on the QR menu right now (active dish + visible category).
 *
 * @param object   $dish      Dish row object.
 * @param int|null $timestamp Optional Unix timestamp in site timezone context.
 * @return bool
 */
function maca_menulist_dish_is_visible_on_qr($dish, $timestamp = null) {
    if (!$dish || empty($dish->is_active)) {
        return false;
    }

    if (empty($dish->category_id)) {
        return false;
    }

    $category = maca_menulist_db_get_dish_category((int) $dish->category_id);

    return maca_menulist_dish_category_is_visible_on_qr($category, $timestamp);
}

/**
 * Whether a dish can be ordered from the website menu (active only).
 *
 * @param object $dish Dish row object.
 * @return bool
 */
function maca_menulist_dish_is_visible_on_website($dish) {
    return (bool) ($dish && !empty($dish->is_active));
}

/**
 * Admin status label for a dish category schedule (same rules as menu files).
 *
 * @param object $category Dish category row object.
 * @return string
 */
function maca_menulist_get_dish_category_status($category) {
    if (!$category || !maca_menulist_dish_category_is_active($category)) {
        return 'inactive';
    }

    return maca_menulist_get_menu_status($category);
}

/**
 * Admin QR status for a dish (active flag + parent category schedule).
 *
 * @param object $dish Dish row object.
 * @return string
 */
function maca_menulist_get_dish_qr_status($dish) {
    if (!$dish || empty($dish->is_active)) {
        return 'inactive';
    }

    if (empty($dish->category_id)) {
        return 'inactive';
    }

    $category = maca_menulist_db_get_dish_category((int) $dish->category_id);
    if (!$category || !maca_menulist_dish_category_is_active($category)) {
        return 'inactive';
    }

    $category_status = maca_menulist_get_menu_status($category);
    if ($category_status !== 'active') {
        return $category_status;
    }

    return 'active';
}

/**
 * Admin status label for a dish on the QR menu.
 *
 * @param object $dish Dish row object.
 * @return string
 */
function maca_menulist_get_dish_status($dish) {
    return maca_menulist_get_dish_qr_status($dish);
}

/**
 * Normalize schedule start/end values on a single DB table row set.
 *
 * @param string $table Table name.
 * @return int Number of rows updated.
 */
function maca_menulist_db_migrate_schedule_datetime_table($table) {
    global $wpdb;

    $table = (string) $table;
    if ($table === '') {
        return 0;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    if ($wpdb->get_var("SHOW TABLES LIKE '$table'") !== $table) {
        return 0;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    if (!$wpdb->get_var("SHOW COLUMNS FROM $table LIKE 'start_date'")) {
        return 0;
    }

    // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    $rows = $wpdb->get_results("SELECT id, start_date, end_date FROM $table WHERE start_date IS NOT NULL OR end_date IS NOT NULL");
    if (empty($rows)) {
        return 0;
    }

    $updated = 0;

    foreach ($rows as $row) {
        $row_id = (int) $row->id;
        if ($row_id <= 0) {
            continue;
        }

        $data = array();
        $formats = array();

        foreach (array('start_date', 'end_date') as $column) {
            $raw = isset($row->$column) ? (string) $row->$column : '';
            if ($raw === '') {
                continue;
            }

            $normalized = maca_menulist_normalize_schedule_stored_datetime($raw);
            if ($normalized === '' || ($normalized === $raw && strpos($raw, 'T') === false)) {
                continue;
            }

            $data[$column] = $normalized;
            $formats[] = '%s';
        }

        if (empty($data)) {
            continue;
        }

        $result = $wpdb->update(
            $table,
            $data,
            array('id' => $row_id),
            $formats,
            array('%d')
        );

        if ($result !== false) {
            $updated++;
        }
    }

    return $updated;
}

/**
 * One-time migration: convert datetime-local values (Y-m-dTH:i) to MySQL (Y-m-d H:i:s).
 *
 * @return void
 */
function maca_menulist_db_migrate_schedule_datetime_format() {
    if (get_option('maca_menulist_db_migrated_schedule_datetime_format') === '1') {
        return;
    }

    if (!function_exists('maca_menulist_datetime_local_to_mysql')) {
        require_once MACA_MENULIST_PLUGIN_DIR . 'includes/datetime.php';
    }

    $tables = array();

    if (!function_exists('maca_menulist_db_dish_categories_table')) {
        require_once MACA_MENULIST_PLUGIN_DIR . 'includes/db-dishes.php';
    }
    if (function_exists('maca_menulist_db_dish_categories_table')) {
        $tables[] = maca_menulist_db_dish_categories_table();
    }

    if (!function_exists('maca_menulist_db_campaigns_table')) {
        require_once MACA_MENULIST_PLUGIN_DIR . 'includes/db-campaigns.php';
    }
    if (function_exists('maca_menulist_db_campaigns_table')) {
        $tables[] = maca_menulist_db_campaigns_table();
    }

    if (!function_exists('maca_menulist_db_menus_table')) {
        require_once MACA_MENULIST_PLUGIN_DIR . 'includes/db.php';
    }
    if (function_exists('maca_menulist_db_menus_table')) {
        $tables[] = maca_menulist_db_menus_table();
    }

    if (function_exists('maca_menulist_db_dishes_table')) {
        $tables[] = maca_menulist_db_dishes_table();
    }

    $tables = array_values(array_unique(array_filter($tables)));
    $updated_rows = 0;

    foreach ($tables as $table) {
        $updated_rows += maca_menulist_db_migrate_schedule_datetime_table($table);
    }

    update_option('maca_menulist_db_migrated_schedule_datetime_format', '1', false);

    if ($updated_rows > 0 && function_exists('maca_menulist_debug_log')) {
        maca_menulist_debug_log('schedule_datetime_format: normalized rows=' . (string) $updated_rows);
    }
}

/**
 * Re-run schedule datetime normalization for rows still stored as datetime-local (T).
 *
 * @return void
 */
function maca_menulist_db_migrate_schedule_datetime_format_v2() {
    if (get_option('maca_menulist_db_migrated_schedule_datetime_format_v2') === '1') {
        return;
    }

    delete_option('maca_menulist_db_migrated_schedule_datetime_format');
    maca_menulist_db_migrate_schedule_datetime_format();
    update_option('maca_menulist_db_migrated_schedule_datetime_format_v2', '1', false);
}
