<?php
/**
 * Recurrence Rule Handler
 *
 * Handles parsing and calculating recurrence patterns for events
 * Based on simplified RFC 5545 recurrence rules
 *
 * @package NemtlyBooking
 * @since   PBOOKING_SINCE
 */

namespace NemtlyBooking\Services;

use DateTime;
use DateInterval;
use WP_Error;
use NemtlyBooking\Models\Virtual_Instance;

/**
 * Recurrence Rule class
 *
 * Calculates virtual event instances from recurrence patterns
 *
 * @since PBOOKING_SINCE
 */
class Recurrence_Rule {

    /**
     * Supported frequencies
     *
     * @var array
     */
    const FREQUENCIES = [
        'AVAILABILITY' => 'Availability',
        'DAILY'        => 'Daily',
        'WEEKLY'       => 'Weekly',
        'MONTHLY'      => 'Monthly',
        'YEARLY'       => 'Yearly',
    ];

    /**
     * Day-of-week abbreviation map for matching availability keys
     *
     * @var array
     */
    const DAY_MAP = [
        1 => 'mon',
        2 => 'tue',
        3 => 'wed',
        4 => 'thu',
        5 => 'fri',
        6 => 'sat',
        7 => 'sun',
    ];

    /**
     * Master event ID
     *
     * @var int
     */
    private $master_event_id;

    /**
     * Recurrence rule string
     *
     * @var string
     */
    private $rule;

    /**
     * Start date
     *
     * @var DateTime
     */
    private $start_date;

    /**
     * Availability schedule for availability-based recurrence
     *
     * Keyed by 3-letter day abbreviation (mon, tue, ...), each value is an
     * array of time blocks. A day with a non-empty array means the event
     * occurs on that day of the week.
     *
     * @var array
     */
    private $availability;

    /**
     * Constructor
     *
     * @since PBOOKING_SINCE
     *
     * @param int    $master_event_id Master event post ID.
     * @param string $rule            Recurrence rule string.
     * @param string $start_date      Start date in Y-m-d format.
     * @param array  $availability    Availability schedule (for availability-based recurrence).
     */
    public function __construct( $master_event_id, $rule, $start_date, $availability = [] ) {
        $this->master_event_id = $master_event_id;
        $this->rule            = $rule;
        $this->start_date      = new DateTime( $start_date );
        $this->availability    = $availability;
    }

    /**
     * Generate virtual instances for a date range
     *
     * @since PBOOKING_SINCE
     *
     * @param string $range_start Start of range (Y-m-d).
     * @param string $range_end   End of range (Y-m-d).
     *
     * @return array Array of Virtual_Instance objects.
     */
    public function generate_instances( $range_start, $range_end ) {
        $instances = [];
        $rule_data = $this->parse_rule();

        if ( is_wp_error( $rule_data ) ) {
            return [];
        }

        // Availability-based recurrence: step day-by-day through the range
        // and emit an instance for each day whose day-of-week has entries in
        // the availability schedule.
        if ( 'AVAILABILITY' === $rule_data['freq'] ) {
            return $this->generate_availability_instances( $range_start, $range_end );
        }

        $current_date = clone $this->start_date;
        $end_date     = new DateTime( $range_end );
        $start_range  = new DateTime( $range_start );
        $count        = 0;
        $max_count    = $rule_data['count'] ?? 1000; // Safety limit

        // Fast-forward to range start if needed
        if ( $current_date < $start_range ) {
            $current_date = $this->fast_forward_to_range( $current_date, $start_range, $rule_data );
        }

        while ( $current_date <= $end_date && $count < $max_count ) {
            // Create virtual instance
            $instance_id = $this->generate_instance_id( $current_date );
            $instances[] = new Virtual_Instance(
                $instance_id,
                $this->master_event_id,
                $current_date->format( 'Y-m-d' )
            );

            // Calculate next occurrence
            $current_date = $this->calculate_next_occurrence( $current_date, $rule_data );
            ++$count;

            // Stop if we've reached the end date specified in rule
            if ( ! empty( $rule_data['until'] ) && $current_date > new DateTime( $rule_data['until'] ) ) {
                break;
            }
        }

        return $instances;
    }

    /**
     * Generate instances based on the availability schedule
     *
     * Walks day-by-day from the later of (event start, range start) through
     * range end.  For each day whose 3-letter abbreviation (mon, tue, …) has
     * a non-empty array in $this->availability, a Virtual_Instance is created.
     *
     * @since PBOOKING_SINCE
     *
     * @param string $range_start Start of range (Y-m-d).
     * @param string $range_end   End of range (Y-m-d).
     *
     * @return array Array of Virtual_Instance objects.
     */
    private function generate_availability_instances( $range_start, $range_end ) {
        $instances   = [];
        $start_range = new DateTime( $range_start );
        $end_date    = new DateTime( $range_end );

        // Begin from whichever is later: the event start date or the range start.
        $current_date = clone $this->start_date;
        if ( $current_date < $start_range ) {
            $current_date = clone $start_range;
        }

        while ( $current_date <= $end_date ) {
            // ISO-8601 numeric day: 1 = Mon … 7 = Sun
            $day_key = self::DAY_MAP[ (int) $current_date->format( 'N' ) ];

            if ( ! empty( $this->availability[ $day_key ] ) ) {
                $instance_id = $this->generate_instance_id( $current_date );
                $instances[] = new Virtual_Instance(
                    $instance_id,
                    $this->master_event_id,
                    $current_date->format( 'Y-m-d' )
                );
            }

            $current_date->add( new DateInterval( 'P1D' ) );
        }

        return $instances;
    }

    /**
     * Parse recurrence rule string
     *
     * @since PBOOKING_SINCE
     *
     * @return array|WP_Error Parsed rule data or error.
     */
    private function parse_rule() {
        if ( empty( $this->rule ) ) {
            return new WP_Error( 'empty_rule', 'Recurrence rule is empty' );
        }

        // Handle simple pattern format or RFC-style format
        if ( in_array( strtoupper( $this->rule ), array_keys( self::FREQUENCIES ), true ) ) {
            // Simple format: "DAILY", "WEEKLY", "MONTHLY"
            return [
                'freq'  => strtoupper( $this->rule ),
                'count' => 52, // Default reasonable limit for recurring events.
            ];
        }

        // RFC 5545 style: "FREQ=WEEKLY;COUNT=10;INTERVAL=1"
        $rule_parts = [];
        $parts      = explode( ';', $this->rule );

        foreach ( $parts as $part ) {
            if ( strpos( $part, '=' ) !== false ) {
                list($key, $value)                = explode( '=', $part, 2 );
                $rule_parts[ strtolower( $key ) ] = $value;
            }
        }

        // Validate frequency
        if ( empty( $rule_parts['freq'] ) || ! array_key_exists( strtoupper( $rule_parts['freq'] ), self::FREQUENCIES ) ) {
            return new WP_Error( 'invalid_frequency', 'Invalid or missing frequency' );
        }

        return [
            'freq'     => strtoupper( $rule_parts['freq'] ),
            'interval' => intval( $rule_parts['interval'] ?? 1 ),
            'count'    => intval( $rule_parts['count'] ?? 1000 ),
            'until'    => $rule_parts['until'] ?? null,
            'byday'    => $rule_parts['byday'] ?? null,
        ];
    }

    /**
     * Calculate next occurrence based on rule
     *
     * @since PBOOKING_SINCE
     *
     * @param DateTime $current_date Current date.
     * @param array    $rule_data    Parsed rule data.
     *
     * @return DateTime Next occurrence date.
     */
    private function calculate_next_occurrence( $current_date, $rule_data ) {
        $interval = $rule_data['interval'] ?? 1;

        switch ( $rule_data['freq'] ) {
            case 'DAILY':
                return $current_date->add( new DateInterval( "P{$interval}D" ) );

            case 'WEEKLY':
                return $current_date->add( new DateInterval( "P{$interval}W" ) );

            case 'MONTHLY':
                return $current_date->add( new DateInterval( "P{$interval}M" ) );

            case 'YEARLY':
                return $current_date->add( new DateInterval( "P{$interval}Y" ) );

            default:
                return $current_date->add( new DateInterval( 'P1D' ) );
        }
    }

    /**
     * Fast-forward to range start for performance
     *
     * @since PBOOKING_SINCE
     *
     * @param DateTime $start_date  Event start date.
     * @param DateTime $range_start Range start date.
     * @param array    $rule_data   Parsed rule data.
     *
     * @return DateTime Fast-forwarded date.
     */
    private function fast_forward_to_range( $start_date, $range_start, $rule_data ) {
        $interval = $rule_data['interval'] ?? 1;
        $diff     = $start_date->diff( $range_start );

        switch ( $rule_data['freq'] ) {
            case 'DAILY':
                $skip_count = floor( $diff->days / $interval );
                return $start_date->add( new DateInterval( 'P' . ( $skip_count * $interval ) . 'D' ) );

            case 'WEEKLY':
                $skip_weeks = floor( $diff->days / ( 7 * $interval ) );
                return $start_date->add( new DateInterval( 'P' . ( $skip_weeks * $interval ) . 'W' ) );

            case 'MONTHLY':
                $skip_months = $diff->y * 12 + $diff->m;
                $skip_months = floor( $skip_months / $interval ) * $interval;
                return $start_date->add( new DateInterval( "P{$skip_months}M" ) );

            case 'YEARLY':
                $skip_years = $diff->y;
                $skip_years = floor( $skip_years / $interval ) * $interval;
                return $start_date->add( new DateInterval( "P{$skip_years}Y" ) );

            default:
                return $start_date;
        }
    }

    /**
     * Generate unique instance ID
     *
     * @since PBOOKING_SINCE
     *
     * @param DateTime $date Instance date.
     *
     * @return string Unique instance identifier.
     */
    private function generate_instance_id( $date ) {
        return $this->master_event_id . '_' . $date->format( 'Y-m-d' );
    }

    /**
     * Create rule from simple pattern
     *
     * @since PBOOKING_SINCE
     *
     * @param string $pattern Simple pattern (daily, weekly, monthly).
     * @param int    $count   Number of occurrences.
     * @param string $until   End date (Y-m-d format).
     *
     * @return string RFC-style rule string.
     */
    public static function create_rule( $pattern, $count = null, $until = null ) {
        $freq = strtoupper( $pattern );

        if ( ! array_key_exists( $freq, self::FREQUENCIES ) ) {
            return '';
        }

        $rule_parts = [ "FREQ={$freq}" ];

        if ( $count ) {
            $rule_parts[] = "COUNT={$count}";
        }

        if ( $until ) {
            $rule_parts[] = "UNTIL={$until}";
        }

        return implode( ';', $rule_parts );
    }

    /**
     * Get supported frequencies
     *
     * @since PBOOKING_SINCE
     *
     * @return array Supported frequencies.
     */
    public static function get_supported_frequencies() {
        return self::FREQUENCIES;
    }
}
