<?php
/**
 * Time_Slot_Manager class
 *
 * @package Booktics\Appointment\Schedule
 */

namespace Booktics\Appointment\Schedule;

use Booktics\Models\Appointment_Model;
use Booktics\Models\Team_Member_Model;
use Booktics\Module\Google_Calendar\Service\Calendar;
use Booktics\Services\Schedule_Record;
use DateTime;
use Exception;

/**
 * Class Time_Slot_Manager
 */
class Time_Slot_Manager {
    /**
     * Store Service Schedule Object
     *
     * @var Service_Schedule
     */
    private $service_schedule;

    /**
     * Store Team member Schedule Object
     *
     * @var Team_Member_Schedule
     */
    private $team_member_schedule;

    /**
     * Store Default Schedule Object
     *
     * @var Default_Schedule
     */
    private $default_schedule;

    /**
     * Store service duration in minutes
     *
     * @var integer
     */
    private $service_duration;

    /**
     * Store slot length in minutes
     *
     * @var integer
     */
    private $slot_length;

    /**
     * Store before buffer time in minutes
     *
     * @var integer
     */
    private $before_buffer_time;

    /**
     * Store after buffer time in minutes
     *
     * @var integer
     */
    private $after_buffer_time;

    /**
     * Store time format (12 or 24 hour)
     *
     * @var string
     */
    private $slot_format = '12'; // Default to 12 hour format.

    /**
     * Store service id 
     */
    private $service_id = null;

    /**
     * Number of attendees for time slot
     */
    private $total_attendees = 1;

    /**
     * Cached Google Calendar booked slots grouped by date (Y-m-d => array of slots).
     * Populated once per range request to avoid repeated API calls.
     *
     * @var array|null
     */
    private $google_booked_slots_cache = null;

    /**
     * Constructor
     *
     * @param Service_Schedule $service Service schedule object.
     * @param Team_Member_Schedule $team_member Team member schedule object.
     * @param array $options Optional parameters as an associative array.
     */
    public function __construct( Service_Schedule $service, Team_Member_Schedule $team_member, $options = array() ) {
        $defaults = array(
            'default_schedule'    => null,
            'service_duration'    => 30,
            'slot_length'         => 30,
            'before_buffer_time'  => 0,
            'after_buffer_time'   => 0,
            'total_attendees'     => 1,
        );
        $args = \wp_parse_args( $options, $defaults );

        $this->service_schedule     = $service;
        $this->team_member_schedule = $team_member;
        $this->default_schedule     = $args['default_schedule'];
        $this->service_duration     = $args['service_duration'];
        $this->slot_length          = $args['slot_length'];
        $this->before_buffer_time   = $args['before_buffer_time'];
        $this->after_buffer_time    = $args['after_buffer_time'];
        $this->service_id           = $args['service_id'] ?? null;
        $this->total_attendees      = $args['total_attendees'];
    }

    /**
     * Remove duplicates and sort time slots
     *
     * @param array $slots Array of time slots to deduplicate and sort.
     * @return array Deduplicated and sorted array of time slots
     */
    private function deduplicate_and_sort_slots( array $slots ): array {
        // Remove duplicates based on start and end times
        $unique = array();
        $seen = array();

        foreach ( $slots as $slot ) {
            $key = $slot->start . '-' . $slot->end;
            if ( ! isset( $seen[ $key ] ) ) {
                $seen[ $key ] = true;
                $unique[] = $slot;
            }
        }

        // Sort slots by start time
        usort(
            $unique,
            function ( $a, $b ) {
                $time_a = strtotime( $a->start );
                $time_b = strtotime( $b->start );
                return $time_a - $time_b;
            }
        );

        return $unique;
    }

    /**
     * Generate time slots for a specific date
     *
     * @param DateTime $date
     *
     * @return array
     */
    public function generate_slots( DateTime $date ): array {
        $service_entries    = $this->service_schedule->get_first_date( $date );
        $team_member_entries = $this->team_member_schedule->get_first_date( $date );
        $default_entries     = $this->default_schedule ? $this->default_schedule->get_first_date( $date ) : array();

        $all_slots = array();

        // Handle the case when any schedule returns a single entry (backwards compatibility)
        if ( ! is_array( $default_entries ) ) {
            $default_entries = array( $default_entries );
        }
        if ( ! is_array( $service_entries ) ) {
            $service_entries = array( $service_entries );
        }
        if ( ! is_array( $team_member_entries ) ) {
            $team_member_entries = array( $team_member_entries );
        }

        // Remove null entries
        $default_entries = array_filter( $default_entries );
        $service_entries = array_filter( $service_entries );
        $team_member_entries = array_filter( $team_member_entries );

        // Get the appropriate strategy from factory and generate slots
        $strategy = Time_Slot_Strategy_Factory::get_strategy(
            $this->service_id,
            $this->team_member_schedule->get_id() ?? null
        );

        $slots = $strategy->generate_slots(
            $service_entries,
            $team_member_entries,
            $default_entries,
            $this,
            $date
        );
        $all_slots = array_merge( $all_slots, $slots );

        // Remove duplicates and sort slots
        $all_slots = $this->deduplicate_and_sort_slots( $all_slots );
        $all_slots = $this->set_holiday_slots_as_empty_array( $all_slots );

        return $all_slots;
    }

    /**
     * Generate time slots for a date range.
     *
     * Creates an array of available time slots for each day in the given date range.
     * The slots are marked as booked if they overlap with existing appointments.
     *
     * @param string $start_date Start date for the date range in Y-m-d format
     * @param string $end_date End date for the date range in Y-m-d format
     *
     * @return array Collection of daily time slots indexed by date in Y-m-d format
     * @throws Exception When date parsing fails or invalid date range is provided
     */
    public function generate_slots_for_range( string $start_date, string $end_date ): array {
        // Prefetch Google Calendar events for the entire range (single API call)
        $this->prefetch_google_calendar_events( $start_date, $end_date );

        $slots_by_day = array();
        $start        = new DateTime( $start_date );
        $end          = new DateTime( $end_date );

        while ( $start <= $end ) {
            $daily_slots                               = $this->generate_slots( clone $start );
            $slots_by_day[ $start->format( 'Y-m-d' ) ] = $this->mark_booked_slots( clone $start, $daily_slots );
            $start->modify( '+1 day' );
        }

        $slots_by_day = $this->set_holiday_slots_as_empty_array( $slots_by_day );

        return $slots_by_day;
    }

    /**
     * Removes slots that fall on holidays from the given array of generated slots by date.
     *
     * @param array $slots Array of time slots to remove holiday slots from.
     * @return array Array of time slots with holiday slots removed.
     */
    private function set_holiday_slots_as_empty_array( array $slots ): array {
        $team_member_holidays = [];
        $holidays             = booktics_get_option( 'holiday' , []); // holidays from default settings
        $team_member_id       = $this->team_member_schedule->get_id() ?? null;
        
        if ( $team_member_id ) {
            $team_member_holidays = ( new Schedule_Record() )->get_schedules( $team_member_id )['holiday'] ?? [];
            $team_member          = ( new Team_Member_Model( $team_member_id ) );
            
            // If team member has custom holidays enabled, merge them with default holidays
            if ( $team_member->enable_custom_holiday ) {
                $holidays = array_merge( $holidays, $team_member_holidays );
            }
        }

        if ( empty( $holidays ) || empty( $slots ) ) {
            return $slots;
        }

        foreach ( $slots as $slot_date => $slot_data ) {
            if ( empty( $slot_data ) ) {
                continue;
            }

            if ( ! is_array( $holidays ) || empty( $holidays ) ) {
                break;
            }

            foreach ( $holidays as $holiday ) {
                $formatted_slot_date = gmdate( 'Y-m-d', strtotime( $slot_date ) );
                $holiday_start       = gmdate( 'Y-m-d', strtotime( $holiday[0] ) );
                $holiday_end         = gmdate( 'Y-m-d', strtotime( $holiday[1] ) );
                
                // if the slot date is between holiday range, remove it by setting slots as empty
                if ( $formatted_slot_date >= $holiday_start && $formatted_slot_date <= $holiday_end ) {
                    $slots[ $slot_date ] = [];
                    break;
                }
            }
        }

        return $slots;
    }

    /**
     * Get all available slots
     *
     * @param DateTime $date
     *
     * @return  array
     * @throws Exception
     */
    public function get_available_slots( DateTime $date ): array {
        $date_str = $date->format( 'Y-m-d' );
        $this->prefetch_google_calendar_events( $date_str, $date_str );

        $slots = $this->generate_slots( $date );
        $slots = $this->mark_booked_slots( $date, $slots );

        return array_filter( $slots, fn( $slot ) => $slot->is_available );
    }

    /**
     * Mark booked slots
     *
     * @param DateTime $date
     * @param array $slots
     *
     * @return array
     * @throws Exception
     */
    public function mark_booked_slots( DateTime $date, array $slots ): array {
        $booked_slots = $this->get_booked_slots( $date );
        foreach ( $slots as $slot ) {
            $slot_start = strtotime( $slot->start );
            $slot_end   = strtotime( $slot->end );
            $booked_count = 0;

            foreach ( $booked_slots as $booked ) {
                $booked_start = strtotime( $booked['start'] );
                $booked_end   = strtotime( $booked['end'] );
                if ( $slot_start < $booked_end && $slot_end > $booked_start ) {
                    if ( (int) $booked['team_member_id'] === (int) $this->team_member_schedule->get_id() ) {
                        $attendees = ! empty( $booked['total_attendees'] ) ? (int) $booked['total_attendees'] : 1;
                        $booked_count += $attendees;
                    }
                }
            }

            $slot->booked_count = $booked_count;
            $slot->available_spots = max( 0, $slot->max_capacity - $booked_count );
            $slot->is_available = $slot->available_spots >= $this->total_attendees;
        }

        return $slots;
    }

    /**
     * Get booked slots
     *
     * @param DateTime $date
     *
     * @return array
     * @throws Exception
     */
    private function get_booked_slots( DateTime $date ): array {
        $team_member_id = $this->team_member_schedule->get_id();
        $appointments   = ( new Appointment_Model() )
            ->get_appointments_for_slot(
                $date->format( 'Y-m-d' ),
                $team_member_id
            );

        $booked_slots = array_map(
            function ( $appointment ) use ( $date ) {
                $start_time           = strtotime( $appointment->start_time );
                $appointment_duration = 30;
                if ( ! empty( $appointment->duration ) && (int) $appointment->duration > 0 ) {
                    $appointment_duration = (int) $appointment->duration;
                }
                $actual_end_time = $start_time + ( $appointment_duration * 60 );

                return array(
                    'start'          => gmdate( 'H:i', $start_time ),
                    'end'            => gmdate( 'H:i', $actual_end_time ),
                    'service_id'     => $appointment->service_id,
                    'team_member_id' => $appointment->team_member_id,
                    'total_attendees'=> $appointment->total_attendees ?? 1,
                );
            },
            $appointments
        );

        // Merge cached Google Calendar booked slots for this date
        $date_str = $date->format( 'Y-m-d' );
        if ( is_array( $this->google_booked_slots_cache ) && ! empty( $this->google_booked_slots_cache[ $date_str ] ) ) {
            $booked_slots = array_merge( $booked_slots, $this->google_booked_slots_cache[ $date_str ] );
        }

        return $booked_slots;
    }

    /**
     * Prefetch Google Calendar events for an entire date range in a single API call.
     * Groups results by date and stores in instance cache.
     *
     * @param string $start_date Start date in Y-m-d format.
     * @param string $end_date   End date in Y-m-d format.
     */
    private function prefetch_google_calendar_events( string $start_date, string $end_date ): void {
        // Already fetched or feature disabled — skip
        if ( null !== $this->google_booked_slots_cache ) {
            return;
        }

        $this->google_booked_slots_cache = array();

        $team_member_id = $this->team_member_schedule->get_id();
        if ( ! $team_member_id ) {
            return;
        }

        $block_enabled = booktics_get_option( 'block_booking_during_google_events', false );
        if ( ! $block_enabled ) {
            return;
        }

        if ( ! class_exists( Calendar::class ) ) {
            return;
        }

        $calendar = new Calendar();
        if ( ! $calendar->is_configured( (int) $team_member_id ) ) {
            return;
        }

        $wp_timezone = wp_timezone_string();

        try {
            $range_start = new \DateTime( $start_date . ' 00:00:00', new \DateTimeZone( $wp_timezone ) );
            $range_end   = new \DateTime( $end_date . ' 23:59:59', new \DateTimeZone( $wp_timezone ) );
        } catch ( \Exception $e ) {
            return;
        }

        $api_filters = array(
            'timeMin' => rawurlencode( $range_start->format( 'c' ) ),
            'timeMax' => rawurlencode( $range_end->format( 'c' ) ),
        );

        $events = $calendar->get_google_calendar_events( (int) $team_member_id, $api_filters );

        if ( is_wp_error( $events ) || empty( $events ) ) {
            return;
        }

        // Collect all Booktics-synced Google event IDs to avoid double-blocking
        $existing_event_ids = $this->get_all_synced_google_event_ids( $start_date, $end_date, $team_member_id );

        foreach ( $events as $event ) {
            // Skip events already synced as Booktics appointments
            if ( isset( $event['id'] ) && in_array( $event['id'], $existing_event_ids, true ) ) {
                continue;
            }

            // Skip all-day events (no dateTime key)
            if ( empty( $event['start']['dateTime'] ) || empty( $event['end']['dateTime'] ) ) {
                continue;
            }

            // Skip transparent (free) events — only block busy time
            if ( isset( $event['transparency'] ) && 'transparent' === $event['transparency'] ) {
                continue;
            }

            try {
                $event_start = new \DateTime( $event['start']['dateTime'] );
                $event_end   = new \DateTime( $event['end']['dateTime'] );
            } catch ( \Exception $e ) {
                continue;
            }

            $event_date = $event_start->format( 'Y-m-d' );

            if ( ! isset( $this->google_booked_slots_cache[ $event_date ] ) ) {
                $this->google_booked_slots_cache[ $event_date ] = array();
            }

            $this->google_booked_slots_cache[ $event_date ][] = array(
                'start'           => $event_start->format( 'H:i' ),
                'end'             => $event_end->format( 'H:i' ),
                'service_id'      => 0,
                'team_member_id'  => $team_member_id,
                'total_attendees' => 1,
            );
        }
    }

    /**
     * Get all Google Calendar event IDs already linked to Booktics appointments in a date range.
     *
     * @param string $start_date     Start date in Y-m-d format.
     * @param string $end_date       End date in Y-m-d format.
     * @param int    $team_member_id Team member user ID.
     *
     * @return array Array of Google Calendar event IDs.
     */
    private function get_all_synced_google_event_ids( string $start_date, string $end_date, $team_member_id ): array {
        $args = array(
            'post_type'      => 'booktics-appointment',
            'posts_per_page' => -1,
            'fields'         => 'ids',
            // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required to find synced appointments in date range
            'meta_query'     => array(
                'relation' => 'AND',
                array(
                    'key'     => 'date',
                    'value'   => array( $start_date, $end_date ),
                    'compare' => 'BETWEEN',
                    'type'    => 'DATE',
                ),
                array(
                    'key'     => 'team_member_id',
                    'value'   => $team_member_id,
                    'compare' => '=',
                ),
                array(
                    'key'     => '_google_calendar_event_id',
                    'compare' => 'EXISTS',
                ),
            ),
        );

        $post_ids  = get_posts( $args );
        $event_ids = array();

        foreach ( $post_ids as $post_id ) {
            $event_id = get_post_meta( $post_id, '_google_calendar_event_id', true );
            if ( ! empty( $event_id ) ) {
                $event_ids[] = $event_id;
            }
        }

        return $event_ids;
    }

    /**
     * Get the length of time slots in minutes
     *
     * @return integer Slot length in minutes
     */
    public function get_slot_length(): int {
        return $this->slot_length;
    }

    /**
     * Get the service duration in minutes
     *
     * @return integer Service duration in minutes
     */
    public function get_service_duration(): int {
        return $this->service_duration;
    }

    /**
     * Get the before buffer time in minutes
     *
     * @return integer Before buffer time in minutes
     */
    public function get_before_buffer_time(): int {
        return $this->before_buffer_time;
    }

    /**
     * Get the after buffer time in minutes
     *
     * @return integer After buffer time in minutes
     */
    public function get_after_buffer_time(): int {
        return $this->after_buffer_time;
    }

    /**
     * Get the total slot duration including buffer times
     *
     * @return integer Total slot duration in minutes
     */
    public function get_total_slot_duration(): int {
        return $this->service_duration + $this->before_buffer_time + $this->after_buffer_time;
    }

    /**
     * Gets service max capacity for a single time slot
     * Its used to get services group booking max capacity 
     */
    public function get_service_max_capacity_for_timeslot(): int {
        // For handling cases where bookitcs pro is not active/ available
        if ( ! class_exists( '\BookticsPro\Models\Group_Booking_Model' ) ) {
            return 1;
        }

        $group_bookings_list = ( new \BookticsPro\Models\Group_Booking_Model() )->where( 'service_id', $this->service_id );
        if ( ! is_array( $group_bookings_list ) ) {
            return 1;
        }

        $group_booking = $group_bookings_list[0];
        
        if ( ! $group_booking || ! $group_booking->enable_group_booking ) {
            return 1;
        }

        return $group_booking->max_booking ?? 1;
    }
}
