<?php
/**
 * Google OIDC plus passwordless staff authentication.
 * Identity is independent from the Local/Square booking backend.
 */
if ( ! defined( 'ABSPATH' ) ) { exit; }

class YATOON_Google_Identity {
    const SCHEMA = '4.6.0-identity-v2';
    const STATE_TTL = 10 * MINUTE_IN_SECONDS;
    const OTP_TTL = 10 * MINUTE_IN_SECONDS;

    public static function init() {
        self::install_schema();
        add_action( 'admin_menu', array( __CLASS__, 'admin_menu' ), 28 );
        add_action( 'admin_post_yatoon_save_identity_settings', array( __CLASS__, 'save_settings' ) );
        add_action( 'admin_post_yatoon_unlink_identity', array( __CLASS__, 'unlink_identity' ) );
        add_action( 'admin_post_nopriv_yatoon_google_identity_start', array( __CLASS__, 'start_google' ) );
        add_action( 'admin_post_yatoon_google_identity_start', array( __CLASS__, 'start_google' ) );
        add_action( 'admin_post_nopriv_yatoon_google_identity_callback', array( __CLASS__, 'google_callback' ) );
        add_action( 'admin_post_yatoon_google_identity_callback', array( __CLASS__, 'google_callback' ) );
        foreach ( array( 'yatoon_staff_identity_send_otp' => 'ajax_send_staff_otp', 'yatoon_staff_identity_verify_otp' => 'ajax_verify_staff_otp' ) as $action => $method ) {
            add_action( 'wp_ajax_' . $action, array( __CLASS__, $method ) );
            add_action( 'wp_ajax_nopriv_' . $action, array( __CLASS__, $method ) );
        }
        add_action( 'yatoon_booking_created', array( __CLASS__, 'reconcile_customer_link' ), 120 );
    }

    public static function install_schema() {
        if ( self::SCHEMA === get_option( 'yatoon_identity_schema', '' ) ) { return; }
        global $wpdb;
        require_once ABSPATH . 'wp-admin/includes/upgrade.php';
        $charset = $wpdb->get_charset_collate();
        dbDelta( "CREATE TABLE {$wpdb->prefix}yatoon_identity_links (
            id bigint(20) NOT NULL AUTO_INCREMENT,
            account_type varchar(20) NOT NULL,
            account_id bigint(20) NOT NULL DEFAULT 0,
            provider varchar(20) NOT NULL,
            provider_subject varchar(255) NOT NULL,
            verified_email varchar(255) DEFAULT '',
            external_account_id varchar(255) DEFAULT '',
            created_at datetime DEFAULT CURRENT_TIMESTAMP,
            last_login_at datetime DEFAULT CURRENT_TIMESTAMP,
            PRIMARY KEY (id),
            UNIQUE KEY provider_identity (account_type,provider,provider_subject),
            KEY account_provider (account_type,account_id,provider),
            KEY verified_email (verified_email)
        ) {$charset};" );
        if ( YATOON_Schema_Verifier::tables_exist( array( 'yatoon_identity_links' ) ) ) {
            update_option( 'yatoon_identity_schema', self::SCHEMA, false );
        }
    }

    public static function callback_url() {
        return admin_url( 'admin-post.php?action=yatoon_google_identity_callback' );
    }

    private static function client_id() {
        return trim( (string) get_option( 'yatoon_google_identity_client_id', get_option( 'yatoon_google_cal_client_id', '' ) ) );
    }

    private static function client_secret() {
        if ( class_exists( 'YATOON_Crypto' ) ) {
            $secret = YATOON_Crypto::get_option( 'yatoon_google_identity_client_secret', '' );
            if ( $secret ) { return trim( (string) $secret ); }
            return trim( (string) YATOON_Crypto::get_option( 'yatoon_google_cal_client_secret', '' ) );
        }
        return '';
    }

    public static function google_enabled( $context = 'customer' ) {
        if ( 'customer' === $context && ! class_exists( 'YATOON_Customer_Portal' ) ) { return false; }
        $enabled = 'staff' === $context ? 'yatoon_staff_google_login_enabled' : 'yatoon_customer_google_login_enabled';
        return '1' === (string) get_option( 'yatoon_google_identity_enabled', '0' )
            && '1' === (string) get_option( $enabled, '1' )
            && self::client_id() && self::client_secret();
    }

    public static function start_url( $context, $return_url ) {
        if ( ! self::google_enabled( $context ) ) { return ''; }
        return add_query_arg( array(
            'action'     => 'yatoon_google_identity_start',
            'context'    => 'staff' === $context ? 'staff' : 'customer',
            'return_url' => rawurlencode( self::safe_return_url( $return_url ) ),
        ), admin_url( 'admin-post.php' ) );
    }

    /** Official multicolour Google G mark for sign-in controls. */
    public static function button_icon() {
        return '<span class="sbs-google-signin-mark" aria-hidden="true"><svg width="18" height="18" style="display:block;width:18px!important;height:18px!important;max-width:18px!important;max-height:18px!important" viewBox="0 0 18 18" focusable="false"><path fill="#4285F4" d="M17.64 9.205c0-.638-.057-1.252-.164-1.841H9v3.481h4.844a4.14 4.14 0 0 1-1.797 2.716v2.258h2.909c1.702-1.567 2.684-3.874 2.684-6.614Z"/><path fill="#34A853" d="M9 18c2.43 0 4.468-.806 5.956-2.181l-2.909-2.258c-.806.54-1.835.859-3.047.859-2.344 0-4.328-1.585-5.037-3.714H.956v2.333A9 9 0 0 0 9 18Z"/><path fill="#FBBC05" d="M3.963 10.706A5.41 5.41 0 0 1 3.682 9c0-.592.102-1.167.281-1.706V4.961H.956A9 9 0 0 0 0 9c0 1.452.347 2.827.956 4.039l3.007-2.333Z"/><path fill="#EA4335" d="M9 3.58c1.322 0 2.508.455 3.441 1.346l2.581-2.581C13.464.892 11.426 0 9 0A9 9 0 0 0 .956 4.961l3.007 2.333C4.672 5.165 6.656 3.58 9 3.58Z"/></svg></span>';
    }

    private static function safe_return_url( $url ) {
        $fallback = home_url( '/' );
        $url = wp_validate_redirect( esc_url_raw( rawurldecode( (string) $url ) ), $fallback );
        return 0 === strpos( $url, home_url() ) ? $url : $fallback;
    }

    public static function start_google() {
        $context = 'staff' === sanitize_key( wp_unslash( $_GET['context'] ?? '' ) ) ? 'staff' : 'customer';
        if ( ! self::google_enabled( $context ) ) { wp_die( esc_html__( 'Google sign-in is not configured.', 'yatoon-booking-system' ), 400 ); }
        $state = wp_generate_password( 48, false, false );
        $nonce = wp_generate_password( 48, false, false );
        $return_url = self::safe_return_url( wp_unslash( $_GET['return_url'] ?? home_url( '/' ) ) );
        set_transient( 'yatoon_oidc_state_' . hash( 'sha256', $state ), array(
            'context' => $context, 'nonce' => $nonce, 'return_url' => $return_url,
            'fingerprint' => self::fingerprint(), 'created_at' => time(),
        ), self::STATE_TTL );
        setcookie( 'yatoon_oidc_state', hash_hmac( 'sha256', $state, wp_salt( 'nonce' ) ), array(
            'expires' => time() + self::STATE_TTL, 'path' => COOKIEPATH ? COOKIEPATH : '/',
            'domain' => COOKIE_DOMAIN ? COOKIE_DOMAIN : '', 'secure' => is_ssl(),
            'httponly' => true, 'samesite' => 'Lax',
        ) );
        $url = add_query_arg( array(
            'client_id' => self::client_id(), 'redirect_uri' => self::callback_url(),
            'response_type' => 'code', 'scope' => 'openid email profile',
            'state' => $state, 'nonce' => $nonce, 'prompt' => 'select_account',
        ), 'https://accounts.google.com/o/oauth2/v2/auth' );
        wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect -- fixed Google OAuth endpoint.
        exit;
    }

    public static function google_callback() {
        $state = sanitize_text_field( wp_unslash( $_GET['state'] ?? '' ) );
        $key = 'yatoon_oidc_state_' . hash( 'sha256', $state );
        $pending = $state ? get_transient( $key ) : false;
        delete_transient( $key );
        $cookie_state = sanitize_text_field( wp_unslash( $_COOKIE['yatoon_oidc_state'] ?? '' ) );
        setcookie( 'yatoon_oidc_state', '', array( 'expires' => time() - HOUR_IN_SECONDS, 'path' => COOKIEPATH ? COOKIEPATH : '/', 'domain' => COOKIE_DOMAIN ? COOKIE_DOMAIN : '', 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax' ) );
        $expected_cookie = $state ? hash_hmac( 'sha256', $state, wp_salt( 'nonce' ) ) : '';
        if ( ! is_array( $pending ) || ! $cookie_state || ! hash_equals( $expected_cookie, $cookie_state ) || ! hash_equals( (string) ( $pending['fingerprint'] ?? '' ), self::fingerprint() ) ) {
            self::redirect_error( home_url( '/' ), 'invalid_state' );
        }
        $return_url = self::safe_return_url( $pending['return_url'] ?? home_url( '/' ) );
        if ( isset( $_GET['error'] ) ) { self::redirect_error( $return_url, 'access_denied' ); }
        $code = sanitize_text_field( wp_unslash( $_GET['code'] ?? '' ) );
        $response = wp_remote_post( 'https://oauth2.googleapis.com/token', array( 'timeout' => 15, 'body' => array(
            'code' => $code, 'client_id' => self::client_id(), 'client_secret' => self::client_secret(),
            'redirect_uri' => self::callback_url(), 'grant_type' => 'authorization_code',
        ) ) );
        if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { self::redirect_error( $return_url, 'token_exchange' ); }
        $body = json_decode( wp_remote_retrieve_body( $response ), true );
        $claims = self::validated_claims( $body['id_token'] ?? '', $pending['nonce'] ?? '' );
        if ( is_wp_error( $claims ) ) { self::redirect_error( $return_url, $claims->get_error_code() ); }
        // Cross-check the assertion against Google's authenticated UserInfo
        // endpoint. The access token came directly from the code exchange.
        $userinfo_response = wp_remote_get( 'https://openidconnect.googleapis.com/v1/userinfo', array( 'timeout' => 15, 'headers' => array( 'Authorization' => 'Bearer ' . sanitize_text_field( $body['access_token'] ?? '' ) ) ) );
        $userinfo = ! is_wp_error( $userinfo_response ) && 200 === wp_remote_retrieve_response_code( $userinfo_response ) ? json_decode( wp_remote_retrieve_body( $userinfo_response ), true ) : null;
        if ( ! is_array( $userinfo ) || ! hash_equals( (string) $claims['sub'], (string) ( $userinfo['sub'] ?? '' ) ) || ! hash_equals( strtolower( (string) $claims['email'] ), strtolower( (string) ( $userinfo['email'] ?? '' ) ) ) ) {
            self::redirect_error( $return_url, 'userinfo_mismatch' );
        }
        $context = $pending['context'] ?? 'customer';
        if ( 'staff' === $context ) {
            $staff_id = self::resolve_staff( $claims );
            if ( ! $staff_id ) { self::redirect_error( $return_url, 'staff_not_authorized' ); }
            self::remember_link( 'staff', $staff_id, $claims );
            $handoff = wp_generate_password( 48, false, false );
            set_transient( 'yatoon_staff_handoff_' . hash( 'sha256', $handoff ), array( 'staff_id' => $staff_id, 'fingerprint' => self::fingerprint() ), 2 * MINUTE_IN_SECONDS );
            wp_safe_redirect( add_query_arg( array( 'yatoon_staff_handoff' => rawurlencode( $handoff ) ), $return_url ) );
            exit;
        }
        if ( ! class_exists( 'YATOON_Customer_Portal' ) || ! YATOON_Customer_Portal::get_instance() ) { self::redirect_error( $return_url, 'portal_unavailable' ); }
        $customer = YATOON_Customer_Portal::get_instance()->login_from_google( $claims );
        if ( is_wp_error( $customer ) ) { self::redirect_error( $return_url, $customer->get_error_code() ); }
        self::remember_link( 'customer', absint( $customer['local_client_id'] ?? 0 ), $claims );
        if ( ! empty( $customer['square_id'] ) ) {
            global $wpdb;
            $wpdb->update(
                $wpdb->prefix . 'yatoon_identity_links',
                array( 'external_account_id' => sanitize_text_field( $customer['square_id'] ) ),
                array( 'account_type' => 'customer', 'provider' => 'google', 'provider_subject' => sanitize_text_field( $claims['sub'] ) ),
                array( '%s' ),
                array( '%s', '%s', '%s' )
            );
        }
        // Do not rely only on the callback request's PHP session/cookie. Some
        // managed hosts and reverse proxies route the final booking page to a
        // different worker or cache layer. The destination consumes this
        // opaque, single-use handoff and establishes its own customer session.
        $portal  = class_exists( 'YATOON_Customer_Portal' ) ? YATOON_Customer_Portal::get_instance() : null;
        $handoff = $portal ? $portal->create_login_handoff( $customer ) : '';
        $redirect_args = array( 'yatoon_google_login' => 'success' );
        if ( $handoff ) {
            $redirect_args['yatoon_customer_handoff'] = rawurlencode( $handoff );
        }
        wp_safe_redirect( add_query_arg( $redirect_args, $return_url ) );
        exit;
    }

    private static function validated_claims( $jwt, $expected_nonce ) {
        $parts = explode( '.', (string) $jwt );
        if ( 3 !== count( $parts ) ) { return new WP_Error( 'invalid_id_token' ); }
        $payload = json_decode( self::base64url_decode( $parts[1] ), true );
        if ( ! is_array( $payload ) ) { return new WP_Error( 'invalid_id_token' ); }
        $aud = $payload['aud'] ?? '';
        $issuer = $payload['iss'] ?? '';
        if ( ! hash_equals( self::client_id(), (string) $aud )
            || ! in_array( $issuer, array( 'https://accounts.google.com', 'accounts.google.com' ), true )
            || (int) ( $payload['exp'] ?? 0 ) < time() - 30
            || (int) ( $payload['iat'] ?? 0 ) > time() + 300
            || ! hash_equals( (string) $expected_nonce, (string) ( $payload['nonce'] ?? '' ) )
            || ! filter_var( $payload['email_verified'] ?? false, FILTER_VALIDATE_BOOLEAN )
            || empty( $payload['sub'] ) || empty( $payload['email'] ) ) {
            return new WP_Error( 'invalid_id_claims' );
        }
        return array(
            'sub' => sanitize_text_field( $payload['sub'] ), 'email' => sanitize_email( $payload['email'] ),
            'name' => sanitize_text_field( $payload['name'] ?? '' ), 'picture' => esc_url_raw( $payload['picture'] ?? '' ),
        );
    }

    private static function base64url_decode( $value ) {
        $value = strtr( (string) $value, '-_', '+/' );
        return (string) base64_decode( $value . str_repeat( '=', ( 4 - strlen( $value ) % 4 ) % 4 ), true );
    }

    private static function resolve_staff( array $claims ) {
        global $wpdb;
        $linked = $wpdb->get_var( $wpdb->prepare( "SELECT account_id FROM {$wpdb->prefix}yatoon_identity_links WHERE account_type='staff' AND provider='google' AND provider_subject=%s LIMIT 1", $claims['sub'] ) );
        if ( $linked ) {
            return (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_staff WHERE id=%d AND is_active=1 LIMIT 1", $linked ) );
        }
        $allowed = array_map( 'absint', (array) get_option( 'yatoon_staff_google_login_ids', array() ) );
        if ( ! $allowed ) { return 0; }
        $placeholders = implode( ',', array_fill( 0, count( $allowed ), '%d' ) );
        $params = array_merge( array( $claims['email'] ), $allowed );
        return (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_staff WHERE LOWER(email)=LOWER(%s) AND is_active=1 AND id IN ({$placeholders}) LIMIT 1", $params ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
    }

    public static function exchange_staff_handoff( $handoff ) {
        $key = 'yatoon_staff_handoff_' . hash( 'sha256', (string) $handoff );
        $data = get_transient( $key );
        delete_transient( $key );
        if ( ! is_array( $data ) || ! hash_equals( (string) ( $data['fingerprint'] ?? '' ), self::fingerprint() ) ) { return 0; }
        return absint( $data['staff_id'] ?? 0 );
    }

    private static function remember_link( $type, $account_id, array $claims ) {
        global $wpdb;
        $table = $wpdb->prefix . 'yatoon_identity_links';
        $id = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE account_type=%s AND provider='google' AND provider_subject=%s LIMIT 1", $type, $claims['sub'] ) );
        $data = array( 'account_id' => absint( $account_id ), 'verified_email' => $claims['email'], 'last_login_at' => current_time( 'mysql', true ) );
        if ( $id ) { $wpdb->update( $table, $data, array( 'id' => (int) $id ), array( '%d', '%s', '%s' ), array( '%d' ) ); }
        else {
            $wpdb->insert( $table, array_merge( $data, array( 'account_type' => $type, 'provider' => 'google', 'provider_subject' => $claims['sub'], 'created_at' => current_time( 'mysql', true ) ) ), array( '%d', '%s', '%s', '%s', '%s', '%s', '%s' ) );
            if ( class_exists( 'YATOON_Insights' ) ) { YATOON_Insights::audit( 'identity_linked', $type . ' Google identity linked', $type, absint( $account_id ) ); }
        }
    }

    /** Attach a previously unbound Google identity after its first booking. */
    public static function reconcile_customer_link( $booking ) {
        if ( ! class_exists( 'YATOON_Customer_Portal' ) || ! YATOON_Customer_Portal::get_instance() ) { return; }
        $customer = (array) YATOON_Customer_Portal::get_instance()->current_customer();
        if ( empty( $customer['google_sub'] ) ) { return; }
        $email = sanitize_email( $booking['customer_email'] ?? $customer['email'] ?? '' );
        if ( ! $email || ! hash_equals( strtolower( (string) ( $customer['email'] ?? '' ) ), strtolower( $email ) ) ) { return; }
        global $wpdb;
        $client_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$wpdb->prefix}yatoon_clients WHERE LOWER(customer_email)=LOWER(%s) ORDER BY id DESC LIMIT 1", $email ) );
        if ( $client_id ) {
            $wpdb->update( $wpdb->prefix . 'yatoon_identity_links', array( 'account_id' => $client_id ), array( 'account_type' => 'customer', 'provider' => 'google', 'provider_subject' => sanitize_text_field( $customer['google_sub'] ) ), array( '%d' ), array( '%s', '%s', '%s' ) );
        }

        if ( ! empty( $booking['square_booking_id'] ) && class_exists( 'YATOON_Square_API' ) ) {
            $api = new YATOON_Square_API();
            $result = $api->get_booking( sanitize_text_field( $booking['square_booking_id'] ) );
            $square_customer_id = is_array( $result ) ? sanitize_text_field( $result['booking']['customer_id'] ?? '' ) : '';
            if ( $square_customer_id ) {
                $wpdb->update(
                    $wpdb->prefix . 'yatoon_identity_links',
                    array( 'external_account_id' => $square_customer_id ),
                    array( 'account_type' => 'customer', 'provider' => 'google', 'provider_subject' => sanitize_text_field( $customer['google_sub'] ) ),
                    array( '%s' ),
                    array( '%s', '%s', '%s' )
                );
            }
        }
    }

    private static function fingerprint() {
        $ua = substr( sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ), 0, 255 );
        return hash_hmac( 'sha256', $ua, wp_salt( 'nonce' ) );
    }

    private static function redirect_error( $return_url, $code ) {
        wp_safe_redirect( add_query_arg( 'yatoon_identity_error', sanitize_key( $code ), self::safe_return_url( $return_url ) ) );
        exit;
    }

    private static function find_staff_for_otp( $identifier, $channel ) {
        global $wpdb;
        $allowed = array_map( 'absint', (array) get_option( 'yatoon_staff_otp_login_ids', array() ) );
        if ( ! $allowed ) { return null; }
        $rows = $wpdb->get_results( "SELECT id,name,email,phone FROM {$wpdb->prefix}yatoon_staff WHERE is_active=1" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $needle = 'email' === $channel ? strtolower( sanitize_email( $identifier ) ) : preg_replace( '/\D+/', '', $identifier );
        foreach ( $rows as $row ) {
            if ( ! in_array( (int) $row->id, $allowed, true ) ) { continue; }
            $value = 'email' === $channel ? strtolower( (string) $row->email ) : preg_replace( '/\D+/', '', (string) $row->phone );
            if ( $needle && hash_equals( $value, $needle ) ) { return $row; }
        }
        return null;
    }

    public static function ajax_send_staff_otp() {
        check_ajax_referer( 'yatoon_staff_portal_nonce', 'nonce' );
        $channel = 'sms' === sanitize_key( wp_unslash( $_POST['channel'] ?? '' ) ) ? 'sms' : 'email';
        $identifier = sanitize_text_field( wp_unslash( $_POST['identifier'] ?? '' ) );
        $staff = self::find_staff_for_otp( $identifier, $channel );
        $rate = YATOON_Request_Guard::consume_counter( 'ysp_otp_' . hash( 'sha256', self::fingerprint() . '|' . strtolower( $identifier ) ), self::OTP_TTL );
        if ( $rate > 5 ) { wp_send_json_error( array( 'message' => __( 'Too many code requests. Please wait and try again.', 'yatoon-booking-system' ) ) ); }
        if ( ! $staff ) { wp_send_json_error( array( 'message' => __( 'This sign-in method is not enabled for that employee.', 'yatoon-booking-system' ) ) ); }
        $code = sprintf( '%06d', random_int( 0, 999999 ) );
        $challenge = wp_generate_password( 40, false, false );
        set_transient( 'ysp_otp_challenge_' . hash( 'sha256', $challenge ), array( 'staff_id' => (int) $staff->id, 'hash' => wp_hash_password( $code ), 'fingerprint' => self::fingerprint() ), self::OTP_TTL );
        $sent = false;
        if ( 'sms' === $channel && class_exists( 'YATOON_Twilio' ) ) { $sent = (bool) YATOON_Twilio::send( $staff->phone, sprintf( __( 'Your %1$s staff sign-in code is %2$s. It expires in 10 minutes.', 'yatoon-booking-system' ), get_bloginfo( 'name' ), $code ) ); }
        if ( 'email' === $channel ) { $sent = wp_mail( $staff->email, sprintf( '[%s] Staff sign-in code', get_bloginfo( 'name' ) ), sprintf( "Your staff sign-in code is %s.\n\nIt expires in 10 minutes.", $code ) ); }
        if ( ! $sent ) { delete_transient( 'ysp_otp_challenge_' . hash( 'sha256', $challenge ) ); wp_send_json_error( array( 'message' => __( 'The sign-in code could not be delivered.', 'yatoon-booking-system' ) ) ); }
        wp_send_json_success( array( 'challenge' => $challenge, 'message' => __( 'A 6-digit sign-in code was sent.', 'yatoon-booking-system' ) ) );
    }

    public static function ajax_verify_staff_otp() {
        check_ajax_referer( 'yatoon_staff_portal_nonce', 'nonce' );
        $challenge = sanitize_text_field( wp_unslash( $_POST['challenge'] ?? '' ) );
        $code = preg_replace( '/\D+/', '', sanitize_text_field( wp_unslash( $_POST['code'] ?? '' ) ) );
        $key = 'ysp_otp_challenge_' . hash( 'sha256', $challenge );
        $data = get_transient( $key );
        $attempts = YATOON_Request_Guard::consume_counter( $key . '_attempts', self::OTP_TTL );
        if ( $attempts > 6 ) { delete_transient( $key ); wp_send_json_error( array( 'message' => __( 'Too many incorrect codes. Request a new code.', 'yatoon-booking-system' ) ) ); }
        if ( ! is_array( $data ) || ! hash_equals( (string) ( $data['fingerprint'] ?? '' ), self::fingerprint() ) || ! wp_check_password( $code, $data['hash'] ?? '' ) ) {
            wp_send_json_error( array( 'message' => __( 'Invalid or expired code.', 'yatoon-booking-system' ) ) );
        }
        delete_transient( $key );
        $payload = YATOON_Staff_Portal::login_payload_for_staff( absint( $data['staff_id'] ?? 0 ) );
        if ( is_wp_error( $payload ) ) { wp_send_json_error( array( 'message' => $payload->get_error_message() ) ); }
        wp_send_json_success( $payload );
    }

    public static function admin_menu() {
        add_submenu_page( 'yatoon-booking-system', __( 'Identity & Sign-in', 'yatoon-booking-system' ), __( 'Identity & Sign-in', 'yatoon-booking-system' ), 'manage_options', 'yatoon-identity', array( __CLASS__, 'render_admin' ) );
    }

    public static function save_settings() {
        if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'yatoon-booking-system' ) ); }
        check_admin_referer( 'yatoon_save_identity_settings' );
        foreach ( array( 'yatoon_google_identity_enabled', 'yatoon_customer_google_login_enabled', 'yatoon_staff_google_login_enabled' ) as $key ) { update_option( $key, isset( $_POST[ $key ] ) ? '1' : '0', false ); }
        update_option( 'yatoon_google_identity_client_id', sanitize_text_field( wp_unslash( $_POST['yatoon_google_identity_client_id'] ?? '' ) ), false );
        $secret = trim( (string) wp_unslash( $_POST['yatoon_google_identity_client_secret'] ?? '' ) );
        if ( $secret && class_exists( 'YATOON_Crypto' ) ) { YATOON_Crypto::update_option( 'yatoon_google_identity_client_secret', $secret ); }
        if ( class_exists( 'YATOON_Apple_Identity' ) ) { YATOON_Apple_Identity::save_settings_from_request(); }
        if ( class_exists( 'YATOON_Facebook_Identity' ) ) { YATOON_Facebook_Identity::save_settings_from_request(); }
        $staff_google_ids = array_values( array_unique( array_map( 'absint', (array) ( $_POST['staff_google'] ?? array() ) ) ) );
        update_option( 'yatoon_staff_google_login_ids', $staff_google_ids, false );
        update_option( 'yatoon_staff_otp_login_ids', array_values( array_unique( array_map( 'absint', (array) ( $_POST['staff_otp'] ?? array() ) ) ) ), false );
        global $wpdb;
        if ( $staff_google_ids ) {
            $ids = implode( ',', array_map( 'absint', $staff_google_ids ) );
            $wpdb->query( "DELETE FROM {$wpdb->prefix}yatoon_identity_links WHERE account_type='staff' AND provider='google' AND account_id NOT IN ({$ids})" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery,WordPress.DB.PreparedSQL.NotPrepared
        } else {
            $wpdb->delete( $wpdb->prefix . 'yatoon_identity_links', array( 'account_type' => 'staff', 'provider' => 'google' ), array( '%s', '%s' ) );
        }
        wp_safe_redirect( add_query_arg( array( 'page' => 'yatoon-identity', 'updated' => '1' ), admin_url( 'admin.php' ) ) ); exit;
    }

    public static function unlink_identity() {
        if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'Permission denied.', 'yatoon-booking-system' ) ); }
        $id = absint( $_GET['identity_id'] ?? 0 );
        check_admin_referer( 'yatoon_unlink_identity_' . $id );
        global $wpdb;
        $link = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}yatoon_identity_links WHERE id=%d LIMIT 1", $id ) );
        if ( $link ) {
            $wpdb->delete( $wpdb->prefix . 'yatoon_identity_links', array( 'id' => $id ), array( '%d' ) );
            if ( class_exists( 'YATOON_Insights' ) ) { YATOON_Insights::audit( 'identity_unlinked', $link->account_type . ' ' . $link->provider . ' identity unlinked', $link->account_type, absint( $link->account_id ) ); }
        }
        wp_safe_redirect( add_query_arg( 'page', 'yatoon-identity', admin_url( 'admin.php' ) ) ); exit;
    }

    public static function render_admin() {
        if ( ! current_user_can( 'manage_options' ) ) { return; }
        global $wpdb;
        $staff = $wpdb->get_results( "SELECT id,name,email,phone,is_active FROM {$wpdb->prefix}yatoon_staff ORDER BY name" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $google_ids = array_map( 'absint', (array) get_option( 'yatoon_staff_google_login_ids', array() ) );
        $otp_ids = array_map( 'absint', (array) get_option( 'yatoon_staff_otp_login_ids', array() ) );
        $links = $wpdb->get_results( "SELECT id,account_type,account_id,provider,verified_email,last_login_at FROM {$wpdb->prefix}yatoon_identity_links ORDER BY last_login_at DESC LIMIT 100" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery
        $google_ready = self::client_id() && self::client_secret();
        $apple_ready = class_exists( 'YATOON_Apple_Identity' ) && YATOON_Apple_Identity::ready();
        $facebook_ready = class_exists( 'YATOON_Facebook_Identity' ) && YATOON_Facebook_Identity::ready();
        ?>
        <div class="wrap yatoon-identity-page"><h1><?php esc_html_e( 'Identity & Sign-in', 'yatoon-booking-system' ); ?></h1>
        <style>
        .yatoon-identity-guide{max-width:1040px;margin:14px 0;border:1px solid #cbd5e1;border-radius:12px;background:#fff;box-shadow:0 1px 3px rgba(15,23,42,.06);overflow:hidden}.yatoon-identity-guide summary{cursor:pointer;padding:14px 16px;color:#0f172a;background:#f8fafc;font-weight:700}.yatoon-identity-guide[open] summary{border-bottom:1px solid #e2e8f0}.yatoon-identity-guide-body{padding:16px 18px}.yatoon-identity-guide-body ol{margin:8px 0 14px 22px}.yatoon-identity-guide-body li{margin:0 0 9px;line-height:1.5}.yatoon-identity-guide-actions{display:flex;flex-wrap:wrap;gap:8px;margin:12px 0}.yatoon-identity-copy{display:flex;align-items:center;gap:8px;max-width:820px}.yatoon-identity-copy input{width:min(100%,680px);font-family:ui-monospace,SFMono-Regular,Consolas,monospace}.yatoon-identity-tip{padding:10px 12px;border-left:4px solid #2271b1;background:#eff6ff;color:#1e3a8a}.yatoon-identity-warning{padding:10px 12px;border-left:4px solid #dba617;background:#fff8e5;color:#664d03}@media(max-width:782px){.yatoon-identity-copy{align-items:stretch;flex-direction:column}.yatoon-identity-copy input,.yatoon-identity-copy .button{width:100%}}
        </style>
        <p><?php esc_html_e( 'Customer sign-in supports Google, Apple, Facebook, email codes, and text-message codes in both Local and Square booking modes.', 'yatoon-booking-system' ); ?></p>
        <div class="notice <?php echo $google_ready && self::google_enabled( 'customer' ) ? 'notice-success' : 'notice-warning'; ?> inline"><p><strong><?php echo $google_ready && self::google_enabled( 'customer' ) ? esc_html__( 'Customer Google sign-in is ready.', 'yatoon-booking-system' ) : esc_html__( 'Google sign-in is not visible yet.', 'yatoon-booking-system' ); ?></strong> <?php esc_html_e( 'Google appears on the My Appointments sign-in page after identity is enabled and both OAuth credentials are saved.', 'yatoon-booking-system' ); ?></p></div>
        <div class="notice <?php echo $apple_ready && YATOON_Apple_Identity::enabled() ? 'notice-success' : 'notice-info'; ?> inline"><p><strong><?php echo $apple_ready && YATOON_Apple_Identity::enabled() ? esc_html__( 'Customer Apple sign-in is ready.', 'yatoon-booking-system' ) : esc_html__( 'Apple sign-in is optional and not visible yet.', 'yatoon-booking-system' ); ?></strong> <?php esc_html_e( 'Configure the Services ID, Team ID, Key ID, private key, and exact HTTPS return URL below.', 'yatoon-booking-system' ); ?></p></div>
        <div class="notice <?php echo $facebook_ready && YATOON_Facebook_Identity::enabled() ? 'notice-success' : 'notice-info'; ?> inline"><p><strong><?php echo $facebook_ready && YATOON_Facebook_Identity::enabled() ? esc_html__( 'Customer Facebook Login is ready.', 'yatoon-booking-system' ) : esc_html__( 'Facebook Login is optional and not visible yet.', 'yatoon-booking-system' ); ?></strong> <?php esc_html_e( 'There is no per-login fee; configure a Meta app and its exact OAuth redirect URI below.', 'yatoon-booking-system' ); ?></p></div>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
        <input type="hidden" name="action" value="yatoon_save_identity_settings"><?php wp_nonce_field( 'yatoon_save_identity_settings' ); ?>
        <table class="form-table"><tr><th><?php esc_html_e( 'Google sign-in', 'yatoon-booking-system' ); ?></th><td><label><input type="checkbox" name="yatoon_google_identity_enabled" value="1" <?php checked( get_option( 'yatoon_google_identity_enabled', '0' ), '1' ); ?>> <?php esc_html_e( 'Enable Google identity', 'yatoon-booking-system' ); ?></label><br><label><input type="checkbox" name="yatoon_customer_google_login_enabled" value="1" <?php checked( get_option( 'yatoon_customer_google_login_enabled', '1' ), '1' ); ?>> <?php esc_html_e( 'Allow customer Google sign-in', 'yatoon-booking-system' ); ?></label><br><label><input type="checkbox" name="yatoon_staff_google_login_enabled" value="1" <?php checked( get_option( 'yatoon_staff_google_login_enabled', '1' ), '1' ); ?>> <?php esc_html_e( 'Allow authorized staff Google sign-in', 'yatoon-booking-system' ); ?></label>
        <details class="yatoon-identity-guide" <?php echo $google_ready ? '' : 'open'; ?>><summary><?php esc_html_e( 'Google setup guide — where to apply and what to enter', 'yatoon-booking-system' ); ?></summary><div class="yatoon-identity-guide-body">
        <div class="yatoon-identity-guide-actions"><a class="button button-primary" href="https://console.cloud.google.com/auth/clients" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Open Google Cloud Clients', 'yatoon-booking-system' ); ?> ↗</a><a class="button" href="https://developers.google.com/identity/openid-connect/openid-connect" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Official Google instructions', 'yatoon-booking-system' ); ?> ↗</a></div>
        <ol><li><?php esc_html_e( 'Create or select a Google Cloud project. Under Google Auth Platform, complete Branding and Audience. If the app is still in Testing, add the Google accounts that will test it.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Open Clients, choose Create client, and select Web application. Do not create a Desktop, Android, or iOS client.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Under Authorized redirect URIs, paste the exact URL below. The scheme, domain, path, and trailing slash must match exactly.', 'yatoon-booking-system' ); ?></li><li><?php esc_html_e( 'Copy the generated Client ID and Client Secret into the fields below, enable Google identity and customer sign-in, then save changes.', 'yatoon-booking-system' ); ?></li></ol>
        <div class="yatoon-identity-copy"><input type="text" readonly value="<?php echo esc_attr( self::callback_url() ); ?>" aria-label="<?php esc_attr_e( 'Google authorized redirect URI', 'yatoon-booking-system' ); ?>"><button type="button" class="button yatoon-copy-identity-value"><?php esc_html_e( 'Copy redirect URI', 'yatoon-booking-system' ); ?></button></div>
        <p class="yatoon-identity-tip"><strong><?php esc_html_e( 'Most common error:', 'yatoon-booking-system' ); ?></strong> <?php esc_html_e( 'redirect_uri_mismatch means the URI saved in Google Cloud is not character-for-character identical to the URI above.', 'yatoon-booking-system' ); ?></p>
        </div></details></td></tr>
        <tr><th>Google OAuth Client ID</th><td><input class="regular-text" name="yatoon_google_identity_client_id" value="<?php echo esc_attr( self::client_id() ); ?>"><p class="description"><?php esc_html_e( 'Create a Web application OAuth client in Google Cloud and add the exact redirect URI shown below.', 'yatoon-booking-system' ); ?></p></td></tr>
        <tr><th>Google OAuth Client Secret</th><td><input class="regular-text" type="password" name="yatoon_google_identity_client_secret" value="" autocomplete="new-password"><p class="description"><?php esc_html_e( 'Leave blank to keep the saved secret. The existing Google Calendar credential is used as a fallback.', 'yatoon-booking-system' ); ?></p></td></tr>
        <tr><th><?php esc_html_e( 'Authorized redirect URI', 'yatoon-booking-system' ); ?></th><td><code><?php echo esc_html( self::callback_url() ); ?></code></td></tr>
        <?php if ( class_exists( 'YATOON_Apple_Identity' ) ) { YATOON_Apple_Identity::render_settings_rows(); } ?>
        <?php if ( class_exists( 'YATOON_Facebook_Identity' ) ) { YATOON_Facebook_Identity::render_settings_rows(); } ?>
        </table>
        <h2><?php esc_html_e( 'Employee authorization', 'yatoon-booking-system' ); ?></h2><table class="widefat striped"><thead><tr><th><?php esc_html_e( 'Employee', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Google', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Email / SMS code', 'yatoon-booking-system' ); ?></th></tr></thead><tbody>
        <?php foreach ( $staff as $member ) : ?><tr><td><?php echo esc_html( $member->name ); ?><br><small><?php echo esc_html( $member->email . ( $member->phone ? ' · ' . $member->phone : '' ) ); ?></small></td><td><input type="checkbox" name="staff_google[]" value="<?php echo absint( $member->id ); ?>" <?php checked( in_array( (int) $member->id, $google_ids, true ) ); ?> <?php disabled( ! $member->is_active || ! is_email( $member->email ) ); ?>></td><td><input type="checkbox" name="staff_otp[]" value="<?php echo absint( $member->id ); ?>" <?php checked( in_array( (int) $member->id, $otp_ids, true ) ); ?> <?php disabled( ! $member->is_active || ( ! is_email( $member->email ) && ! $member->phone ) ); ?>></td></tr><?php endforeach; ?>
        </tbody></table><?php submit_button(); ?></form>
        <h2><?php esc_html_e( 'Linked identities', 'yatoon-booking-system' ); ?></h2><table class="widefat striped"><thead><tr><th><?php esc_html_e( 'Type', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Provider', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Account', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Verified email', 'yatoon-booking-system' ); ?></th><th><?php esc_html_e( 'Last sign-in', 'yatoon-booking-system' ); ?></th><th></th></tr></thead><tbody><?php if ( ! $links ) : ?><tr><td colspan="6"><?php esc_html_e( 'No identities linked yet.', 'yatoon-booking-system' ); ?></td></tr><?php endif; ?><?php foreach ( $links as $link ) : ?><tr><td><?php echo esc_html( $link->account_type ); ?></td><td><?php echo esc_html( ucfirst( $link->provider ) ); ?></td><td>#<?php echo absint( $link->account_id ); ?></td><td><?php echo esc_html( $link->verified_email ); ?></td><td><?php echo esc_html( $link->last_login_at ); ?></td><td><a class="button" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=yatoon_unlink_identity&identity_id=' . absint( $link->id ) ), 'yatoon_unlink_identity_' . absint( $link->id ) ) ); ?>"><?php esc_html_e( 'Unlink', 'yatoon-booking-system' ); ?></a></td></tr><?php endforeach; ?></tbody></table>
        <script>document.addEventListener('click',function(e){var b=e.target.closest('.yatoon-copy-identity-value');if(!b)return;var i=b.parentNode.querySelector('input');if(!i)return;var done=function(){var old=b.textContent;b.textContent='<?php echo esc_js( __( 'Copied!', 'yatoon-booking-system' ) ); ?>';setTimeout(function(){b.textContent=old;},1400);};if(navigator.clipboard&&window.isSecureContext){navigator.clipboard.writeText(i.value).then(done);return;}i.focus();i.select();try{document.execCommand('copy');done();}catch(err){}});</script>
        </div><?php
    }
}
