<?php
/**
 * Contact Model for database operations.
 *
 * @package BoostBot
 * @since   1.0.0
 */

// Prevent direct access
// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Class BoostBot_Contact_Model
 *
 * Handles CRUD operations for contacts.
 *
 * @since 1.0.0
 */
class BoostBot_Contact_Model extends BoostBot_Base_Model {

    /**
     * Constructor.
     */
    public function __construct() {
        parent::__construct();
        $this->table_name = self::sanitize_identifier( $this->wpdb->prefix . 'boostbot_contacts' );
        $this->fillable   = array(
            'list_id',
            'name',
            'title',
            'department',
            'phone',
            'email',
            'whatsapp',
            'description',
            'meeting_availability',
            'specialties',
            'sort_order',
        );
        $this->column_types = array(
            'id'         => 'bigint',
            'list_id'    => 'bigint',
            'sort_order' => 'int',
        );
    }

    /**
     * Get contacts by list ID.
     *
     * @param int $list_id The list ID.
     * @return array Array of contacts.
     */
    public function get_by_list( $list_id ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $this->wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $this->wpdb->prepare(
                "SELECT * FROM {$this->table_name} WHERE list_id = %d ORDER BY sort_order ASC, name ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $list_id
            )
        );
    }

    /**
     * Update sort order for contacts.
     *
     * @param array $contacts_order Array of contact IDs in desired order.
     * @return bool True on success.
     */
    public function update_order( $contacts_order ) {
        if ( ! is_array( $contacts_order ) ) {
            return false;
        }

        foreach ( $contacts_order as $index => $contact_id ) {
            $this->update( absint( $contact_id ), array( 'sort_order' => $index ) );
        }

        return true;
    }

    /**
     * Delete all contacts in a list.
     *
     * @param int $list_id The list ID.
     * @return int|false Number of deleted rows or false on error.
     */
    public function delete_by_list( $list_id ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $this->wpdb->delete(
            $this->table_name,
            array( 'list_id' => $list_id ),
            array( '%d' )
        );
    }

    /**
     * Search contacts by name or email.
     *
     * @param int    $list_id The list ID.
     * @param string $search  The search query.
     * @return array Array of matching contacts.
     */
    public function search( $list_id, $search ) {
        $search_like = '%' . $this->wpdb->esc_like( $search ) . '%';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $this->wpdb->get_results(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $this->wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                "SELECT * FROM {$this->table_name} 
                WHERE list_id = %d 
                AND (name LIKE %s OR email LIKE %s OR phone LIKE %s)
                ORDER BY sort_order ASC, name ASC", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $list_id,
                $search_like,
                $search_like,
                $search_like
            )
        );
    }

    /**
     * Get the next sort order for a list.
     *
     * @param int $list_id The list ID.
     * @return int The next sort order.
     */
    public function get_next_sort_order( $list_id ) {
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $max = $this->wpdb->get_var(
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
            $this->wpdb->prepare(
                "SELECT MAX(sort_order) FROM {$this->table_name} WHERE list_id = %d", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
                $list_id
            )
        );

        return ( null === $max ) ? 0 : ( (int) $max + 1 );
    }

    /**
     * Validate contact data.
     *
     * @param array    $data The data to validate.
     * @param int|null $id   Optional. The record ID for updates.
     * @return true|WP_Error True if valid, WP_Error otherwise.
     */
    public function validate( $data, $id = null ) {
        $errors = new WP_Error();

        // Name is required for new contacts; for updates, only validate if provided
        if ( ! $id && empty( $data['name'] ) ) {
            $errors->add( 'name_required', __( 'Contact name is required.', 'boostbot' ) );
        }
        if ( $id && array_key_exists( 'name', $data ) && empty( $data['name'] ) ) {
            $errors->add( 'name_required', __( 'Contact name is required.', 'boostbot' ) );
        }

        // List ID is required for new contacts
        if ( ! $id && empty( $data['list_id'] ) ) {
            $errors->add( 'list_required', __( 'Contact list is required.', 'boostbot' ) );
        }

        // Validate email format if provided
        if ( ! empty( $data['email'] ) && ! is_email( $data['email'] ) ) {
            $errors->add( 'invalid_email', __( 'Invalid email address.', 'boostbot' ) );
        }

        if ( $errors->has_errors() ) {
            return $errors;
        }

        return true;
    }

    /**
     * Sanitize contact data.
     *
     * @param array $data The data to sanitize.
     * @return array The sanitized data.
     */
    public function sanitize( $data ) {
        $sanitized = array();

        if ( isset( $data['list_id'] ) ) {
            $sanitized['list_id'] = absint( $data['list_id'] );
        }

        if ( isset( $data['name'] ) ) {
            $sanitized['name'] = sanitize_text_field( $data['name'] );
        }

        if ( isset( $data['title'] ) ) {
            $sanitized['title'] = sanitize_text_field( $data['title'] );
        }

        if ( isset( $data['department'] ) ) {
            $sanitized['department'] = sanitize_text_field( $data['department'] );
        }

        if ( isset( $data['phone'] ) ) {
            $sanitized['phone'] = sanitize_text_field( $data['phone'] );
        }

        if ( isset( $data['email'] ) ) {
            $sanitized['email'] = sanitize_email( $data['email'] );
        }

        if ( isset( $data['whatsapp'] ) ) {
            $sanitized['whatsapp'] = sanitize_text_field( $data['whatsapp'] );
        }

        if ( isset( $data['description'] ) ) {
            $sanitized['description'] = sanitize_textarea_field( $data['description'] );
        }

        if ( isset( $data['meeting_availability'] ) ) {
            $sanitized['meeting_availability'] = is_string( $data['meeting_availability'] )
                ? $data['meeting_availability']
                : wp_json_encode( $data['meeting_availability'] );
        }

        if ( isset( $data['specialties'] ) ) {
            $sanitized['specialties'] = is_string( $data['specialties'] )
                ? $data['specialties']
                : wp_json_encode( $data['specialties'] );
        }

        if ( isset( $data['sort_order'] ) ) {
            $sanitized['sort_order'] = absint( $data['sort_order'] );
        }

        return $sanitized;
    }

    /**
     * Create a new contact with auto sort order.
     *
     * @param array $data The data to insert.
     * @return int|false The new record ID or false on failure.
     */
    public function create( $data ) {
        // Set sort order if not provided
        if ( ! isset( $data['sort_order'] ) && ! empty( $data['list_id'] ) ) {
            $data['sort_order'] = $this->get_next_sort_order( $data['list_id'] );
        }

        return parent::create( $data );
    }

    /**
     * Get parsed meeting availability schedule for a contact.
     *
     * @param int $contact_id The contact ID.
     * @return array|null Parsed availability data or null if not set.
     */
    public function get_meeting_availability( $contact_id ) {
        $contact = $this->get( $contact_id );
        if ( ! $contact || empty( $contact->meeting_availability ) ) {
            return null;
        }

        $data = json_decode( $contact->meeting_availability, true );
        return is_array( $data ) ? $data : null;
    }

    /**
     * Get the meeting duration (minutes) for a contact.
     *
     * @param int $contact_id The contact ID.
     * @return int Meeting duration in minutes (default 30).
     */
    public function get_meeting_duration( $contact_id ) {
        $availability = $this->get_meeting_availability( $contact_id );
        if ( $availability && ! empty( $availability['meeting_duration'] ) ) {
            return absint( $availability['meeting_duration'] );
        }
        return 30; // Default 30 minutes.
    }

    /**
     * Get the day schedule for a contact on a specific day of the week.
     *
     * @param int $contact_id  The contact ID.
     * @param int $day_of_week Day of week (0 = Sunday, 6 = Saturday).
     * @return array Array of time ranges, e.g. [['start' => '09:00', 'end' => '12:00'], ...].
     */
    public function get_day_schedule( $contact_id, $day_of_week ) {
        $availability = $this->get_meeting_availability( $contact_id );
        if ( ! $availability || empty( $availability['days'] ) ) {
            return array();
        }

        $day_key = (string) $day_of_week;
        if ( ! isset( $availability['days'][ $day_key ] ) || ! is_array( $availability['days'][ $day_key ] ) ) {
            return array();
        }

        return $availability['days'][ $day_key ];
    }

    /**
     * Generate available time slots for a contact on a given day.
     *
     * Slots are spaced by $slot_interval minutes (defaults to the contact's meeting_duration).
     * A slot is only included if the full meeting_duration fits within the availability range,
     * i.e. slot_start + meeting_duration <= range_end. This prevents showing a 16:45 slot
     * when a 90-minute meeting would extend past a 17:00 availability end.
     *
     * @param int $contact_id   The contact ID.
     * @param int $day_of_week  Day of week (0 = Sunday, 6 = Saturday).
     * @param int $slot_interval Optional. Minutes between slots. 0 = use meeting_duration.
     * @return array Array of start time strings, e.g. ['09:00', '09:30', ...].
     */
    public function generate_time_slots( $contact_id, $day_of_week, $slot_interval = 0 ) {
        $ranges   = $this->get_day_schedule( $contact_id, $day_of_week );
        $duration = $this->get_meeting_duration( $contact_id );
        $step     = ( $slot_interval > 0 ) ? $slot_interval : $duration;
        $slots    = array();

        if ( empty( $ranges ) || $duration < 15 ) {
            return $slots;
        }

        // Fix #7 — Merge overlapping ranges before generating slots to avoid duplicates.
        $ranges = $this->merge_overlapping_ranges( $ranges );

        foreach ( $ranges as $range ) {
            if ( empty( $range['start'] ) || empty( $range['end'] ) ) {
                continue;
            }

            $start_parts = explode( ':', $range['start'] );
            $end_parts   = explode( ':', $range['end'] );

            if ( count( $start_parts ) < 2 || count( $end_parts ) < 2 ) {
                continue;
            }

            $start_minutes = (int) $start_parts[0] * 60 + (int) $start_parts[1];
            $end_minutes   = (int) $end_parts[0] * 60 + (int) $end_parts[1];

            // Step by $step, but validate that the full meeting_duration fits
            for ( $m = $start_minutes; $m + $duration <= $end_minutes; $m += $step ) {
                $h       = intdiv( $m, 60 );
                $min     = $m % 60;
                $slots[] = sprintf( '%02d:%02d', $h, $min );
            }
        }

        // Deduplicate (just in case) and sort
        $slots = array_values( array_unique( $slots ) );
        sort( $slots );

        return $slots;
    }

    /**
     * Merge overlapping or adjacent time ranges.
     *
     * E.g. [09:00-12:00, 11:00-14:00] → [09:00-14:00].
     *
     * @param array $ranges Array of ['start' => 'HH:MM', 'end' => 'HH:MM'].
     * @return array Merged ranges.
     */
    private function merge_overlapping_ranges( array $ranges ): array {
        if ( count( $ranges ) <= 1 ) {
            return $ranges;
        }

        // Convert to minutes for sorting/comparison
        $intervals = array();
        foreach ( $ranges as $r ) {
            if ( empty( $r['start'] ) || empty( $r['end'] ) ) {
                continue;
            }
            $s = explode( ':', $r['start'] );
            $e = explode( ':', $r['end'] );
            if ( count( $s ) < 2 || count( $e ) < 2 ) {
                continue;
            }
            $intervals[] = array(
                'start' => (int) $s[0] * 60 + (int) $s[1],
                'end'   => (int) $e[0] * 60 + (int) $e[1],
            );
        }

        // Sort by start time
        usort( $intervals, function ( $a, $b ) {
            return $a['start'] - $b['start'];
        } );

        $merged = array( $intervals[0] );
        for ( $i = 1; $i < count( $intervals ); $i++ ) {
            $last = &$merged[ count( $merged ) - 1 ];
            if ( $intervals[ $i ]['start'] <= $last['end'] ) {
                // Overlapping or adjacent — extend
                $last['end'] = max( $last['end'], $intervals[ $i ]['end'] );
            } else {
                $merged[] = $intervals[ $i ];
            }
        }

        // Convert back to HH:MM strings
        $result = array();
        foreach ( $merged as $m ) {
            $result[] = array(
                'start' => sprintf( '%02d:%02d', intdiv( $m['start'], 60 ), $m['start'] % 60 ),
                'end'   => sprintf( '%02d:%02d', intdiv( $m['end'], 60 ), $m['end'] % 60 ),
            );
        }

        return $result;
    }
}





