<?php
/**
 * Final server-side guard for customer service changes.
 */
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Real-time collision checks must query the plugin's custom scheduling tables; identifiers and ID lists are constructed only from the WordPress prefix and absint values.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

class YATOON_Change_Service_Safety {
    /** Find a complete non-overlapping employee assignment for a replacement set. */
    public static function choose_staff_assignment( array $prepared, array $candidate_sets ) {
        $order = array_keys( $candidate_sets );
        usort( $order, static function ( $left, $right ) use ( $candidate_sets, $prepared ) {
            $left_explicit = ! empty( $prepared[ $left ]['requested_staff_id'] );
            $right_explicit = ! empty( $prepared[ $right ]['requested_staff_id'] );
            if ( $left_explicit !== $right_explicit ) { return $left_explicit ? -1 : 1;
            }
            $by_count = count( $candidate_sets[ $left ] ) <=> count( $candidate_sets[ $right ] );
            return 0 !== $by_count ? $by_count : $left <=> $right;
        } );

        $assigned = array();
        $search = function ( $position ) use ( &$search, &$assigned, $order, $candidate_sets, $prepared ) {
            if ( $position >= count( $order ) ) { return true;
            }
            $index = $order[ $position ];
            foreach ( $candidate_sets[ $index ] as $staff ) {
                $conflict = false;
                foreach ( $assigned as $other_index => $other_staff ) {
                    if (
                        absint( $other_staff->id ?? 0 ) === absint( $staff->id ?? 0 )
                        && $prepared[ $index ]['start'] < $prepared[ $other_index ]['end']
                        && $prepared[ $index ]['end'] > $prepared[ $other_index ]['start']
                    ) {
                        $conflict = true;
                        break;
                    }
                }
                if ( $conflict ) { continue;
                }
                $assigned[ $index ] = $staff;
                if ( $search( $position + 1 ) ) { return true;
                }
                unset( $assigned[ $index ] );
            }
            return false;
        };

        return $search( 0 ) ? $assigned : false;
    }

    public static function lock_name( $booking ) {
        $identity = ! empty( $booking->group_booking_id )
            ? (string) $booking->group_booking_id
            : ( ! empty( $booking->square_booking_id ) ? (string) $booking->square_booking_id : (string) $booking->id );
        return 'yatoon_change_' . substr( hash( 'sha256', $identity ), 0, 32 );
    }

    public static function acquire( $name, $timeout = 3 ) {
        global $wpdb;
        return '1' === (string) $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK(%s,%d)', $name, max( 0, absint( $timeout ) ) ) );
    }

    public static function release( $name ) {
        global $wpdb;
        if ( $name ) {
            $wpdb->get_var( $wpdb->prepare( 'SELECT RELEASE_LOCK(%s)', $name ) );
        }
    }

    public static function validate_prepared( array $prepared, array $group_rows, $platform = 'local' ) {
        // The database checks below intentionally exclude the appointment's
        // existing rows. Check the complete replacement set against itself so
        // two parallel guests cannot accidentally be assigned to the same
        // employee at overlapping times in one multi-booking save.
        foreach ( array_values( $prepared ) as $index => $entry ) {
            foreach ( array_slice( array_values( $prepared ), $index + 1 ) as $other ) {
                $staff_id = absint( $entry['staff']->id ?? 0 );
                $other_staff_id = absint( $other['staff']->id ?? 0 );
                $start = $entry['start'] ?? null;
                $end = $entry['end'] ?? null;
                $other_start = $other['start'] ?? null;
                $other_end = $other['end'] ?? null;
                if (
                    $staff_id && $staff_id === $other_staff_id
                    && $start instanceof DateTime && $end instanceof DateTime
                    && $other_start instanceof DateTime && $other_end instanceof DateTime
                    && $start < $other_end && $end > $other_start
                ) {
                    return new WP_Error( 'staff_conflict', __( 'Two services in this appointment overlap for the same staff member. Choose another available staff member for one of them.', 'yatoon-booking-system' ) );
                }
            }
        }

        foreach ( $prepared as $entry ) {
            $service_id = absint( $entry['service']->id ?? 0 );
            $staff_id   = absint( $entry['staff']->id ?? 0 );
            $start      = $entry['start'] ?? null;
            $end        = $entry['end'] ?? null;
            if ( ! $service_id || ! $staff_id || ! $start instanceof DateTime || ! $end instanceof DateTime ) {
                return new WP_Error( 'invalid_change_window', __( 'The updated appointment schedule is incomplete. Please try again.', 'yatoon-booking-system' ) );
            }
            // Business hours are a hard salon rule regardless of the active
            // integration. Square accepts booking updates outside a seller's
            // opening window in some cases, so it cannot be the only guard.
            if ( ! self::within_open_hours( $start, $end ) ) {
                return new WP_Error( 'outside_business_hours', __( 'The selected services would extend outside business hours or into a break. Please reschedule first.', 'yatoon-booking-system' ) );
            }
            if ( self::is_closed_date( $start ) ) {
                return new WP_Error( 'closed_date', __( 'The appointment date is closed for online changes. Please contact the salon.', 'yatoon-booking-system' ) );
            }
            if ( ! self::staff_window_free( $staff_id, $start, $end, $group_rows ) ) {
                return new WP_Error( 'staff_conflict', __( 'That time is no longer available for the assigned staff member. Please reschedule first.', 'yatoon-booking-system' ) );
            }
            $location_id = absint( $entry['location_id'] ?? ( $group_rows[0]->location_id ?? 0 ) );
            if ( ! self::resources_available( $service_id, $start, $end, $group_rows, $location_id ) ) {
                return new WP_Error( 'resource_conflict', __( 'A required chair, room, or shared resource is not available for the added service. Please choose another time.', 'yatoon-booking-system' ) );
            }
        }
        return true;
    }

    public static function within_open_hours( DateTime $start, DateTime $end ) {
        if ( $start >= $end || $start->format( 'Y-m-d' ) !== $end->format( 'Y-m-d' ) ) {
            return false;
        }
        if ( ! class_exists( 'YATOON_Business_Hours' ) ) {
            return true;
        }
        $day = YATOON_Business_Hours::get_day_hours( $start->format( 'Y-m-d' ) );
        if ( ! $day || empty( $day['start'] ) || empty( $day['end'] ) ) {
            return false;
        }
        $tz = $start->getTimezone();
        $date = $start->format( 'Y-m-d' );
        $segments = array(
            array( new DateTime( $date . ' ' . $day['start'], $tz ), new DateTime( $date . ' ' . $day['end'], $tz ) ),
        );
        if ( ! empty( $day['break_start'] ) && ! empty( $day['break_end'] ) ) {
            $break_start = new DateTime( $date . ' ' . $day['break_start'], $tz );
            $break_end   = new DateTime( $date . ' ' . $day['break_end'], $tz );
            $segments = array(
                array( $segments[0][0], $break_start ),
                array( $break_end, $segments[0][1] ),
            );
        }
        foreach ( $segments as $segment ) {
            if ( $start >= $segment[0] && $end <= $segment[1] ) {
                return true;
            }
        }
        return false;
    }

    public static function is_closed_date( DateTime $start ) {
        $dates = preg_split( '/[\s,]+/', (string) get_option( 'yatoon_closed_dates', '' ) );
        return in_array( $start->format( 'Y-m-d' ), array_filter( array_map( 'trim', (array) $dates ) ), true );
    }

    private static function excluded_ids( array $group_rows ) {
        return array_values( array_filter( array_map( static function ( $row ) {
            return absint( $row->id ?? 0 );
        }, $group_rows ) ) );
    }

    public static function staff_window_free( $staff_id, DateTime $start, DateTime $end, array $group_rows ) {
        global $wpdb;
        $exclude = self::excluded_ids( $group_rows );
        $exclude_sql = $exclude ? ' AND id NOT IN (' . implode( ',', $exclude ) . ')' : '';
        $conflict = $wpdb->get_var( $wpdb->prepare(
            "SELECT id FROM {$wpdb->prefix}yatoon_bookings
             WHERE staff_id=%d
               AND LOWER(COALESCE(status,'')) NOT IN ('cancelled','canceled','cancelled_by_customer','cancelled_by_seller','canceled_by_customer','canceled_by_seller','declined','no_show')
               AND start_time < %s AND end_time > %s {$exclude_sql} LIMIT 1",
            absint( $staff_id ),
            $end->format( 'Y-m-d H:i:s' ),
            $start->format( 'Y-m-d H:i:s' )
        ) );
        if ( $conflict ) {
            return false;
        }
        $tz = yatoon_timezone();
        return ! class_exists( 'YATOON_Business_Hours' ) || ! YATOON_Business_Hours::has_staff_time_off_conflict( $staff_id, $start, $end, $tz );
    }

    public static function resources_available( $service_id, DateTime $start, DateTime $end, array $group_rows, $location_id = 0 ) {
        global $wpdb;
        $requirements = $wpdb->get_results( $wpdb->prepare(
            "SELECT r.id, r.capacity, r.location_id, rs.quantity_required
             FROM {$wpdb->prefix}yatoon_resources r
             INNER JOIN {$wpdb->prefix}yatoon_resource_services rs ON rs.resource_id=r.id
             WHERE rs.service_id=%d AND r.is_active=1 AND (r.location_id=0 OR r.location_id=%d)",
            absint( $service_id ),
            absint( $location_id )
        ) );
        if ( ! $requirements ) {
            return true;
        }
        $exclude = self::excluded_ids( $group_rows );
        $exclude_sql = $exclude ? ' AND b.id NOT IN (' . implode( ',', $exclude ) . ')' : '';
        foreach ( $requirements as $resource ) {
            $used = (int) $wpdb->get_var( $wpdb->prepare(
                "SELECT COALESCE(SUM(rs.quantity_required),0)
                 FROM {$wpdb->prefix}yatoon_bookings b
                 INNER JOIN {$wpdb->prefix}yatoon_resource_services rs ON rs.service_id=b.service_id
                 WHERE rs.resource_id=%d {$exclude_sql}
                   AND (%d=0 OR b.location_id=%d)
                   AND LOWER(COALESCE(b.status,'')) NOT IN ('cancelled','canceled','declined','no_show')
                   AND b.start_time < %s AND b.end_time > %s",
                absint( $resource->id ),
                absint( $resource->location_id ),
                absint( $resource->location_id ),
                $end->format( 'Y-m-d H:i:s' ),
                $start->format( 'Y-m-d H:i:s' )
            ) );
            if ( $used + max( 1, (int) $resource->quantity_required ) > max( 1, (int) $resource->capacity ) ) {
                return false;
            }
        }
        return true;
    }
}
