<?php
// phpcs:ignoreFile WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
// phpcs:ignoreFile WordPress.PHP.DevelopmentFunctions.error_log_print_r

class SLN_Wrapper_Booking_Builder
{
    protected $plugin;
    protected $data;
    protected $lastId;
    /** @var SLN_Service_BookingPersistence */
    protected $persistence;
    /** @var string|null */
    protected $clientId;

    public function __construct(SLN_Plugin $plugin)
    {
        SLN_Helper_Session::maybeStart(true);
        $this->plugin = $plugin;
        $clientId      = $this->extractClientIdFromRequest();
        $this->persistence = new SLN_Service_BookingPersistence(__CLASS__, __CLASS__ . 'last_id', $clientId);
        $initialState      = $this->persistence->load($this->getEmptyValue());
        $this->data   = $initialState['data'];
        $this->lastId = $initialState['last_id'];
        $this->clientId = $this->persistence->ensureClientId();
    }

    public function save()
    {
        $this->persistence->save($this->data, $this->lastId);
    }

    public function clear($id = null, $empty = true)
    {
        if ($empty) {
            $this->emptyData();
        }
        $this->lastId = $id;
        $this->persistence->save($this->data, $this->lastId);
    }

    public function emptyData()
    {
        $this->data = $this->getEmptyValue();
        $this->persistence->save($this->data, $this->lastId);
    }

    /**
     * @return $this
     */
    public function removeLastId()
    {
        $this->persistence->removeLastId();
        $this->lastId = null;

        return $this;
    }

    /**
     * @return SLN_Wrapper_Booking
     */
    public function getLastBooking()
    {
        if ($this->lastId) {
            return $this->plugin->createBooking($this->lastId);
        }
    }

    public function getClientId()
    {
        return $this->clientId;
    }

    public function forceTransientStorage()
    {
        if (method_exists($this->persistence, 'switchToTransient')) {
            $this->clientId = $this->persistence->switchToTransient($this->data, $this->lastId);
        }
        return $this->clientId;
    }

    public function isUsingTransient()
    {
        return $this->persistence->isUsingTransient();
    }

    /**
     * Get last booking or throw exception if not found
     * Use this method in critical paths where null booking would cause problems
     * 
     * @return SLN_Wrapper_Booking
     * @throws Exception if booking is not found
     */
    public function getLastBookingOrFail()
    {
        $booking = $this->getLastBooking();
        
        if (empty($booking)) {
            SLN_Plugin::addLog("ERROR: getLastBooking returned null/empty");
            SLN_Plugin::addLog("lastId: " . var_export($this->lastId, true));
            SLN_Plugin::addLog("Session exists: " . (isset($_SESSION) ? 'yes' : 'no'));
            SLN_Plugin::addLog("Session status: " . session_status());
            throw new Exception(__('Booking data not found. Your session may have expired. Please start the booking process again.', 'salon-booking-system'));
        }
        
        return $booking;
    }

    public function getEmptyValue()
    {
        $from = $this->plugin->getSettings()->getHoursBeforeFrom();
        $d = new SLN_DateTime(SLN_TimeFunc::date('Y-m-d H:i:00'));
        $d->modify($from);
        $d = SLN_Func::alignDateTimeToInterval($d, SLN_Plugin::getInstance()->getSettings()->getInterval());

        return array(
            'date' => $d->format('Y-m-d'),
            'time' => $d->format('H:i'),
            'services' => array(),
        );
    }

    public function get($k)
    {
        return isset($this->data[$k]) ? $this->data[$k] : null;
    }

    public function set($key, $val)
    {
        if (empty($val)) {
            unset($this->data[$key]);
        } else {
            $this->data[$key] = $val;
        }
    }

    public function getDate()
    {
        return $this->data['date'];
    }

    public function getTime()
    {
        return $this->data['time'];
    }

    public function getDateTime()
    {
        $ret = new SLN_DateTime($this->getDate().' '.$this->getTime());

        return $ret;
    }

    public function setDate($date)
    {
        $this->data['date'] = $date;

        return $this;
    }

    public function setTime($time)
    {
        $this->data['time'] = SLN_Func::filter($time, 'time');

        return $this;
    }

    public function setAttendant(SLN_Wrapper_AttendantInterface $attendant, SLN_Wrapper_ServiceInterface $service)
    {
        if ($this->hasService($service)) {
            $this->data['services'][$service->getId()] = $attendant->getId();
        }
    }

    /**
     * Set multiple attendants for a service (for services requiring multiple attendants)
     * @param array $attendantIds Array of attendant IDs
     * @param SLN_Wrapper_ServiceInterface $service
     */
    public function setAttendants(array $attendantIds, SLN_Wrapper_ServiceInterface $service)
    {
        if ($this->hasService($service)) {
            $this->data['services'][$service->getId()] = $attendantIds;
        }
    }

    public function hasAttendant(SLN_Wrapper_AttendantInterface $attendant, SLN_Wrapper_ServiceInterface $service = null)
    {
        if (!isset($this->data['services'])) {
            return false;
        }

        if (is_null($service)) {
            return in_array($attendant->getId(), $this->data['services']);
        } else {
            return isset($this->data['services'][$service->getId()]) && $this->data['services'][$service->getId(
            )] == $attendant->getId();
        }
    }

    public function removeAttendants()
    {
        $this->data['services'] = array_fill_keys(array_keys($this->data['services']), 0);
    }


    public function hasService(SLN_Wrapper_ServiceInterface $service)
    {
        return in_array($service->getId(), array_keys($this->data['services']));
    }

    public function getAttendantsIds()
    {
        return $this->data['services'];
    }

    public function getShopsIds()
    {
        return $this->data['shop'];
    }

    private function extractClientIdFromRequest()
    {
        $clientId = '';

        if (isset($_REQUEST['sln_client_id'])) {
            $clientId = $this->sanitizeClientId($_REQUEST['sln_client_id']);
        } elseif (!empty($_SERVER['HTTP_X_SLN_CLIENT_ID'])) {
            $clientId = $this->sanitizeClientId($_SERVER['HTTP_X_SLN_CLIENT_ID']);
        }

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

        return substr($clientId, 0, 64);
    }

    private function sanitizeClientId($value)
    {
        if (function_exists('wp_unslash')) {
            $value = wp_unslash($value);
        }

        if (function_exists('sanitize_text_field')) {
            $value = sanitize_text_field($value);
        } else {
            $value = preg_replace('/[^a-zA-Z0-9_-]/', '', $value);
        }

        return $value;
    }

    /**
     * @return SLN_Wrapper_AttendantInterface|false
     */
    public function getAttendant()
    {
        $atts = $this->getAttendants();

        return reset($atts);
    }

    /**
     * @return SLN_Wrapper_AttendantInterface[]
     */
    public function getAttendants($unique=false)
    {
        $ids = $this->getAttendantsIds();
        $ret = array();
        if($unique){
            $ids = array_unique($ids, SORT_NUMERIC);
        }
        foreach ($ids as $service_id => $attendant_id) {
            if ($attendant_id) {
                $ret[$service_id] = $this->plugin->createAttendant($attendant_id);
            }
        }

        return $ret;
    }

    public function setServicesAndAttendants($data) {
        // DEBUG: Log when services are set
        SLN_Plugin::addLog('[BookingBuilder] setServicesAndAttendants called with: ' . print_r($data, true));
        $this->data['services'] = $data;
    }

    public function addService(SLN_Wrapper_ServiceInterface $service)
    {
        if ((!isset($this->data['services'])) || (!in_array($service->getId(), array_keys($this->data['services'])))) {
            $this->data['services'][$service->getId()] = 0;
            uksort($this->data['services'], array('SLN_Repository_ServiceRepository', 'serviceCmp'));
        }
    }

    public function removeService(SLN_Wrapper_ServiceInterface $service)
    {
        if (isset($this->data['services'])) {
            unset($this->data['services'][$service->getId()]);
        }
    }

    public function clearService(SLN_Wrapper_ServiceInterface $service)
    {
        if (isset($this->data['services'][$service->getId()])) {
            $this->data['services'][$service->getId()] = 0;
        }
    }

    public function removeServices()
    {
        $this->data['services'] = array();
    }

    public function getServicesIds()
    {
        return array_keys($this->getServices());
    }

    public function getPrimaryServicesIds()
    {
        return array_keys($this->getPrimaryServices());
    }

    public function getSecondaryServicesIds()
    {
        return array_keys($this->getSecondaryServices());
    }

    /**
     * @param bool $primary
     * @param bool $secondary
     *
     * @return SLN_Wrapper_ServiceInterface[]
     */
    public function getServices($primary = true, $secondary = true)
    {
        $ids = array_keys($this->data['services']);
        $ret = array();
        /** @var SLN_Repository_ServiceRepository $repo */
        $repo = $this->plugin->getRepository(SLN_Plugin::POST_TYPE_SERVICE);
        $services = $repo->getAll();

        foreach ($services as $service) {
            if (in_array($service->getId(), $ids)) {
                if ($secondary && $service->isSecondary()) {
                    $ret[$service->getId()] = $service;
                } elseif ($primary && !$service->isSecondary()) {
                    $ret[$service->getId()] = $service;
                }
            }
        }

        return apply_filters('sln.booking_builder.getServices',$ret);
    }

    public function getPrimaryServices()
    {
        return $this->getServices(true, false);
    }

    public function getSecondaryServices()
    {
        return $this->getServices(false, true);
    }

    public function getTotal()
    {
        $ret = 0;
        foreach ($this->getServices() as $s) {
            $attendantID = isset($this->getAttendantsIds()[$s->getId()]) ? $this->getAttendantsIds()[$s->getId()] : null;
            $price           = $s->getVariablePriceEnabled() && $s->getVariablePrice($attendantID) !== '' ? $s->getVariablePrice($attendantID) : $s->getPrice();

            $price = $price * $this->getCountService($s->getId());

            $ret = $ret + SLN_Func::filter($price, 'float');
        }

        $settings = SLN_Plugin::getInstance()->getSettings();
        if ($settings->get('enable_booking_tax_calculation') && 'inclusive' !== $settings->get('enter_tax_price')) {
            $ret = $ret * (1 + floatval($settings->get('tax_value')) / 100);
        }

        // Tips after tax, matching SLN_Wrapper_Booking::evalTotal() and CalcBookingTotal (exclusive tax).
        $ret += SLN_Func::filter($this->getTips(), 'float');

        $ret = apply_filters('sln.booking_builder.getTotal', $ret, $this);

        return SLN_Func::filter($ret, 'float');
    }

    public function create($bookingStatus = '', $clear = true)
    {
        $settings             = $this->plugin->getSettings();
        $datetime             = $this->plugin->format()->datetime($this->getDateTime());
        
        // CRITICAL DEFENSIVE CHECK: Validate required customer data exists before creating booking
        // This prevents bookings without customer information (regression bug from Feb 5, 2026)
        $this->validateAndEnsureCustomerData();

        $name                 = $this->get('firstname') . ' ' . $this->get('lastname');
        $status               = $bookingStatus ? $bookingStatus : $this->getCreateStatus();

	// DEBUG: Log what status we're trying to set
	SLN_Plugin::addLog(sprintf(
	    'Builder::create() - Creating booking with status: %s (bookingStatus param: %s, getCreateStatus: %s)',
	    $status,
	    $bookingStatus ? $bookingStatus : 'null',
	    $this->getCreateStatus()
	));

	$args = array(
	    'post_type' => SLN_Plugin::POST_TYPE_BOOKING,
	    'post_title' => $name.' - '.$datetime,
	    'post_status' => $status, // CRITICAL FIX: Explicitly set status to prevent auto-draft default
	);

	$args = apply_filters('sln.booking_builder.create.getPostArgs', $args);

	// DEBUG: Log args after filter to see if post_status was removed
	SLN_Plugin::addLog(sprintf(
	    'Builder::create() - After getPostArgs filter: post_status=%s',
	    isset($args['post_status']) ? $args['post_status'] : 'NOT_SET'
	));

	$id = wp_insert_post($args);

	// Error handling for wp_insert_post failure
	if (is_wp_error($id)) {
	    $error_msg = $id->get_error_message();
	    SLN_Plugin::addLog("ERROR: wp_insert_post failed: " . $error_msg);
	    
	    // Send error notification to support
	    if (class_exists('SLN_Helper_ErrorNotification')) {
	        SLN_Helper_ErrorNotification::send(
	            'BOOKING_CREATION_FAILED',
	            'wp_insert_post returned WP_Error: ' . $error_msg,
	            "Booking details:\nName: {$name}\nDateTime: {$datetime}\nStatus: {$status}"
	        );
	    }
	    
	    throw new Exception(__('Unable to create booking: ', 'salon-booking-system') . $error_msg);
	}

	if (!$id || $id === 0) {
	    SLN_Plugin::addLog("ERROR: wp_insert_post returned invalid ID: " . var_export($id, true));
	    
	    // Send error notification to support
	    if (class_exists('SLN_Helper_ErrorNotification')) {
	        SLN_Helper_ErrorNotification::send(
	            'BOOKING_CREATION_FAILED',
	            'wp_insert_post returned invalid ID (0 or false)',
	            "Booking details:\nName: {$name}\nDateTime: {$datetime}\nStatus: {$status}"
	        );
	    }
	    
	    throw new Exception(__('Unable to create booking. Please try again or contact the website administrator.', 'salon-booking-system'));
	}

	SLN_Plugin::addLog(sprintf(
	    "Booking post created successfully with ID: %d, initial status: %s",
	    $id,
	    $status
	));

	// CRITICAL FIX: Set lastId BEFORE 'sln.booking_builder.create' hook fires
	// This ensures extensions calling getLastBooking() in their hook handlers receive valid booking
	// Previously lastId was set AFTER hook (line 469), causing extensions to receive NULL
	// Root cause of: FATAL_ERROR: Call to a member function getStartsAt() on null
	// Extensions affected: SLB_Discount, SalonMultishop, and any hooking into this action
	// Related docs: FIX_NULL_BOOKING_REFERENCE.md, CRITICAL_FIX_MULTISHOP_GETLASTBOOKING_ERROR.md
	$this->lastId = $id;

        do_action('sln.booking_builder.create', $this);

	if ($status === SLN_Enum_BookingStatus::PENDING_PAYMENT && $settings->get('disable_first_pending_payment_email_to_customer')) {
            update_post_meta($id, '_'.SLN_Plugin::POST_TYPE_BOOKING.'_disable_status_change_email', 1);
	}

        // DEBUG: Log booking data being saved
        SLN_Plugin::addLog('[BookingBuilder] Saving booking data to post meta. Data keys: ' . implode(', ', array_keys($this->data)));
        SLN_Plugin::addLog('[BookingBuilder] Services in data: ' . print_r($this->get('services'), true));
        
        foreach ($this->data as $k => $v) {
            update_post_meta($id, '_'.SLN_Plugin::POST_TYPE_BOOKING.'_'.$k, $v);
        }

        // Capture device / IP info at booking creation time (stored once, never overwritten).
        // Skipped for admin-created bookings and REST API requests where no real browser UA is present.
        $is_admin_create = is_admin() && !( defined('DOING_AJAX') && DOING_AJAX );
        $is_rest         = defined('REST_REQUEST') && REST_REQUEST;
        if ( ! $is_admin_create && ! $is_rest ) {
            $user_agent = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';
            $ip_address = $this->getClientIp();
            update_post_meta( $id, '_sln_booking_device_info', array(
                'user_agent'  => $user_agent,
                'ip'          => $ip_address,
                'parsed'      => $this->parseUserAgent( $user_agent ),
                'captured_at' => current_time( 'mysql' ),
            ) );
        }

        $discounts = $this->get('discounts');
        $this->clear($id, $clear);
        
        // Ensure cache is cleared so evalBookingServices and email templates read fresh meta.
        // Critical for PWA/API-created bookings where object cache can serve stale data.
        clean_post_cache($id);
        
        // Use getLastBookingOrFail() to ensure we have a valid booking object
        // This prevents null reference errors in extensions that hook into these actions
        $lastBooking = $this->getLastBookingOrFail();
        
        do_action('sln.api.booking.pre_eval', $lastBooking, $discounts);
        $lastBooking->evalBookingServices();
        $lastBooking->evalTotal();
        $lastBooking->evalDuration();
        
        // IMPORTANT: Even though we set post_status during wp_insert_post(),
        // we still call setStatus() here to ensure:
        // 1. The 'transition_post_status' hook fires properly
        // 2. Any status-dependent logic in setStatus() runs
        // 3. Logging and audit trail is complete
        // Since status is already set, this should be a no-op for the actual status change
        $lastBooking->setStatus($status);

        // CRITICAL FIX: For new posts, transition_post_status fires during wp_insert_post
        // BEFORE meta is saved (services, date, time, amount). Sending email there produces
        // notifications with blank data. Send here after meta is saved and evaluated.
        $this->plugin->messages()->sendByStatus($lastBooking, $status);
        do_action('sln.booking_builder.create.booking_created', $lastBooking);
        do_action('sln.booking_builder.new_booking_ready', $lastBooking);

        $userid = $lastBooking->getUserId();
        if ($userid) {
            $user = new WP_User($userid);
            if (array_search('administrator', $user->roles) === false && array_search(
	                'subscriber',
	                $user->roles
	            ) !== false
            ) {
                wp_update_user(
                    array(
                        'ID' => $userid,
                        'role' => SLN_Plugin::USER_ROLE_CUSTOMER,
                    )
                );
            }
        }
        $this->plugin->getBookingCache()->processBooking($lastBooking, true);

    }

    /**
     * Return the real client IP address, checking CDN / proxy headers first.
     *
     * @return string IP address or empty string.
     */
    private function getClientIp()
    {
        $headers = array(
            'HTTP_CF_CONNECTING_IP', // Cloudflare
            'HTTP_X_FORWARDED_FOR',  // Most reverse proxies
            'HTTP_X_REAL_IP',
            'REMOTE_ADDR',
        );
        foreach ( $headers as $header ) {
            if ( ! empty( $_SERVER[ $header ] ) ) {
                $ip = sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) );
                // X-Forwarded-For can be a comma-separated list; take the first
                $ip = trim( explode( ',', $ip )[0] );
                if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) {
                    return $ip;
                }
            }
        }
        return '';
    }

    /**
     * Parse a User-Agent string into platform, OS, and browser labels.
     *
     * @param  string $ua Raw User-Agent string.
     * @return array  Keys: platform, os, browser (all optional).
     */
    private function parseUserAgent( $ua )
    {
        if ( empty( $ua ) ) {
            return array( 'platform' => 'Unknown' );
        }

        $parsed = array();

        // Platform / device type
        if ( preg_match( '/iPad/i', $ua ) ) {
            $parsed['platform'] = 'Tablet';
            $parsed['os']       = 'iPadOS';
        } elseif ( preg_match( '/iPhone/i', $ua ) ) {
            $parsed['platform'] = 'Mobile';
            $parsed['os']       = 'iOS';
        } elseif ( preg_match( '/Android.*Mobile/i', $ua ) ) {
            $parsed['platform'] = 'Mobile';
            $parsed['os']       = 'Android';
        } elseif ( preg_match( '/Android/i', $ua ) ) {
            $parsed['platform'] = 'Tablet';
            $parsed['os']       = 'Android';
        } elseif ( preg_match( '/Windows Phone/i', $ua ) ) {
            $parsed['platform'] = 'Mobile';
            $parsed['os']       = 'Windows Phone';
        } elseif ( preg_match( '/Windows/i', $ua ) ) {
            $parsed['platform'] = 'Desktop';
            $parsed['os']       = 'Windows';
        } elseif ( preg_match( '/Macintosh|Mac OS X/i', $ua ) ) {
            $parsed['platform'] = 'Desktop';
            $parsed['os']       = 'macOS';
        } elseif ( preg_match( '/Linux/i', $ua ) ) {
            $parsed['platform'] = 'Desktop';
            $parsed['os']       = 'Linux';
        } else {
            $parsed['platform'] = 'Unknown';
        }

        // Browser — order matters: Edge/Opera before Chrome, Chrome before Safari
        if ( preg_match( '/Edg\//i', $ua ) ) {
            $parsed['browser'] = 'Edge';
        } elseif ( preg_match( '/OPR\//i', $ua ) ) {
            $parsed['browser'] = 'Opera';
        } elseif ( preg_match( '/Chrome\//i', $ua ) ) {
            $parsed['browser'] = 'Chrome';
        } elseif ( preg_match( '/Firefox\//i', $ua ) ) {
            $parsed['browser'] = 'Firefox';
        } elseif ( preg_match( '/Safari\//i', $ua ) ) {
            $parsed['browser'] = 'Safari';
        }

        return $parsed;
    }

    private function getCreateStatus()
    {
        $settings = $this->plugin->getSettings();
        $is_api_request = defined('REST_REQUEST') && REST_REQUEST;
        if($settings->isPayEnabled() /*&& $settings->get('create_booking_after_pay')*/ && !$is_api_request){
            return SLN_Enum_BookingStatus::DRAFT;
        }

        $status = $settings->get('confirmation') ?
            SLN_Enum_BookingStatus::PENDING
            : ($settings->isPayEnabled() && $this->getTotal() > 0 ?
                SLN_Enum_BookingStatus::PENDING_PAYMENT
                : SLN_Enum_BookingStatus::CONFIRMED);

        return apply_filters('sln.booking_builder.getCreateStatus', $status);
    }

    public function getEndsAt()
    {
        $endsAt = clone $this->getDateTime();
        $endsAt->modify("+".SLN_Func::getMinutesFromDuration($this->getDuration())."minutes");

        return $endsAt;
    }

    public function getDuration()
    {
        $i = $this->getServicesDurationMinutes();
        $str = SLN_Func::convertToHoursMins($i);
        return $str;
    }

    public function getServicesDurationMinutes()
    {
        $h = 0;
        $i = 0;
		$max = 0;
        foreach ($this->getServices() as $s) {
	        $d = $s->getTotalDuration();
			// Multiply by quantity so the wizard duration matches the saved booking
			// (SLN_Wrapper_Booking::evalDuration, which also multiplies by count).
			$dInMinutes = SLN_Func::getMinutesFromDuration($d) * $this->getCountService($s->getId());
			if ($s->isExecutionParalleled()) {
				if ($dInMinutes > $max) {
					$max = $dInMinutes;
				}
			} else {
				$i += $dInMinutes;
			}
        }
		$i += $max;

        return $i;
    }

    /**
     * @return SLN_Wrapper_Booking_Services
     */
    public function getBookingServices()
    {
        return SLN_Wrapper_Booking_Services::build(
            $this->getAttendantsIds(),
            $this->getDateTime(),
            0,
            $this->getCountServices()
        );
    }

    public function isValid()
    {
	SLN_Plugin::addLog('SLN booking date/time: ' . $this->getDate() . ' ' . $this->getTime());
	SLN_Plugin::addLog('SLN booking services: ' . print_r($this->data['services'], true));
	SLN_Plugin::addLog('SLN booking settings: ' . print_r(array(
	    'attendant_enabled' => $this->plugin->getSettings()->isAttendantsEnabled(),
	), true));

        if ($this->getTime() && !SLN_Func::isTimeAlignedToInterval($this->getTime())) {
            SLN_Plugin::addLog('SLN booking time not aligned to interval: ' . $this->getTime());
            return false;
        }

        $ah = SLN_Plugin::getInstance()->getAvailabilityHelper();
        if ( ! $ah->isValidTime($this->getDateTime())) {
            return false;
        }
        if ($this->data['services']) {
            $bookingServices = SLN_Wrapper_Booking_Services::build($this->data['services'], $this->getDateTime(), 0, $this->getCountServices());
            foreach ($bookingServices->getItems() as $bookingService) {

            SLN_Plugin::addLog('SLN booking service id: ' . print_r($bookingService->getService()->getId(), true));
            SLN_Plugin::addLog('SLN booking service attendant enabled: ' . print_r($bookingService->getService()->isAttendantsEnabled(), true));
        if($attendant = $bookingService->getAttendant()){
            if(!is_array($attendant)){
                SLN_Plugin::addLog('SLN booking service attendant id: ' . print_r($attendant ? $attendant->getId() : '', true));
            }else{
                SLN_Plugin::addLog('SLN booking service attendant ids: '. print_r(SLN_Wrapper_Attendant::getArrayAttendantsValue('getId', $attendant), true));
            }
        }

		if ($res = $ah->validateBookingService($bookingService)) {
                    return false;
                }
                if ($bookingService->getAttendant() && !is_array($bookingService->getAttendant()) && $res = $ah->validateBookingAttendant($bookingService)) {
                    return false;
                }elseif(is_array($bookingService->getAttendant()) && $ah->validateBookingAttendants($bookingService)){
                    return false;
                }
                if ($this->plugin->getSettings()->isAttendantsEnabled() && !$bookingService->getAttendant() && $bookingService->getService()->isAttendantsEnabled() && !$this->plugin->getSettings()->isAttendantsEnabledOnlyBackend()) {
                    return false;
                }
            }
        }

        return true;
    }

    public function getTips()
    {
	return $this->get('tips');
    }

    public function addTips($tips)
    {
	return $this->set('tips', SLN_Func::filter($this->getTips(), 'float') + SLN_Func::filter($tips, 'float'));
    }

    public function setCustomerTimezone($timezone)
    {
        $this->data['customer_timezone'] = $timezone;

        return $this;
    }

    public function getDateTimeCustomerTimezone()
    {
	return (new SLN_DateTime($this->getDate().' '.$this->getTime()))->setTimezone(new DateTimeZone($this->get('customer_timezone')));
    }

    public function getCountService($serviceID)
    {
        if (! isset($this->data['service_count'][$serviceID])) {
            return 1;
        }
        $c = (int) $this->data['service_count'][$serviceID];

        return $c > 0 ? $c : 1;
    }

    public function addCountService($serviceID, $countService)
    {
        $serviceCount = $this->get('service_count') && is_array($this->get('service_count')) ? $this->get('service_count') : array();
        $c            = (int) $countService;
        $serviceCount[$serviceID] = $c > 0 ? $c : 1;

        $this->set('service_count', $serviceCount);
    }

    public function removeCountService($serviceID)
    {
        $serviceCount = $this->get('service_count') && is_array($this->get('service_count')) ? $this->get('service_count') : array();

        if (isset($serviceCount[$serviceID])) {
            unset($serviceCount[$serviceID]);
        }

        $this->set('service_count', $serviceCount);
    }

    public function getCountServices()
    {
        return isset($this->data['service_count']) ? $this->data['service_count'] : array();
    }

    public function getData() {
        return $this->data;
    }

    /**
     * Overwrite the in-memory booking data wholesale.
     *
     * Used to restore a previously captured snapshot (e.g. after a read-only
     * process such as the forecast engine borrows the builder as scratch space).
     * Call save() afterwards to persist.
     *
     * @param array $data
     * @return $this
     */
    public function setData(array $data) {
        $this->data = $data;

        return $this;
    }

    public function removeResources()
    {
        $this->set('services_resources', array());
    }

    public function setResources(array $resources)
    {
        $this->set('services_resources', $resources);
    }

    public function getResources()
    {
        return $this->get('services_resources') ? $this->get('services_resources') : array();
    }

    /**
     * Validate and ensure required customer data exists before booking creation
     * 
     * CRITICAL DEFENSIVE METHOD added Feb 10, 2026 to prevent regression bug where
     * logged-in users could create bookings without customer data when selecting "Pay Later"
     * 
     * This method:
     * 1. Checks if required customer fields are populated
     * 2. Attempts to load from logged-in user if missing
     * 3. Throws exception if data still unavailable
     * 4. Logs all actions for debugging
     * 
     * @throws Exception if required customer data is missing and cannot be loaded
     */
    private function validateAndEnsureCustomerData()
    {
        SLN_Plugin::addLog('[Booking Validation] Checking customer data before creation...');
        
        // Check which required fields are missing
        $missingFields = array();
        
        if (SLN_Enum_CheckoutFields::getField('firstname')->isRequired() && empty($this->get('firstname'))) {
            $missingFields[] = 'firstname';
        }
        if (SLN_Enum_CheckoutFields::getField('lastname')->isRequired() && empty($this->get('lastname'))) {
            $missingFields[] = 'lastname';
        }
        if (SLN_Enum_CheckoutFields::getField('email')->isRequired() && empty($this->get('email'))) {
            $missingFields[] = 'email';
        }
        if (SLN_Enum_CheckoutFields::getField('phone')->isRequired() && empty($this->get('phone'))) {
            $missingFields[] = 'phone';
        }
        
        // If all required fields present, we're good
        if (empty($missingFields)) {
            SLN_Plugin::addLog('[Booking Validation] ✓ All required customer fields present');
            return;
        }
        
        // Missing required fields - this is a critical issue
        $missingFieldsList = implode(', ', $missingFields);
        SLN_Plugin::addLog(sprintf(
            '[Booking Validation] ✗ ERROR: Missing required customer fields: %s',
            $missingFieldsList
        ));
        
        // Log context for debugging
        SLN_Plugin::addLog(sprintf(
            '[Booking Validation] Context - User ID: %d, Session ID: %s, Data keys: %s',
            get_current_user_id(),
            session_id(),
            implode(', ', array_keys($this->data))
        ));
        
        // DEFENSIVE FALLBACK: Try to load from logged-in user profile
        if (is_user_logged_in()) {
            SLN_Plugin::addLog('[Booking Validation] Attempting to load customer data from logged-in user profile...');
            
            $loadedFields = array();
            $customer_fields = SLN_Enum_CheckoutFields::forRegistration()->appendSmsPrefix();
            
            foreach ($customer_fields as $key => $field) {
                if (empty($this->get($key))) {
                    $value = $field->getValue(get_current_user_id());
                    if (!empty($value)) {
                        $this->set($key, $value);
                        $loadedFields[] = $key;
                    }
                }
            }
            
            if (!empty($loadedFields)) {
                SLN_Plugin::addLog(sprintf(
                    '[Booking Validation] ✓ Loaded fields from user profile: %s',
                    implode(', ', $loadedFields)
                ));
                
                // Save the loaded data
                $this->save();
                
                // Re-check if we now have all required fields
                $stillMissing = array();
                foreach ($missingFields as $field) {
                    if (empty($this->get($field))) {
                        $stillMissing[] = $field;
                    }
                }
                
                if (empty($stillMissing)) {
                    SLN_Plugin::addLog('[Booking Validation] ✓ All required fields now present after loading from user profile');
                    return;
                }
                
                $missingFieldsList = implode(', ', $stillMissing);
                SLN_Plugin::addLog(sprintf(
                    '[Booking Validation] ✗ Still missing after user profile load: %s',
                    $missingFieldsList
                ));
            } else {
                SLN_Plugin::addLog('[Booking Validation] ✗ No fields could be loaded from user profile');
            }
        } else {
            SLN_Plugin::addLog('[Booking Validation] User not logged in - cannot load from profile');
        }
        
        // CRITICAL ERROR: Cannot create booking without required customer data
        // This prevents silent bugs where bookings appear in backend without customer info
        $errorMessage = sprintf(
            __('Unable to create booking: Required customer information is missing (%s). Please ensure all required fields are filled.', 'salon-booking-system'),
            $missingFieldsList
        );
        
        SLN_Plugin::addLog(sprintf('[Booking Validation] BLOCKING BOOKING CREATION: %s', $errorMessage));
        
        // Send error notification for monitoring
        if (class_exists('SLN_Helper_ErrorNotification')) {
            SLN_Helper_ErrorNotification::send(
                'BOOKING_MISSING_CUSTOMER_DATA',
                $errorMessage,
                sprintf(
                    "User ID: %d\nSession ID: %s\nMissing Fields: %s\nData Keys: %s",
                    get_current_user_id(),
                    session_id(),
                    $missingFieldsList,
                    implode(', ', array_keys($this->data))
                )
            );
        }
        
        throw new Exception($errorMessage);
    }

}
