<?php
/**
 * External Calendar Model
 * 
 * Handles external calendar URLs (iCal/ICS format) for availability integration
 *
 * @package Freelance_Studio
 */

class FS_External_Calendar {

    /**
     * Get all external calendars
     *
     * @param array $args Query arguments
     * @return array
     */
    public static function get_all($args = array()) {
        global $wpdb;
        
        $defaults = array(
            'is_active' => null,
            'affects_availability' => null,
            'orderby' => 'name',
            'order' => 'ASC',
        );
        
        $args = wp_parse_args($args, $defaults);
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        $where = array('1=1');
        
        if ($args['is_active'] !== null) {
            $where[] = $wpdb->prepare('is_active = %d', $args['is_active']);
        }
        
        if ($args['affects_availability'] !== null) {
            $where[] = $wpdb->prepare('affects_availability = %d', $args['affects_availability']);
        }
        
        $where_clause = implode(' AND ', $where);
        $orderby = in_array($args['orderby'], array('name', 'created_at', 'last_synced')) ? $args['orderby'] : 'name';
        $order = strtoupper($args['order']) === 'DESC' ? 'DESC' : 'ASC';
        
        return $wpdb->get_results(
            "SELECT * FROM {$table} WHERE {$where_clause} ORDER BY {$orderby} {$order}"
        );
    }

    /**
     * Get a single external calendar
     *
     * @param int $id Calendar ID
     * @return object|null
     */
    public static function get($id) {
        global $wpdb;
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        return $wpdb->get_row($wpdb->prepare(
            "SELECT * FROM {$table} WHERE id = %d",
            $id
        ));
    }

    /**
     * Create a new external calendar
     *
     * @param array $data Calendar data
     * @return int|false Insert ID or false on failure
     */
    public static function create($data) {
        global $wpdb;
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        $result = $wpdb->insert(
            $table,
            array(
                'name' => sanitize_text_field($data['name']),
                'url' => esc_url_raw($data['url']),
                'color' => isset($data['color']) ? sanitize_hex_color($data['color']) : '#6b7280',
                'is_active' => isset($data['is_active']) ? intval($data['is_active']) : 1,
                'affects_availability' => isset($data['affects_availability']) ? intval($data['affects_availability']) : 1,
            ),
            array('%s', '%s', '%s', '%d', '%d')
        );
        
        if ($result) {
            $id = $wpdb->insert_id;
            // Sync immediately
            self::sync_calendar($id);
            return $id;
        }
        
        return false;
    }

    /**
     * Update an external calendar
     *
     * @param int   $id   Calendar ID
     * @param array $data Calendar data
     * @return int|false
     */
    public static function update($id, $data) {
        global $wpdb;
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        $update_data = array();
        $format = array();
        
        if (isset($data['name'])) {
            $update_data['name'] = sanitize_text_field($data['name']);
            $format[] = '%s';
        }
        
        if (isset($data['url'])) {
            $update_data['url'] = esc_url_raw($data['url']);
            $format[] = '%s';
        }
        
        if (isset($data['color'])) {
            $update_data['color'] = sanitize_hex_color($data['color']);
            $format[] = '%s';
        }
        
        if (isset($data['is_active'])) {
            $update_data['is_active'] = intval($data['is_active']);
            $format[] = '%d';
        }
        
        if (isset($data['affects_availability'])) {
            $update_data['affects_availability'] = intval($data['affects_availability']);
            $format[] = '%d';
        }
        
        if (empty($update_data)) {
            return false;
        }
        
        return $wpdb->update($table, $update_data, array('id' => $id), $format, array('%d'));
    }

    /**
     * Delete an external calendar
     *
     * @param int $id Calendar ID
     * @return int|false
     */
    public static function delete($id) {
        global $wpdb;
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        return $wpdb->delete($table, array('id' => $id), array('%d'));
    }

    /**
     * Sync a calendar from its URL
     *
     * @param int $id Calendar ID
     * @return bool Success
     */
    public static function sync_calendar($id) {
        global $wpdb;
        
        $calendar = self::get($id);
        if (!$calendar) {
            return false;
        }
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        // Fetch the ICS file
        $response = wp_remote_get($calendar->url, array(
            'timeout' => 30,
            'sslverify' => true,
        ));
        
        if (is_wp_error($response)) {
            $wpdb->update(
                $table,
                array('sync_error' => $response->get_error_message()),
                array('id' => $id),
                array('%s'),
                array('%d')
            );
            return false;
        }
        
        $body = wp_remote_retrieve_body($response);
        $status_code = wp_remote_retrieve_response_code($response);
        
        if ($status_code !== 200 || empty($body)) {
            $wpdb->update(
                $table,
                array('sync_error' => 'Failed to fetch calendar (HTTP ' . $status_code . ')'),
                array('id' => $id),
                array('%s'),
                array('%d')
            );
            return false;
        }
        
        // Parse the ICS content
        $events = self::parse_ics($body);
        
        // Store cached events
        $wpdb->update(
            $table,
            array(
                'cached_events' => json_encode($events),
                'last_synced' => current_time('mysql'),
                'sync_error' => null,
            ),
            array('id' => $id),
            array('%s', '%s', '%s'),
            array('%d')
        );
        
        return true;
    }

    /**
     * Sync all active calendars
     *
     * @return int Number of calendars synced
     */
    public static function sync_all() {
        $calendars = self::get_all(array('is_active' => 1));
        $synced = 0;
        
        foreach ($calendars as $calendar) {
            if (self::sync_calendar($calendar->id)) {
                $synced++;
            }
        }
        
        return $synced;
    }

    /**
     * Parse ICS content into events array
     *
     * @param string $ics_content Raw ICS content
     * @return array Array of events
     */
    public static function parse_ics($ics_content) {
        $events = array();
        $recurring_events = array();
        
        // Normalize line endings and handle line folding (lines starting with space are continuations)
        $ics_content = str_replace("\r\n", "\n", $ics_content);
        $ics_content = str_replace("\r", "\n", $ics_content);
        $ics_content = preg_replace("/\n[ \t]/", "", $ics_content);
        
        $lines = explode("\n", $ics_content);
        $event = null;
        $in_event = false;
        
        foreach ($lines as $line) {
            $line = trim($line);
            
            if ($line === 'BEGIN:VEVENT') {
                $in_event = true;
                $event = array(
                    'title' => '',
                    'start' => null,
                    'end' => null,
                    'all_day' => false,
                    'location' => '',
                    'description' => '',
                    'rrule' => null,
                    'duration_seconds' => null,
                );
            } elseif ($line === 'END:VEVENT') {
                if ($event && $event['start']) {
                    // If no end time, use start time + 1 hour for timed events, or same day for all-day
                    if (!$event['end']) {
                        if ($event['all_day']) {
                            $event['end'] = $event['start'];
                        } else {
                            $event['end'] = gmdate('Y-m-d H:i:s', strtotime($event['start'] . ' +1 hour'));
                        }
                    }
                    
                    // Calculate duration for recurring events
                    $event['duration_seconds'] = strtotime($event['end']) - strtotime($event['start']);
                    
                    // If recurring, expand occurrences; otherwise add single event
                    if ($event['rrule']) {
                        $recurring_events[] = $event;
                    } else {
                        unset($event['rrule'], $event['duration_seconds']);
                        $events[] = $event;
                    }
                }
                $in_event = false;
                $event = null;
            } elseif ($in_event && $event) {
                // Parse event properties - handle properties with parameters (e.g., DTSTART;TZID=...)
                if (strpos($line, 'SUMMARY') === 0) {
                    $event['title'] = self::decode_ics_value(self::extract_ics_value($line));
                } elseif (strpos($line, 'DTSTART') === 0) {
                    $value = self::extract_ics_value($line);
                    $parsed = self::parse_ics_date($value);
                    $event['start'] = $parsed['datetime'];
                    $event['all_day'] = $parsed['all_day'] || (strpos($line, 'VALUE=DATE') !== false);
                } elseif (strpos($line, 'DTEND') === 0) {
                    $value = self::extract_ics_value($line);
                    $parsed = self::parse_ics_date($value);
                    $event['end'] = $parsed['datetime'];
                } elseif (strpos($line, 'LOCATION') === 0) {
                    $event['location'] = self::decode_ics_value(self::extract_ics_value($line));
                } elseif (strpos($line, 'DESCRIPTION') === 0) {
                    $event['description'] = self::decode_ics_value(self::extract_ics_value($line));
                } elseif (strpos($line, 'RRULE:') === 0) {
                    $event['rrule'] = substr($line, 6);
                }
            }
        }
        
        // Expand recurring events (limit to 1 year ahead for performance)
        $expansion_limit = strtotime('+1 year');
        foreach ($recurring_events as $revent) {
            $occurrences = self::expand_rrule($revent, $expansion_limit);
            $events = array_merge($events, $occurrences);
        }
        
        return $events;
    }
    
    /**
     * Expand a recurring event into individual occurrences
     */
    private static function expand_rrule($event, $until_timestamp) {
        $occurrences = array();
        $rrule = self::parse_rrule($event['rrule']);
        
        if (!$rrule || !isset($rrule['FREQ'])) {
            return $occurrences;
        }
        
        $start_ts = strtotime($event['start']);
        $duration = $event['duration_seconds'];
        $count = isset($rrule['COUNT']) ? intval($rrule['COUNT']) : 365; // Default max
        $interval = isset($rrule['INTERVAL']) ? intval($rrule['INTERVAL']) : 1;
        $until = isset($rrule['UNTIL']) ? strtotime(self::parse_ics_date($rrule['UNTIL'])['datetime']) : $until_timestamp;
        $until = min($until, $until_timestamp);
        
        // Get BYDAY for weekly recurrence
        $by_day = isset($rrule['BYDAY']) ? explode(',', $rrule['BYDAY']) : null;
        
        $current_ts = $start_ts;
        $generated = 0;
        
        while ($current_ts <= $until && $generated < $count) {
            // For weekly with BYDAY, we need special handling
            if ($rrule['FREQ'] === 'WEEKLY' && $by_day) {
                // Get the week start (Sunday)
                $week_start = strtotime('sunday this week', $current_ts);
                if (gmdate('w', $current_ts) == 0) {
                    $week_start = $current_ts - (gmdate('w', $current_ts) * 86400);
                }
                
                foreach ($by_day as $day_abbr) {
                    $day_map = array('SU' => 0, 'MO' => 1, 'TU' => 2, 'WE' => 3, 'TH' => 4, 'FR' => 5, 'SA' => 6);
                    $day_abbr = strtoupper(preg_replace('/[^A-Z]/', '', $day_abbr));
                    if (!isset($day_map[$day_abbr])) continue;
                    
                    $day_offset = $day_map[$day_abbr];
                    $occurrence_date = strtotime('+' . $day_offset . ' days', $week_start);
                    
                    // Apply time from original event
                    $time_part = gmdate('H:i:s', $start_ts);
                    $occurrence_ts = strtotime(gmdate('Y-m-d', $occurrence_date) . ' ' . $time_part);
                    
                    if ($occurrence_ts >= $start_ts && $occurrence_ts <= $until && $generated < $count) {
                        $occurrences[] = array(
                            'title' => $event['title'],
                            'start' => gmdate('Y-m-d H:i:s', $occurrence_ts),
                            'end' => gmdate('Y-m-d H:i:s', $occurrence_ts + $duration),
                            'all_day' => $event['all_day'],
                            'location' => $event['location'],
                            'description' => $event['description'],
                        );
                        $generated++;
                    }
                }
                // Move to next interval
                $current_ts = strtotime('+' . $interval . ' week', $week_start + 86400); // +1 day to get past Sunday
            } else {
                // Simple recurrence (daily, weekly without BYDAY, monthly, yearly)
                $occurrences[] = array(
                    'title' => $event['title'],
                    'start' => gmdate('Y-m-d H:i:s', $current_ts),
                    'end' => gmdate('Y-m-d H:i:s', $current_ts + $duration),
                    'all_day' => $event['all_day'],
                    'location' => $event['location'],
                    'description' => $event['description'],
                );
                $generated++;
                
                // Calculate next occurrence
                switch ($rrule['FREQ']) {
                    case 'DAILY':
                        $current_ts = strtotime('+' . $interval . ' day', $current_ts);
                        break;
                    case 'WEEKLY':
                        $current_ts = strtotime('+' . $interval . ' week', $current_ts);
                        break;
                    case 'MONTHLY':
                        $current_ts = strtotime('+' . $interval . ' month', $current_ts);
                        break;
                    case 'YEARLY':
                        $current_ts = strtotime('+' . $interval . ' year', $current_ts);
                        break;
                    default:
                        break 2;
                }
            }
        }
        
        return $occurrences;
    }
    
    /**
     * Parse RRULE string into array
     */
    private static function parse_rrule($rrule_str) {
        $parts = explode(';', $rrule_str);
        $rrule = array();
        
        foreach ($parts as $part) {
            if (strpos($part, '=') !== false) {
                list($key, $value) = explode('=', $part, 2);
                $rrule[strtoupper($key)] = $value;
            }
        }
        
        return $rrule;
    }

    /**
     * Extract value from ICS property line (handles parameters like DTSTART;TZID=America/New_York:20250115T090000)
     */
    private static function extract_ics_value($line) {
        if (strpos($line, ':') !== false) {
            return substr($line, strrpos($line, ':') + 1);
        }
        return '';
    }

    /**
     * Parse ICS date format - returns array with datetime and all_day flag
     */
    private static function parse_ics_date($date_str) {
        $result = array('datetime' => null, 'all_day' => false);
        
        $date_str = trim($date_str);
        $is_utc = (substr($date_str, -1) === 'Z');
        $date_str = preg_replace('/Z$/', '', $date_str);
        
        // Handle basic date format (YYYYMMDD) - all day event
        if (preg_match('/^(\d{8})$/', $date_str, $matches)) {
            $d = $matches[1];
            $result['datetime'] = substr($d, 0, 4) . '-' . substr($d, 4, 2) . '-' . substr($d, 6, 2);
            $result['all_day'] = true;
            return $result;
        }
        
        // Handle datetime format (YYYYMMDDTHHMMSS or YYYYMMDDTHHMMSSZ)
        if (preg_match('/^(\d{8})T(\d{6})/', $date_str, $matches)) {
            $d = $matches[1];
            $t = $matches[2];
            $date = substr($d, 0, 4) . '-' . substr($d, 4, 2) . '-' . substr($d, 6, 2);
            $time = substr($t, 0, 2) . ':' . substr($t, 2, 2) . ':' . substr($t, 4, 2);
            $datetime_str = $date . ' ' . $time;
            
            // Convert UTC to local time if needed
            if ($is_utc) {
                $utc_tz = new DateTimeZone('UTC');
                $local_tz = new DateTimeZone(wp_timezone_string());
                $dt = new DateTime($datetime_str, $utc_tz);
                $dt->setTimezone($local_tz);
                $result['datetime'] = $dt->format('Y-m-d H:i:s');
            } else {
                $result['datetime'] = $datetime_str;
            }
            return $result;
        }
        
        // Fallback - try to parse as-is
        $result['datetime'] = $date_str;
        return $result;
    }

    /**
     * Decode ICS escaped values
     */
    private static function decode_ics_value($value) {
        $value = str_replace(array('\\n', '\\,', '\\;', '\\\\'), array("\n", ',', ';', '\\'), $value);
        return $value;
    }

    /**
     * Get events from a calendar within a date range
     *
     * @param int    $id    Calendar ID
     * @param string $start Start date (Y-m-d)
     * @param string $end   End date (Y-m-d)
     * @return array
     */
    public static function get_events($id, $start, $end) {
        $calendar = self::get($id);
        if (!$calendar || !$calendar->cached_events) {
            return array();
        }
        
        $all_events = json_decode($calendar->cached_events ?? '', true);
        if (!is_array($all_events)) {
            return array();
        }
        
        $filtered = array();
        foreach ($all_events as $event) {
            $event_date = substr($event['start'], 0, 10);
            if ($event_date >= $start && $event_date <= $end) {
                $event['calendar_id'] = $id;
                $event['calendar_name'] = $calendar->name;
                $event['color'] = $calendar->color;
                $filtered[] = $event;
            }
        }
        
        return $filtered;
    }

    /**
     * Get all events from all active calendars within a date range
     *
     * @param string $start Start date (Y-m-d)
     * @param string $end   End date (Y-m-d)
     * @param bool   $affects_availability_only Only calendars that affect availability
     * @return array
     */
    public static function get_all_events($start, $end, $affects_availability_only = false) {
        $args = array('is_active' => 1);
        if ($affects_availability_only) {
            $args['affects_availability'] = 1;
        }
        
        $calendars = self::get_all($args);
        $all_events = array();
        
        foreach ($calendars as $calendar) {
            $events = self::get_events($calendar->id, $start, $end);
            $all_events = array_merge($all_events, $events);
        }
        
        // Sort by start date
        usort($all_events, function($a, $b) {
            return strcmp($a['start'], $b['start']);
        });
        
        return $all_events;
    }

    /**
     * Check if a time slot is blocked by external calendar events
     *
     * @param string $date      Date (Y-m-d)
     * @param string $start_time Start time (H:i)
     * @param string $end_time   End time (H:i)
     * @return bool
     */
    public static function is_time_blocked($date, $start_time, $end_time) {
        $events = self::get_all_events($date, $date, true);
        
        $check_start = strtotime($date . ' ' . $start_time);
        $check_end = strtotime($date . ' ' . $end_time);
        
        foreach ($events as $event) {
            // For all-day events, the whole day is blocked
            if ($event['all_day']) {
                return true;
            }
            
            $event_start = strtotime($event['start']);
            $event_end = $event['end'] ? strtotime($event['end']) : $event_start + 3600; // Default 1 hour
            
            // Check for overlap
            if ($check_start < $event_end && $check_end > $event_start) {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Get blocked time slots for a date
     *
     * @param string $date Date (Y-m-d)
     * @return array Array of blocked time ranges
     */
    public static function get_blocked_slots($date) {
        $events = self::get_all_events($date, $date, true);
        $blocked = array();
        
        foreach ($events as $event) {
            if ($event['all_day']) {
                $blocked[] = array(
                    'start' => '00:00',
                    'end' => '23:59',
                    'all_day' => true,
                    'title' => $event['title'],
                    'calendar' => $event['calendar_name'],
                );
            } else {
                $blocked[] = array(
                    'start' => gmdate('H:i', strtotime($event['start'])),
                    'end' => $event['end'] ? gmdate('H:i', strtotime($event['end'])) : gmdate('H:i', strtotime($event['start']) + 3600),
                    'all_day' => false,
                    'title' => $event['title'],
                    'calendar' => $event['calendar_name'],
                );
            }
        }
        
        return $blocked;
    }

    /**
     * Count external calendars
     *
     * @return int
     */
    public static function count() {
        global $wpdb;
        
        $table = $wpdb->prefix . 'fs_external_calendars';
        
        return (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
    }
}
