<?php
/**
 * PaintOps - Offline Time Clock (offline write support, tier 3, slice 1)
 *
 * Tiers 1 and 2 let a painter READ the app and their jobs with no signal.
 * This is the first WRITE: punching the Time Clock (Check In / Check Out)
 * off-signal, then syncing when the phone is back online.
 *
 * How it works
 * ------------
 * When the Time Clock form is submitted with no connectivity, the page
 * intercepts the submit (instead of letting it fail), records the action
 * in an on-device outbox (IndexedDB) stamped with the moment it was
 * tapped, and updates the button/notice locally. Whenever the app is
 * online (on load and on the browser's "online" event) it drains the
 * outbox to an idempotent endpoint that applies each entry using that
 * tap-time, not the sync-time, so an 8am check-in logs 8am even if it
 * syncs at 5pm.
 *
 * Safety
 * ------
 * The endpoint is idempotent (see paintops_checkin_apply_in/out in
 * job-checkin.php): a retried entry cannot double-log. Each sync request
 * carries a fresh page nonce and re-checks paintops_user_can_checkin(),
 * so an entry can only ever apply what that user could have done live.
 * When online, submits are NOT intercepted at all, the existing live
 * form flow is untouched.
 *
 * Scope: Time Clock only for now. Expenses, notes, and photos are later
 * slices that will reuse this same outbox-and-sync pattern.
 */

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

/**
 * Idempotent endpoint that applies one queued Time Clock action for the
 * current user. Logged-in only; the action is user-specific and re-gated
 * per job. Returns JSON {ok:true,result:...} on success. Terminal client
 * errors use 4xx so the queue drops the entry instead of retrying it
 * forever; transient/server errors use 5xx so it retries later.
 */
function paintops_timeclock_sync_endpoint(): void {
    if (!is_user_logged_in()) {
        wp_send_json(['ok' => false, 'error' => 'auth'], 401);
    }

    $nonce = isset($_POST['nonce']) ? sanitize_text_field(wp_unslash($_POST['nonce'])) : '';
    if (!wp_verify_nonce($nonce, 'paintops_timeclock_sync')) {
        wp_send_json(['ok' => false, 'error' => 'nonce'], 403);
    }

    $job_id    = isset($_POST['job_id']) ? (int) $_POST['job_id'] : 0;
    $op        = isset($_POST['op']) ? sanitize_text_field(wp_unslash($_POST['op'])) : '';
    $client_ts = isset($_POST['client_ts']) ? (int) $_POST['client_ts'] : 0;

    if ($job_id <= 0 || !in_array($op, ['in', 'out'], true) || $client_ts <= 0) {
        wp_send_json(['ok' => false, 'error' => 'bad_request'], 400);
    }

    // A tap time more than a day in the future is nonsense (bad device
    // clock); fall back to server now rather than logging a future entry.
    if ($client_ts > time() + DAY_IN_SECONDS) {
        $client_ts = time();
    }

    if (!function_exists('paintops_user_can_checkin') || !paintops_user_can_checkin($job_id)) {
        wp_send_json(['ok' => false, 'error' => 'perm'], 403);
    }

    $user_id = (int) get_current_user_id();

    if ($op === 'in') {
        // Convert the device's REAL epoch to the local pseudo-epoch the
        // checkin module now expects (timezone refactor, 2026-07-21); the
        // future-timestamp clamp above stays real-vs-real on purpose.
        $r = paintops_checkin_apply_in($job_id, $user_id, $client_ts + (current_time('timestamp') - time()));
        if ($r['status'] === 'error') {
            wp_send_json(['ok' => false, 'error' => 'db'], 500);
        }
        // opened / already_open / duplicate are all "the intent is now
        // recorded", so the queue can drop the entry.
        wp_send_json(['ok' => true, 'result' => $r['status']]);
    }

    $r = paintops_checkin_apply_out($job_id, $user_id, $client_ts + (current_time('timestamp') - time()));
    if ($r['status'] === 'error') {
        wp_send_json(['ok' => false, 'error' => 'db'], 500);
    }
    // none / closed / closed_no_hours are all terminal for the queue.
    wp_send_json([
        'ok'     => true,
        'result' => $r['status'],
        'hours'  => isset($r['logged_hours']) ? (float) $r['logged_hours'] : null,
    ]);
}
add_action('wp_ajax_paintops_timeclock_sync', 'paintops_timeclock_sync_endpoint');

/**
 * Whether to print the offline Time Clock client on this request. Only
 * on the front-end app page (same detection the rest of the PWA uses).
 */
function paintops_timeclock_is_app_page(): bool {
    if (function_exists('paintops_pwa_is_app_page')) {
        return paintops_pwa_is_app_page();
    }
    if (is_admin() || !is_singular()) {
        return false;
    }
    $post = get_post();
    return $post && has_shortcode((string) $post->post_content, 'paintops_app');
}

/**
 * Enqueue the offline Time Clock client on app pages. Config (ajax URL +
 * a fresh nonce) rides along as an inline before-fragment so the static
 * script file (assets/js/pwa-timeclock.js) needs no PHP interpolation.
 *
 * Formerly printed from wp_footer at priority 21, which is too late to
 * enqueue (footer scripts print at priority 20); both gates used there
 * (app-page detection + logged-in) are already decidable at
 * wp_enqueue_scripts time, so the whole thing enqueues from here and the
 * script still executes in the footer as before.
 */
function paintops_timeclock_enqueue_assets(): void {
    if (!paintops_timeclock_is_app_page() || !is_user_logged_in()) {
        return;
    }
    $config = [
        'ajax'  => admin_url('admin-ajax.php'),
        'nonce' => wp_create_nonce('paintops_timeclock_sync'),
    ];
    paintops_enqueue_style('pwa-timeclock');
    paintops_enqueue_script('pwa-timeclock', 'window.PAINTOPS_TC = ' . wp_json_encode($config) . ';');
}
add_action('wp_enqueue_scripts', 'paintops_timeclock_enqueue_assets');
