<?php
/**
 * sync-user-profile  (hourly + on-demand)
 *
 * Pulls per-user contact + demographic fields out of WordPress and pushes
 * them to wpdeploy as canonical profile rows.
 *
 * Two trigger paths:
 *   - Hourly cron walks `wp_users` from a user_registered watermark in
 *     batches of 200.
 *   - `wp_login` / `profile_update` actions call push_for_user_id($id)
 *     to push that one user immediately, bypassing the watermark — so
 *     names appear on the central People view as soon as someone logs
 *     in or edits their profile, instead of waiting for the next cron.
 */

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

require_once __DIR__ . '/class-sync-base.php';

class AF_Profile_Sync extends AF_Sync_Base {

    protected $endpoint     = 'sync-user-profile';
    protected $cron_hook    = 'auctionforge_sync_user_profile';
    protected $cron_schedule = 'hourly';
    protected $batch_size   = 200;
    protected $health_key   = 'user-profile';

    private const PRIVILEGED_ROLES = array(
        'administrator', 'editor', 'author', 'contributor',
        'shop_manager', 'super_admin'
    );

    protected function read_batch() {
        global $wpdb;

        $watermark = get_option('auctionforge_profile_sync_watermark', '2000-01-01 00:00:00');

        $cap_key = $wpdb->prefix . 'capabilities';
        $rows = $wpdb->get_results($wpdb->prepare(
            "SELECT u.ID, u.user_email, u.user_registered, u.display_name
               FROM {$wpdb->users} u
               LEFT JOIN {$wpdb->usermeta} um
                      ON um.user_id = u.ID AND um.meta_key = %s
              WHERE u.user_registered >= %s
                AND (
                     um.meta_value IS NULL
                  OR (
                     um.meta_value NOT LIKE %s
                  AND um.meta_value NOT LIKE %s
                  AND um.meta_value NOT LIKE %s
                  AND um.meta_value NOT LIKE %s
                  AND um.meta_value NOT LIKE %s
                  AND um.meta_value NOT LIKE %s
                  )
                )
              ORDER BY u.user_registered ASC
              LIMIT %d",
            $cap_key,
            $watermark,
            '%administrator%',
            '%editor%',
            '%author%',
            '%contributor%',
            '%shop_manager%',
            '%super_admin%',
            $this->batch_size
        ), ARRAY_A);

        if (empty($rows)) return array();

        $hydrated = array();
        foreach ($rows as $r) {
            $row = self::build_user_row((int) $r['ID']);
            if ($row !== null) {
                $hydrated[] = $row;
            }
        }
        return $hydrated;
    }

    protected function build_payload($rows) {
        foreach ($rows as &$r) {
            $r = self::normalize_row($r);
        }
        unset($r);
        return array('records' => $rows);
    }

    protected function mark_synced($rows, $response) {
        if (empty($rows)) return;
        $latest = end($rows);
        if (!empty($latest['user_registered'])) {
            update_option('auctionforge_profile_sync_watermark', $latest['user_registered'], 'no');
        }
    }

    /**
     * Push a single user's profile to wpdeploy immediately.
     *
     * Called from `wp_login` (login refresh) and `profile_update` (edit
     * refresh). Skips the watermark — the row gets UPSERTed regardless,
     * so an existing row with empty name fields is overwritten with the
     * latest snapshot.
     *
     * Non-blocking by default so it doesn't add latency to the login
     * response. `wp_remote_post` with `blocking=false` opens the connection,
     * writes the request, and returns without waiting for the reply.
     */
    public static function push_for_user_id($user_id, $blocking = false) {
        $user_id = (int) $user_id;
        if ($user_id <= 0) return false;

        $row = self::build_user_row($user_id);
        if ($row === null) return false;
        $row = self::normalize_row($row);

        $url = get_option('auctionforge_update_server_url', '');
        $key = get_option('auctionforge_update_api_key', '');
        if (empty($url) || empty($key)) return false;

        $payload = array(
            'records'   => array($row),
            'site_url'  => home_url(),
            'site_name' => get_bloginfo('name'),
            'sync_time' => current_time('mysql', true),
        );

        $args = array(
            'timeout'  => $blocking ? 30 : 2,
            'blocking' => $blocking,
            'headers'  => array(
                'X-API-Key'    => $key,
                'Content-Type' => 'application/json',
            ),
            'body' => wp_json_encode($payload),
        );
        $endpoint = rtrim($url, '/') . '/api.php/sync-user-profile';

        return wp_remote_post($endpoint, $args);
    }

    /**
     * Read one user's full profile shape from WordPress, applying role
     * filtering and the display_name fallback. Returns null for privileged
     * accounts and for anything we can't find.
     */
    private static function build_user_row($user_id) {
        $user = get_userdata($user_id);
        if (!$user) return null;

        // Filter privileged roles (admins, editors, etc.).
        $roles = (array) ($user->roles ?? array());
        foreach ($roles as $role) {
            if (in_array($role, self::PRIVILEGED_ROLES, true)) return null;
        }

        $first = (string) get_user_meta($user_id, 'first_name', true);
        $last  = (string) get_user_meta($user_id, 'last_name', true);

        // Fallback: many BidSpirit-driven sites never populate the standard
        // first_name / last_name usermeta. Parse `display_name` when those
        // fields are empty so we don't ship a row with a missing name when
        // the data is sitting right there on the user record.
        if ($first === '' && $last === '' && !empty($user->display_name)) {
            $parts = preg_split('/\s+/', trim($user->display_name), 2);
            if (count($parts) === 2) {
                $first = $parts[0];
                $last  = $parts[1];
            } elseif (!empty($parts[0])) {
                $first = $parts[0];
            }
        }

        return array(
            'ID'              => $user_id,
            'user_email'      => (string) $user->user_email,
            'user_registered' => (string) $user->user_registered,
            'display_name'    => (string) $user->display_name,
            'first_name'      => $first,
            'last_name'       => $last,
            'phone'           => self::pick_meta($user_id, array('billing_phone', 'phone', 'mobile_phone', 'auctionforge_phone')),
            'address_1'       => self::pick_meta($user_id, array('billing_address_1', 'billing_address1', 'address_1')),
            'address_2'       => self::pick_meta($user_id, array('billing_address_2', 'billing_address2', 'address_2')),
            'city'            => self::pick_meta($user_id, array('billing_city', 'city')),
            'region'          => self::pick_meta($user_id, array('billing_state', 'state', 'region')),
            'postcode'        => self::pick_meta($user_id, array('billing_postcode', 'postcode', 'zip')),
            'country'         => self::pick_meta($user_id, array('billing_country', 'country')),
            'dob'             => self::pick_meta($user_id, array('dob', 'date_of_birth', 'birthday')),
            'gender'          => self::pick_meta($user_id, array('gender')),
        );
    }

    /**
     * Apply PII normalisation (lowercase email, E.164 phone, uppercase
     * stripped postcode, ISO country) to one row.
     */
    private static function normalize_row($r) {
        $r['user_email'] = strtolower(trim((string) ($r['user_email'] ?? '')));
        $r['phone']      = self::normalize_phone($r['phone'] ?? '', $r['country'] ?? '');
        $r['postcode']   = strtoupper(preg_replace('/\s+/', '', (string) ($r['postcode'] ?? '')));
        $r['country']    = strtoupper(substr((string) ($r['country'] ?? ''), 0, 2));
        return $r;
    }

    private static function pick_meta($uid, $keys) {
        foreach ($keys as $k) {
            $v = get_user_meta($uid, $k, true);
            if (!empty($v)) return $v;
        }
        return '';
    }

    /**
     * UK-aware phone normalization to E.164. Light-touch — if we can't
     * make sense of it, hand it back lightly trimmed.
     */
    private static function normalize_phone($raw, $country = '') {
        $raw = trim((string) $raw);
        if ($raw === '') return '';

        $digits = preg_replace('/[^\d+]/', '', $raw);
        if (strlen($digits) > 0 && $digits[0] === '+') return $digits;

        $cc = strtoupper(substr((string) $country, 0, 2));
        if (($cc === 'GB' || $cc === 'UK' || $cc === '') && strlen($digits) === 11 && substr($digits, 0, 2) === '07') {
            return '+44' . substr($digits, 1);
        }
        if (($cc === 'GB' || $cc === 'UK') && strlen($digits) === 10 && substr($digits, 0, 1) === '7') {
            return '+44' . $digits;
        }

        return strlen($digits) >= 7 ? '+' . $digits : $raw;
    }
}
