<?php
/**
 * DESCRIPTION: Booking service handling booking CRUD, availability checks, and cutoff validation.
 * Dispatches events through both internal Event_Manager and WordPress actions.
 *
 * @package NemtlyBooking\Services
 */

namespace NemtlyBooking\Services;

use NemtlyBooking\Core\Event_Manager;
use NemtlyBooking\Core\Status;
use NemtlyBooking\Models\Booking;
use NemtlyBooking\Models\Event;
use NemtlyBooking\Repositories\Booking_Repository;
use NemtlyBooking\Repositories\Event_Repository;

class Booking_Service {
    /**
     * Booking repository
     *
     * @var Booking_Repository
     */
    private $booking_repository;

    /**
     * Event repository
     *
     * @var Event_Repository
     */
    private $event_repository;

    /**
     * Event manager
     *
     * @var Event_Manager
     */
    private $event_manager;

    /**
     * Constructor
     *
     * @param Booking_Repository $booking_repository The booking repository instance.
     * @param Event_Repository   $event_repository   The event repository instance.
     * @param Event_Manager      $event_manager      The event manager instance.
     */
    public function __construct( Booking_Repository $booking_repository, Event_Repository $event_repository, Event_Manager $event_manager ) {
        $this->booking_repository = $booking_repository;
        $this->event_repository   = $event_repository;
        $this->event_manager      = $event_manager;
    }

    /**
     * Create a new booking
     *
     * @param array $data Booking data.
     *
     * @return Booking
     * @throws \Exception When booking creation fails.
     */
    public function create_booking( array $data ): Booking {
        // Validate booking data
        $this->validate_booking_data( $data );

        // Check event exists and is bookable
        $event = $this->event_repository->find( $data['event_id'] );
        if ( ! $event ) {
            throw new \Exception( esc_html__( 'Event not found', 'nemtly-booking' ) );
        }

        // Check availability
        if ( ! $this->check_availability( $data['event_id'], $data['booking_date'] ?? null, $data['booking_start'] ?? null ) ) {
            throw new \Exception( esc_html__( 'Event is fully booked', 'nemtly-booking' ) );
        }

        // Check booking cutoff time
        if ( ! $this->is_booking_allowed( $event, $data['booking_date'] ?? null, $data['booking_start'] ?? null ) ) {
            throw new \Exception( esc_html__( 'Booking deadline has passed', 'nemtly-booking' ) );
        }

        // Create booking object
        $booking = new Booking(
            [
                'event_id'       => (int) $data['event_id'],
                'user_id'        => empty( $data['user_id'] ) ? get_current_user_id() : null,
                'attendee_name'  => sanitize_text_field( $data['attendee_name'] ),
                'attendee_email' => sanitize_email( $data['attendee_email'] ),
                'attendee_phone' => ! empty( $data['attendee_phone'] ) ? sanitize_text_field( $data['attendee_phone'] ) : null,
                'booking_date'   => sanitize_text_field( $data['booking_date'] ?? $event->get_date() ),
                'booking_start'  => $this->build_datetime( $data['booking_date'] ?? $event->get_date(), $data['booking_start'] ?? null ),
                'booking_end'    => $this->build_datetime( $data['booking_date'] ?? $event->get_date(), $data['booking_end'] ?? null ),
                'booking_meta'   => $this->sanitize_booking_meta( $data['booking_meta'] ?? [], $data ),
                'booking_status' => Status::BOOKING_PENDING,
            ]
        );

        // Dispatch before create booking event
        $this->event_manager->dispatch( 'booking.before_create', $booking->to_array() );

        // Save booking
        $saved_booking = $this->booking_repository->create( $booking );
        if ( ! $saved_booking ) {
            throw new \Exception( esc_html__( 'Failed to create booking', 'nemtly-booking' ) );
        }

        // Dispatch through internal Event_Manager for subscribers (e.g., Booking_Event_Subscriber).
        $this->event_manager->dispatch( 'booking.created', $saved_booking->to_array() );

        // Also fire a WordPress action so services registered via add_action can react.
        // Email_Notification_Service listens here to send confirmation emails.
        // Two dispatch mechanisms exist because the internal event system supports
        // subscriber priority/ordering while WP actions provide third-party extensibility.
        do_action( 'nemtly_booking_event_booking_created', $saved_booking->to_array() );

        return $saved_booking;
    }

    /**
     * Create a waitlist entry for a full event.
     *
     * Waitlist entries are bookings with the `waitlisted` status: they hold a
     * place in the queue without decrementing availability or collecting
     * payment. Promotion to a confirmed seat (when a spot opens) is handled
     * separately.
     *
     * @since PBOOKING_SINCE
     *
     * @param array $data Booking data (event_id, attendee_name, attendee_email
     *                    and optional attendee_phone / booking_date /
     *                    booking_start / booking_end / booking_meta).
     *
     * @return Booking
     * @throws \Exception When the event is missing or the entry cannot be saved.
     */
    public function create_waitlist_entry( array $data ): Booking {
        $event = $this->event_repository->find( (int) ( $data['event_id'] ?? 0 ) );
        if ( ! $event ) {
            throw new \Exception( esc_html__( 'Event not found', 'nemtly-booking' ) );
        }

        if ( empty( $data['attendee_name'] ) || empty( $data['attendee_email'] ) ) {
            throw new \Exception( esc_html__( 'Name and email are required to join the waitlist', 'nemtly-booking' ) );
        }

        $booking = new Booking(
            [
                'event_id'       => (int) $data['event_id'],
                'user_id'        => empty( $data['user_id'] ) ? get_current_user_id() : null,
                'attendee_name'  => sanitize_text_field( $data['attendee_name'] ),
                'attendee_email' => sanitize_email( $data['attendee_email'] ),
                'attendee_phone' => ! empty( $data['attendee_phone'] ) ? sanitize_text_field( $data['attendee_phone'] ) : null,
                'booking_date'   => sanitize_text_field( $data['booking_date'] ?? $event->get_date() ),
                'booking_start'  => $this->build_datetime( $data['booking_date'] ?? $event->get_date(), $data['booking_start'] ?? null ),
                'booking_end'    => $this->build_datetime( $data['booking_date'] ?? $event->get_date(), $data['booking_end'] ?? null ),
                'booking_meta'   => $this->sanitize_booking_meta( $data['booking_meta'] ?? [], $data ),
                'booking_status' => Status::BOOKING_WAITLISTED,
            ]
        );

        $saved_booking = $this->booking_repository->create( $booking );
        if ( ! $saved_booking ) {
            throw new \Exception( esc_html__( 'Failed to join the waitlist', 'nemtly-booking' ) );
        }

        $booking_data = $saved_booking->to_array();

        $this->event_manager->dispatch( 'booking.waitlisted', $booking_data );

        /**
         * Fires after a visitor joins an event waitlist.
         *
         * @since PBOOKING_SINCE
         *
         * @param array $booking_data The waitlisted booking data.
         */
        do_action( 'nemtly_booking_event_booking_waitlisted', $booking_data );

        return $saved_booking;
    }

    /**
     * Sanitize booking meta before it is persisted.
     *
     * Applies a default deep sanitizer ({@see sanitize_text_field()} on
     * scalar string values, recursing one level into `custom_fields`) and
     * exposes a filter so developer-injected custom fields registered via
     * the `nemtly_booking.booking.attendee_fields` JS filter can be
     * sanitized according to their own field types.
     *
     * @since PBOOKING_SINCE
     *
     * @param array $meta Raw booking meta from the request.
     * @param array $data Full booking request data.
     *
     * @return array Sanitized booking meta.
     */
    private function sanitize_booking_meta( array $meta, array $data ): array {
        $sanitized = [];

        foreach ( $meta as $key => $value ) {
            if ( 'custom_fields' === $key && is_array( $value ) ) {
                $custom_fields = [];
                foreach ( $value as $field_key => $field_value ) {
                    if ( is_scalar( $field_value ) ) {
                        $custom_fields[ sanitize_key( $field_key ) ] = sanitize_text_field( (string) $field_value );
                    }
                }
                $sanitized[ $key ] = $custom_fields;
                continue;
            }

            if ( is_bool( $value ) ) {
                $sanitized[ $key ] = $value;
            } elseif ( is_scalar( $value ) ) {
                $sanitized[ $key ] = sanitize_text_field( (string) $value );
            } else {
                $sanitized[ $key ] = $value;
            }
        }

        /**
         * Filters sanitized booking meta before it is persisted.
         *
         * @since PBOOKING_SINCE
         *
         * @param array $sanitized Sanitized booking meta, keyed by meta key.
         * @param array $data      Full booking request data.
         */
        return apply_filters( 'nemtly_booking_sanitize_booking_meta', $sanitized, $data );
    }

    /**
     * Update a booking
     *
     * @param int   $id   Booking ID.
     * @param array $data Booking data.
     *
     * @return Booking
     * @throws \Exception When booking update fails.
     */
    public function update_booking( int $id, array $data ): Booking {
        // Find existing booking
        $booking = $this->booking_repository->find( $id );
        if ( ! $booking ) {
            throw new \Exception( esc_html__( 'Booking not found', 'nemtly-booking' ) );
        }

        // Validate booking data (partial validation for updates)
        $this->validate_booking_data( $data, true );

        // Update booking properties
        if ( isset( $data['attendee_name'] ) ) {
            $booking->set_attendee_name( $data['attendee_name'] );
        }
        if ( isset( $data['attendee_email'] ) ) {
            $booking->set_attendee_email( $data['attendee_email'] );
        }
        if ( isset( $data['attendee_phone'] ) ) {
            $booking->set_attendee_phone( $data['attendee_phone'] );
        }
        if ( isset( $data['booking_status'] ) ) {
            $booking->set_booking_status( $data['booking_status'] );
        }
        if ( isset( $data['booking_meta'] ) ) {
            $booking->set_booking_meta( $data['booking_meta'] );
        }

        // Dispatch before update booking event
        $this->event_manager->dispatch( 'booking.before_update', $booking->to_array() );

        // Save booking
        $updated_booking = $this->booking_repository->update( $booking );
        if ( ! $updated_booking ) {
            throw new \Exception( esc_html__( 'Failed to update booking', 'nemtly-booking' ) );
        }

        // Dispatch after update booking event
        $this->event_manager->dispatch( 'booking.updated', $updated_booking->to_array() );

        // Also trigger WordPress action for Email_Notification_Service
        do_action( 'nemtly_booking_event_booking_updated', $updated_booking->to_array() );

        return $updated_booking;
    }

    /**
     * Cancel a booking
     *
     * @param int $id Booking ID.
     *
     * @return Booking
     * @throws \Exception When booking cancellation fails.
     */
    public function cancel_booking( int $id ): Booking {
        // Find existing booking
        $booking = $this->booking_repository->find( $id );
        if ( ! $booking ) {
            throw new \Exception( esc_html__( 'Booking not found', 'nemtly-booking' ) );
        }

        if ( $booking->is_cancelled() ) {
            throw new \Exception( esc_html__( 'Booking is already cancelled', 'nemtly-booking' ) );
        }

        // Update status to cancelled
        $booking->set_booking_status( Status::BOOKING_CANCELLED );

        // Dispatch before cancel booking event
        $this->event_manager->dispatch( 'booking.before_cancel', $booking->to_array() );

        // Save booking
        $cancelled_booking = $this->booking_repository->update( $booking );
        if ( ! $cancelled_booking ) {
            throw new \Exception( esc_html__( 'Failed to cancel booking', 'nemtly-booking' ) );
        }

        // Dispatch after cancel booking event
        $this->event_manager->dispatch( 'booking.cancelled', $cancelled_booking->to_array() );

        // Also trigger WordPress action for Email_Notification_Service
        do_action( 'nemtly_booking_event_booking_cancelled', $cancelled_booking->to_array() );

        return $cancelled_booking;
    }

    /**
     * Confirm a booking
     *
     * @param int $id Booking ID.
     *
     * @return Booking
     * @throws \Exception When booking confirmation fails.
     */
    public function confirm_booking( int $id ): Booking {
        // Find existing booking
        $booking = $this->booking_repository->find( $id );
        if ( ! $booking ) {
            throw new \Exception( esc_html__( 'Booking not found', 'nemtly-booking' ) );
        }

        if ( $booking->is_confirmed() ) {
            throw new \Exception( esc_html__( 'Booking is already confirmed', 'nemtly-booking' ) );
        }

        // Update status to confirmed
        $booking->set_booking_status( Status::BOOKING_CONFIRMED );

        // Dispatch before confirm booking event
        $this->event_manager->dispatch( 'booking.before_confirm', $booking->to_array() );

        // Save booking
        $confirmed_booking = $this->booking_repository->update( $booking );
        if ( ! $confirmed_booking ) {
            throw new \Exception( esc_html__( 'Failed to confirm booking', 'nemtly-booking' ) );
        }

        // Dispatch after confirm booking event
        $this->event_manager->dispatch( 'booking.confirmed', $confirmed_booking->to_array() );

        return $confirmed_booking;
    }

    /**
     * Delete a booking
     *
     * @param int $id Booking ID.
     *
     * @return bool
     * @throws \Exception When booking deletion fails.
     */
    public function delete_booking( int $id ): bool {
        // Find existing booking
        $booking = $this->booking_repository->find( $id );
        if ( ! $booking ) {
            throw new \Exception( esc_html__( 'Booking not found', 'nemtly-booking' ) );
        }

        // Dispatch before delete booking event
        $this->event_manager->dispatch( 'booking.before_delete', $booking->to_array() );

        // Delete booking
        $result = $this->booking_repository->delete( $id );
        if ( ! $result ) {
            throw new \Exception( esc_html__( 'Failed to delete booking', 'nemtly-booking' ) );
        }

        // Dispatch after delete booking event
        $this->event_manager->dispatch( 'booking.deleted', $booking->to_array() );

        return true;
    }

    /**
     * Get booking by ID
     *
     * @param int $id Booking ID.
     *
     * @return Booking|null
     */
    public function get_booking( int $id ): ?Booking {
        return $this->booking_repository->find( $id );
    }

    /**
     * Get booking by UUID
     *
     * @param string $uuid Booking UUID.
     *
     * @return Booking|null
     */
    public function get_booking_by_uuid( string $uuid ): ?Booking {
        return $this->booking_repository->find_by_uuid( $uuid );
    }

    /**
     * Get all bookings
     *
     * @param array $args Query arguments.
     *
     * @return Booking[]
     */
    public function get_bookings( array $args = [] ): array {
        return $this->booking_repository->find_all( $args );
    }

    /**
     * Get bookings for event
     *
     * @param int $event_id Event ID.
     *
     * @return Booking[]
     */
    public function get_bookings_for_event( int $event_id ): array {
        return $this->booking_repository->find_by_event_id( $event_id );
    }

    /**
     * Get bookings for user
     *
     * @param int $user_id User ID.
     *
     * @return Booking[]
     */
    public function get_bookings_for_user( int $user_id ): array {
        return $this->booking_repository->find_by_user_id( $user_id );
    }

    /**
     * Check availability for event, optionally at the slot level.
     *
     * For slot-based events (one-on-one, group) with a booking_start provided,
     * checks per-slot capacity. For recurring events with a booking_date, checks
     * per-instance capacity. Otherwise checks overall event capacity.
     *
     * @since PBOOKING_SINCE
     *
     * @param int         $event_id      Event ID.
     * @param string|null $booking_date  Specific booking date (Y-m-d).
     * @param string|null $booking_start Slot start time (H:i) for slot-based events.
     *
     * @return bool
     */
    public function check_availability( int $event_id, ?string $booking_date = null, ?string $booking_start = null ): bool {
        $event = $this->event_repository->find( $event_id );
        if ( ! $event ) {
            return false;
        }

        $max_attendees = $event->get_max_attendees();

        // max_attendees = 0 means unlimited capacity (convention used across all event types).
        if ( 0 === $max_attendees ) {
            return true;
        }

        // For slot-based events with a specific slot, check per-slot capacity.
        $slot_based_types = [ 'one-on-one', 'group' ];
        if ( in_array( $event->get_event_type(), $slot_based_types, true ) && null !== $booking_date && null !== $booking_start ) {
            $current_bookings = $this->booking_repository->count_bookings_for_slot( $event_id, $booking_date, $booking_start );
            return $current_bookings < $max_attendees;
        }

        // For recurring events each occurrence has its own capacity pool, so we
        // must count bookings for the specific date rather than the event overall.
        // Single events pass null which counts all bookings against the event.
        if ( null !== $booking_date ) {
            $current_bookings = $this->booking_repository->count_bookings_for_event( $event_id, $booking_date );
        } else {
            $current_bookings = $this->booking_repository->count_bookings_for_event( $event_id );
        }

        return $current_bookings < $max_attendees;
    }

    /**
     * Check if booking is allowed (considering cutoff time).
     *
     * For recurring events the master event date may be in the past, so we use
     * the booking_date (specific instance date) instead. For slot-based events
     * the booking_start time is used as the reference time.
     *
     * @since PBOOKING_SINCE
     *
     * @param Event       $event         Event object.
     * @param string|null $booking_date  Specific booking date.
     * @param string|null $booking_start Slot start time (H:i) for slot-based events.
     *
     * @return bool
     */
    private function is_booking_allowed( Event $event, ?string $booking_date = null, ?string $booking_start = null ): bool {
        $event_date = $booking_date ?? $event->get_date();
        $event_time = $booking_start ?? $event->get_time();

        if ( empty( $event_date ) || empty( $event_time ) ) {
            // For date-based events without a time, just check the date is in the future
            if ( ! empty( $event_date ) ) {
                return $event_date >= gmdate( 'Y-m-d' );
            }
            return false;
        }

        $event_datetime = new \DateTime( $event_date . ' ' . $event_time );
        $now            = new \DateTime();

        // Check if event is in the past
        if ( $event_datetime <= $now ) {
            return false;
        }

        // TODO: Implement booking cutoff logic based on event meta
        // For now, allow booking until the event starts

        return true;
    }

    /**
     * Validate booking data
     *
     * @param array $data      Booking data.
     * @param bool  $is_update Whether this is an update operation.
     *
     * @throws \Exception When validation fails.
     */
    private function validate_booking_data( array $data, bool $is_update = false ): void {
        $errors = [];

        // Required fields for create
        if ( ! $is_update ) {
            if ( empty( $data['event_id'] ) ) {
                $errors[] = __( 'Event ID is required', 'nemtly-booking' );
            }

            if ( empty( $data['attendee_name'] ) ) {
                $errors[] = __( 'Attendee name is required', 'nemtly-booking' );
            }

            if ( empty( $data['attendee_email'] ) ) {
                $errors[] = __( 'Attendee email is required', 'nemtly-booking' );
            }
        }

        // Email validation
        if ( ! empty( $data['attendee_email'] ) && ! is_email( $data['attendee_email'] ) ) {
            $errors[] = __( 'Invalid email address', 'nemtly-booking' );
        }

        // Status validation
        if ( isset( $data['booking_status'] ) ) {
            $allowed_statuses = Status::booking_statuses();
            if ( ! in_array( $data['booking_status'], $allowed_statuses, true ) ) {
                $errors[] = __( 'Invalid booking status', 'nemtly-booking' );
            }
        }

        // Date validation
        if ( isset( $data['booking_date'] ) && ! $this->is_valid_date( $data['booking_date'] ) ) {
            $errors[] = __( 'Invalid booking date format', 'nemtly-booking' );
        }

        if ( ! empty( $errors ) ) {
            // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
            throw new \Exception( implode( ', ', $errors ) );
        }
    }

    /**
     * Validate date format
     *
     * @param string $date Date string.
     *
     * @return bool
     */
    private function is_valid_date( string $date ): bool {
        $d = \DateTime::createFromFormat( 'Y-m-d', $date );
        return $d && $d->format( 'Y-m-d' ) === $date;
    }

    /**
     * Build a full datetime string from a date and a time value.
     *
     * The booking_start and booking_end columns are DATETIME, so we need to
     * combine the booking date (Y-m-d) with the time (H:i) into a full
     * datetime string (Y-m-d H:i:s).
     *
     * @since PBOOKING_SINCE
     *
     * @param string      $date Date string (Y-m-d).
     * @param string|null $time Time string (H:i or H:i:s), or null.
     *
     * @return string|null Full datetime string or null if time is empty.
     */
    private function build_datetime( string $date, ?string $time ): ?string {
        if ( empty( $time ) ) {
            return null;
        }

        // If it already looks like a full datetime, return as-is
        if ( strlen( $time ) > 8 ) {
            return $time;
        }

        // Append seconds if not present (H:i → H:i:00)
        if ( 5 === strlen( $time ) ) {
            $time .= ':00';
        }

        return $date . ' ' . $time;
    }

    /**
     * Get booking statistics
     *
     * @return array
     */
    public function get_booking_statistics(): array {
        return [
            'total_bookings'     => $this->booking_repository->count(),
            'pending_bookings'   => $this->booking_repository->count( [ 'status' => Status::BOOKING_PENDING ] ),
            'confirmed_bookings' => $this->booking_repository->count( [ 'status' => Status::BOOKING_CONFIRMED ] ),
            'cancelled_bookings' => $this->booking_repository->count( [ 'status' => Status::BOOKING_CANCELLED ] ),
        ];
    }
}
