<?php
/**
 * Countdown tracking and configuration helpers.
 *
 * @package TIP_Countdown_Generator
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ! class_exists( 'tipcg_tracking' ) ) {

	/**
	 * Handles countdown configuration normalization and visitor tracking data.
	 */
	class tipcg_tracking {

/**
		 * Ensures the tracking cookie exists.
		 */
        public function set_tracking_cookie() {

            $this->ensure_tracking_cookie();

        }

/**
		 * Ensures the tracking cookie exists and returns its ID.
		 */
        protected function ensure_tracking_cookie() {
            $cookie_name = 'tipcg_start_time';
            if ( isset( $_COOKIE[ $cookie_name ] ) && ! empty( $_COOKIE[ $cookie_name ] ) ) {
                return sanitize_text_field( wp_unslash( $_COOKIE[ $cookie_name ] ) );
            }

            $cookie_value = uniqid( 'tipcg_', true );
            setcookie( $cookie_name, $cookie_value, time() + (30 * DAY_IN_SECONDS), COOKIEPATH, COOKIE_DOMAIN );
            $_COOKIE[ $cookie_name ] = $cookie_value;

            return $cookie_value;
        }

/**
		 * Converts a duration string to seconds.
		 *
		 * @param string $duration_str Duration string in DD:HH:MM:SS format.
		 */
        public function duration_to_seconds( $duration_str ) {
            $parts = $this->duration_string_to_parts( $duration_str );
            return $this->duration_parts_to_seconds( $parts );
        }

/**
		 * Converts numeric duration parts to seconds.
		 *
		 * @param array $parts Duration parts.
		 */
		protected function duration_parts_to_seconds( $parts ) {
			$days    = isset( $parts['days'] ) ? intval( $parts['days'] ) : 0;
			$hours   = isset( $parts['hours'] ) ? intval( $parts['hours'] ) : 0;
            $minutes = isset( $parts['minutes'] ) ? intval( $parts['minutes'] ) : 0;
            $seconds = isset( $parts['seconds'] ) ? intval( $parts['seconds'] ) : 0;

            return ( $days * DAY_IN_SECONDS ) + ( $hours * HOUR_IN_SECONDS ) + ( $minutes * MINUTE_IN_SECONDS ) + $seconds;
        }

/**
		 * Returns numeric parts from a DD:HH:MM:SS string.
		 *
		 * @param string $duration_str Duration string.
		 */
		protected function duration_string_to_parts( $duration_str ) {
			$parts = array(
				'days'    => 0,
                'hours'   => 0,
                'minutes' => 0,
                'seconds' => 0,
            );

            if ( preg_match( '/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})$/', $duration_str, $matches ) ) {
                $parts['days']    = intval( $matches[1] );
                $parts['hours']   = intval( $matches[2] );
                $parts['minutes'] = intval( $matches[3] );
                $parts['seconds'] = intval( $matches[4] );
            }

            return $parts;
        }

/**
		 * Returns the default countdown template aligned with the panel.
		 *
		 * @param int $index Countdown number.
		 */
        protected function default_countdown_template( $index = 1 ) {
            $index = ( $index > 0 ) ? $index : 1;

            return array(
                /* translators: %d: countdown number. */
                'name'              => sprintf( esc_html__( 'Countdown %d', 'tip-countdown-generator' ), $index ),
                'type'              => 'evergreen',
                'template'          => 'default',
                'duration'          => '01:00:00:00',
                'evergreen_days'    => 1,
                'evergreen_hours'   => 0,
                'evergreen_minutes' => 0,
                'evergreen_seconds' => 0,
                'tracking'          => 'cookie',
                'id'                => 'countdown_' . $index,
                'target_date'       => '',
                'number_font_size'  => 40,
                'label_font_size'   => 10,
                'cell_background_color' => '#f0f0f0',
                'cell_text_color'   => '#333333',
                'label_text_color'  => '#666666',
                'countdown_alignment' => 'center',
                'show_separators'   => 1,
                'separator_text_color' => '#333333',
                'cell_border_radius'=> 8,
                'margin_top'        => 0,
                'margin_bottom'     => 0,
            );
        }

/**
		 * Returns the DD:HH:MM:SS duration string from numeric parts.
		 *
		 * @param int $days Days.
		 * @param int $hours Hours.
		 * @param int $minutes Minutes.
		 * @param int $seconds Seconds.
		 */
        protected function format_duration_parts( $days, $hours, $minutes, $seconds ) {
            $days    = absint( $days );
            $hours   = absint( $hours );
            $minutes = absint( $minutes );
            $seconds = absint( $seconds );

            $day_part    = str_pad( (string) $days, 2, '0', STR_PAD_LEFT );
            $hour_part   = str_pad( (string) $hours, 2, '0', STR_PAD_LEFT );
            $minute_part = str_pad( (string) $minutes, 2, '0', STR_PAD_LEFT );
            $second_part = str_pad( (string) $seconds, 2, '0', STR_PAD_LEFT );

            return $day_part . ':' . $hour_part . ':' . $minute_part . ':' . $second_part;
        }

/**
		 * Normalizes a saved countdown by adding missing parts.
		 *
		 * @param array $data Countdown data.
		 * @param int   $index Countdown number.
		 */
        protected function normalize_countdown_config( $data, $index = 1 ) {
            $defaults = $this->default_countdown_template( $index );
            $merged   = wp_parse_args( $data, $defaults );

            $merged['type'] = isset( $merged['type'] ) ? sanitize_key( $merged['type'] ) : $defaults['type'];
            $merged['type'] = in_array( $merged['type'], array( 'evergreen', 'fixed' ), true ) ? $merged['type'] : $defaults['type'];

            $merged['template'] = isset( $merged['template'] ) ? sanitize_key( $merged['template'] ) : $defaults['template'];
            $merged['template'] = in_array( $merged['template'], array( 'default', 'flip_clock' ), true ) ? $merged['template'] : $defaults['template'];

            $merged['tracking'] = isset( $merged['tracking'] ) ? sanitize_key( $merged['tracking'] ) : $defaults['tracking'];
            $merged['tracking'] = in_array( $merged['tracking'], array( 'cookie', 'ip', 'cookie_ip' ), true ) ? $merged['tracking'] : $defaults['tracking'];

            if ( ! isset( $merged['evergreen_days'], $merged['evergreen_hours'], $merged['evergreen_minutes'], $merged['evergreen_seconds'] ) ) {
                $parts                     = $this->duration_string_to_parts( $merged['duration'] );
                $merged['evergreen_days']    = $parts['days'];
                $merged['evergreen_hours']   = $parts['hours'];
                $merged['evergreen_minutes'] = $parts['minutes'];
                $merged['evergreen_seconds'] = $parts['seconds'];
            }

            $merged['duration'] = $this->format_duration_parts(
                $merged['evergreen_days'],
                $merged['evergreen_hours'],
                $merged['evergreen_minutes'],
                $merged['evergreen_seconds']
            );

            $merged['number_font_size']       = min( 200, max( 8, absint( $merged['number_font_size'] ) ) );
            $merged['label_font_size']        = min( 80, max( 6, absint( $merged['label_font_size'] ) ) );
            $merged['cell_background_color']  = sanitize_hex_color( $merged['cell_background_color'] );
            $merged['cell_background_color']  = $merged['cell_background_color'] ? $merged['cell_background_color'] : $defaults['cell_background_color'];
            $merged['cell_text_color']        = sanitize_hex_color( $merged['cell_text_color'] );
            $merged['cell_text_color']        = $merged['cell_text_color'] ? $merged['cell_text_color'] : $defaults['cell_text_color'];
            if ( ! isset( $data['label_text_color'] ) || '' === $data['label_text_color'] ) {
                $merged['label_text_color'] = $merged['cell_text_color'];
            }
            $merged['label_text_color']       = sanitize_hex_color( $merged['label_text_color'] );
            $merged['label_text_color']       = $merged['label_text_color'] ? $merged['label_text_color'] : $defaults['label_text_color'];
            $merged['countdown_alignment']    = isset( $merged['countdown_alignment'] ) ? sanitize_key( $merged['countdown_alignment'] ) : $defaults['countdown_alignment'];
            $merged['countdown_alignment']    = in_array( $merged['countdown_alignment'], array( 'left', 'center', 'right' ), true ) ? $merged['countdown_alignment'] : $defaults['countdown_alignment'];
            $merged['show_separators']        = isset( $data['show_separators'] ) ? absint( $merged['show_separators'] ) : $defaults['show_separators'];
            $merged['show_separators']        = $merged['show_separators'] ? 1 : 0;
            if ( ! isset( $data['separator_text_color'] ) || '' === $data['separator_text_color'] ) {
                $merged['separator_text_color'] = $merged['cell_text_color'];
            }
            $merged['separator_text_color']   = sanitize_hex_color( $merged['separator_text_color'] );
            $merged['separator_text_color']   = $merged['separator_text_color'] ? $merged['separator_text_color'] : $defaults['separator_text_color'];
            $merged['cell_border_radius']     = min( 80, max( 0, absint( $merged['cell_border_radius'] ) ) );
            $merged['margin_top']             = min( 400, max( 0, absint( $merged['margin_top'] ) ) );
            $merged['margin_bottom']          = min( 400, max( 0, absint( $merged['margin_bottom'] ) ) );

            return $merged;
        }

/**
		 * Converts a generic slot number into a "slotX" key.
		 *
		 * @param mixed $slot Slot value.
		 */
        protected function resolve_slot_key( $slot ) {
            if ( is_numeric( $slot ) ) {
                $slot = 'slot' . absint( $slot );
            }

            $slot = sanitize_text_field( wp_unslash( $slot ) );
            if ( '' === $slot ) {
                return null;
            }

            if ( 0 === strpos( strtolower( $slot ), 'slot' ) ) {
                $number = absint( str_replace( 'slot', '', strtolower( $slot ) ) );
                return ( $number > 0 ) ? 'slot' . $number : null;
            }

            if ( preg_match( '/(\d+)$/', $slot, $matches ) ) {
                $number = absint( $matches[1] );
                return ( $number > 0 ) ? 'slot' . $number : null;
            }

            return null;
        }

/**
		 * Normalizes the ID used for markup and tracking keys.
		 *
		 * @param string $countdown_id Countdown ID.
		 */
        protected function normalize_countdown_id( $countdown_id ) {
            $countdown_id = sanitize_text_field( wp_unslash( $countdown_id ) );
            $countdown_id = preg_replace( '/[^A-Za-z0-9_-]/', '', $countdown_id );

            return ( '' !== $countdown_id ) ? $countdown_id : 'default';
        }

/**
		 * Retrieves a countdown from options using the slot key.
		 *
		 * @param string $slot_key Countdown slot key.
		 */
        protected function get_saved_countdown_config( $slot_key ) {
            $settings = get_option( TIPCG_OPTION_KEY );
            if ( ! is_array( $settings ) || ! isset( $settings['tipcg_countdowns'] ) || ! isset( $settings['tipcg_countdowns'][ $slot_key ] ) ) {
                return null;
            }

            $index = absint( str_replace( 'slot', '', $slot_key ) );
            $config = $settings['tipcg_countdowns'][ $slot_key ];
            return $this->normalize_countdown_config( $config, $index );
        }

/**
		 * Flattens the configuration into shortcode attributes.
		 *
		 * @param array $config Countdown configuration.
		 */
        protected function flatten_countdown_config( $config ) {
            return array(
                'type'                => $config['type'],
                'template'            => $config['template'],
                'duration'            => $config['duration'],
                'evergreen_days'      => $config['evergreen_days'],
                'evergreen_hours'     => $config['evergreen_hours'],
                'evergreen_minutes'   => $config['evergreen_minutes'],
                'evergreen_seconds'   => $config['evergreen_seconds'],
                'tracking'            => $config['tracking'],
                'id'                  => $config['id'],
                'target_date'         => $config['target_date'],
                'number_font_size'    => $config['number_font_size'],
                'label_font_size'     => $config['label_font_size'],
                'cell_background_color' => $config['cell_background_color'],
                'cell_text_color'     => $config['cell_text_color'],
                'label_text_color'    => $config['label_text_color'],
                'countdown_alignment' => $config['countdown_alignment'],
                'show_separators'     => $config['show_separators'],
                'separator_text_color' => $config['separator_text_color'],
                'cell_border_radius'  => $config['cell_border_radius'],
                'margin_top'          => $config['margin_top'],
                'margin_bottom'       => $config['margin_bottom'],
            );
        }

/**
		 * Converts a local datetime-local value into a timestamp.
		 *
		 * @param string $datetime_str Datetime-local value.
		 * @param string $timezone_identifier Timezone identifier or "site".
		 */
        protected function parse_datetime_to_timestamp( $datetime_str, $timezone_identifier = 'site' ) {
            if ( empty( $datetime_str ) ) {
                return null;
            }

            try {
                if ( 'site' !== $timezone_identifier ) {
                    $timezone = new DateTimeZone( $this->sanitize_timezone_identifier( $timezone_identifier ) );
                } elseif ( function_exists( 'wp_timezone' ) ) {
                    $timezone = wp_timezone();
                } else {
                    $tz_string = get_option( 'timezone_string' );
                    $timezone  = $tz_string ? new DateTimeZone( $tz_string ) : new DateTimeZone( 'UTC' );
                }
                $dt = date_create( $datetime_str, $timezone );
                if ( false === $dt ) {
                    return null;
                }
                return $dt->getTimestamp();
            } catch ( Exception $e ) {
                return null;
            }
        }

/**
		 * Sanitizes a timezone identifier against the PHP timezone list.
		 *
		 * @param string $timezone_identifier Timezone identifier.
		 * @param string $fallback Fallback timezone identifier.
		 */
        protected function sanitize_timezone_identifier( $timezone_identifier, $fallback = 'UTC' ) {
            static $valid_timezones = null;

            if ( null === $valid_timezones ) {
                $valid_timezones = timezone_identifiers_list();
            }

            if ( ! is_scalar( $timezone_identifier ) ) {
                return in_array( $fallback, $valid_timezones, true ) ? $fallback : 'UTC';
            }

            $timezone_identifier = sanitize_text_field( (string) $timezone_identifier );
            if ( in_array( $timezone_identifier, $valid_timezones, true ) ) {
                return $timezone_identifier;
            }

            return in_array( $fallback, $valid_timezones, true ) ? $fallback : 'UTC';
        }

/**
		 * Returns the visitor IP address.
		 */
        public function get_user_ip() {
            $candidates = array();

            if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
                $candidates[] = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) );
            }

            if ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
                $forwarded = explode( ',', sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) );
                foreach ( $forwarded as $forwarded_ip ) {
                    $candidates[] = trim( $forwarded_ip );
                }
            }

            if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
                $candidates[] = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
            }

            foreach ( $candidates as $candidate ) {
                $candidate = sanitize_text_field( wp_unslash( $candidate ) );
                if ( filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
                    return $candidate;
                }
            }

            return '0.0.0.0';

        }

/**
		 * Anonymizes an IP address.
		 *
		 * @param string $ip IP address.
		 */
        public function anonymize_ip( $ip ) {
            return md5( $ip );
        }

/**
		 * Ensure the cleanup event exists, also for already active installations.
		 */
        public function maybe_schedule_tracking_cleanup() {
            if ( ! wp_next_scheduled( TIPCG_TRACKING_CLEANUP_HOOK ) ) {
                wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', TIPCG_TRACKING_CLEANUP_HOOK );
            }
        }

/**
		 * Returns the tracking record lifetime.
		 *
		 * @param int $duration_seconds Countdown duration in seconds.
		 */
        protected function get_tracking_record_ttl( $duration_seconds = 0 ) {
            $duration_seconds = absint( $duration_seconds );
            $ttl              = max( 90 * DAY_IN_SECONDS, $duration_seconds + ( 7 * DAY_IN_SECONDS ) );

            return (int) apply_filters( 'tipcg_tracking_record_ttl', $ttl, $duration_seconds );
        }

/**
		 * Normalize a tracking payload.
		 *
		 * @param array $data Tracking payload.
		 * @param int   $duration_seconds Countdown duration in seconds.
		 */
        protected function normalize_tracking_payload( $data, $duration_seconds = 0 ) {
            if ( ! is_array( $data ) || ! isset( $data['timestamp'] ) ) {
                return null;
            }

            $timestamp = intval( $data['timestamp'] );
            if ( $timestamp <= 0 ) {
                return null;
            }

            $created = isset( $data['created'] ) ? sanitize_text_field( $data['created'] ) : current_time( 'mysql' );
            $expires = isset( $data['expires'] ) ? intval( $data['expires'] ) : 0;
            if ( $expires <= 0 ) {
                $expires = $timestamp + $this->get_tracking_record_ttl( $duration_seconds );
            }

            return array(
                'timestamp' => $timestamp,
                'created'   => $created,
                'updated'   => isset( $data['updated'] ) ? sanitize_text_field( $data['updated'] ) : $created,
                'expires'   => $expires,
            );
        }

/**
		 * Returns the consolidated tracking store.
		 */
        protected function get_tracking_store() {
            $store = get_option( TIPCG_TRACKING_OPTION_KEY, array() );

            if ( ! is_array( $store ) ) {
                $store = array();
            }

            if ( ! isset( $store['records'] ) || ! is_array( $store['records'] ) ) {
                $store = array(
                    'version' => 1,
                    'records' => $store,
                );
            }

            $store['version'] = 1;

            return $store;
        }

/**
		 * Persists the consolidated tracking store without autoloading it.
		 *
		 * @param array $store Tracking store.
		 */
        protected function update_tracking_store( $store ) {
            if ( false === get_option( TIPCG_TRACKING_OPTION_KEY, false ) ) {
                add_option( TIPCG_TRACKING_OPTION_KEY, $store, '', 'no' );
                return;
            }

            update_option( TIPCG_TRACKING_OPTION_KEY, $store );
        }

/**
		 * Saves a single tracking record inside the consolidated option.
		 *
		 * @param string $option_key Tracking record key.
		 * @param array  $data Tracking payload.
		 */
        protected function save_tracking_record( $option_key, $data ) {
            $store = $this->get_tracking_store();
            $store['records'][ $option_key ] = $data;
            $this->update_tracking_store( $store );
        }

/**
		 * Retrieves saved option data, or null when missing or incomplete.
		 *
		 * @param string $option_key Tracking record key.
		 * @param int    $duration_seconds Countdown duration in seconds.
		 */
        protected function read_tracking_option( $option_key, $duration_seconds = 0 ) {
            $now   = time();
            $store = $this->get_tracking_store();

            if ( isset( $store['records'][ $option_key ] ) ) {
                $data = $this->normalize_tracking_payload( $store['records'][ $option_key ], $duration_seconds );

                if ( ! $data || $data['expires'] <= $now ) {
                    unset( $store['records'][ $option_key ] );
                    $this->update_tracking_store( $store );
                    return null;
                }

                if ( $data !== $store['records'][ $option_key ] ) {
                    $store['records'][ $option_key ] = $data;
                    $this->update_tracking_store( $store );
                }

                return $data;
            }

            $legacy_data = $this->normalize_tracking_payload( get_option( $option_key ), $duration_seconds );
            if ( $legacy_data ) {
                delete_option( $option_key );

                if ( $legacy_data['expires'] <= $now ) {
                    return null;
                }

                $this->save_tracking_record( $option_key, $legacy_data );
                return $legacy_data;
            }

            return null;
        }

/**
		 * Updates a tracking payload timestamp while preserving the creation date.
		 *
		 * @param string $option_key Tracking record key.
		 * @param array  $data Tracking payload.
		 * @param int    $timestamp New timestamp.
		 * @param int    $duration_seconds Countdown duration in seconds.
		 */
        protected function update_tracking_timestamp( $option_key, $data, $timestamp, $duration_seconds = 0 ) {
            $data['timestamp'] = intval( $timestamp );
            $data['updated']   = current_time( 'mysql' );
            $data['expires']   = intval( $timestamp ) + $this->get_tracking_record_ttl( $duration_seconds );
            $this->save_tracking_record( $option_key, $data );
            delete_option( $option_key );

            return $data;
        }

/**
		 * Saves tracking data and returns it.
		 *
		 * @param string $option_key Tracking record key.
		 * @param int    $timestamp Tracking timestamp.
		 * @param int    $duration_seconds Countdown duration in seconds.
		 */
        protected function write_tracking_option( $option_key, $timestamp, $duration_seconds = 0 ) {
            $payload = array(
                'timestamp' => intval( $timestamp ),
                'created'   => current_time( 'mysql' ),
                'updated'   => current_time( 'mysql' ),
                'expires'   => intval( $timestamp ) + $this->get_tracking_record_ttl( $duration_seconds ),
            );
            $this->save_tracking_record( $option_key, $payload );
            delete_option( $option_key );

            return $payload;
        }

/**
		 * Periodically cleans and migrates tracking records.
		 */
        public function cleanup_tracking_data() {
            $now     = time();
            $store   = $this->get_tracking_store();
            $changed = false;

            foreach ( $store['records'] as $option_key => $data ) {
                $data = $this->normalize_tracking_payload( $data );
                if ( ! $data || $data['expires'] <= $now ) {
                    unset( $store['records'][ $option_key ] );
                    $changed = true;
                    continue;
                }

                if ( $data !== $store['records'][ $option_key ] ) {
                    $store['records'][ $option_key ] = $data;
                    $changed = true;
                }
            }

            $changed = $this->migrate_legacy_tracking_options( $store, $now ) || $changed;

            if ( $changed ) {
                $this->update_tracking_store( $store );
            }
        }

/**
		 * Moves old per-visitor options into the consolidated store in small batches.
		 *
		 * @param array $store Tracking store.
		 * @param int   $now Current timestamp.
		 */
        protected function migrate_legacy_tracking_options( &$store, $now ) {
            global $wpdb;

            $changed      = false;
            $tracking_key = $wpdb->esc_like( TIPCG_TRACKING_OPTION_KEY );
            $cookie_like  = $wpdb->esc_like( 'tipcg_countdown_cookie_' ) . '%';
            $ip_like      = $wpdb->esc_like( 'tipcg_countdown_ip_' ) . '%';
            $option_names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Limited cron cleanup of legacy tracking option names.
                $wpdb->prepare(
                    "SELECT option_name FROM {$wpdb->options} WHERE option_name <> %s AND (option_name LIKE %s OR option_name LIKE %s) LIMIT 500",
                    $tracking_key,
                    $cookie_like,
                    $ip_like
                )
            );

            foreach ( $option_names as $option_name ) {
                $data = $this->normalize_tracking_payload( get_option( $option_name ) );
                delete_option( $option_name );

                if ( ! $data || $data['expires'] <= $now ) {
                    $changed = true;
                    continue;
                }

                $store['records'][ $option_name ] = $data;
                $changed = true;
            }

            return $changed;
        }

/**
		 * Returns the option key for IP tracking.
		 *
		 * @param string $countdown_id Countdown ID.
		 */
        protected function get_ip_option_key( $countdown_id ) {
            $countdown_id = $this->normalize_countdown_id( $countdown_id );
            $ip           = $this->get_user_ip();
            $anon_ip      = $this->anonymize_ip( $ip );

            return 'tipcg_countdown_ip_' . $countdown_id . '_' . $anon_ip;
        }

/**
		 * Returns the option key for cookie tracking.
		 *
		 * @param string $countdown_id Countdown ID.
		 */
        protected function get_cookie_option_key( $countdown_id ) {
            $countdown_id = $this->normalize_countdown_id( $countdown_id );
            $cookie_id    = $this->ensure_tracking_cookie();

            return 'tipcg_countdown_cookie_' . $countdown_id . '_' . $cookie_id;
        }

/**
		 * Retrieves or initializes IP-based tracking data.
		 *
		 * Saved data is an array containing:
		 * - 'timestamp': the start timestamp in seconds.
		 * - 'created': the creation date in MySQL format.
		 * The key is built from the countdown ID and anonymized IP.
		 *
		 * @param string   $countdown_id Countdown ID.
		 * @param int|null $preferred_timestamp Preferred timestamp.
		 * @param int      $duration_seconds Countdown duration in seconds.
		 */
        public function get_ip_data( $countdown_id, $preferred_timestamp = null, $duration_seconds = 0 ) {
            $option_key = $this->get_ip_option_key( $countdown_id );
            $data = $this->read_tracking_option( $option_key, $duration_seconds );

            if ( $data ) {
                if ( null !== $preferred_timestamp && intval( $preferred_timestamp ) < $data['timestamp'] ) {
                    $data = $this->update_tracking_timestamp( $option_key, $data, $preferred_timestamp, $duration_seconds );
                }
                return $data;
            }

            $timestamp = ( null !== $preferred_timestamp ) ? intval( $preferred_timestamp ) : time();
            return $this->write_tracking_option( $option_key, $timestamp, $duration_seconds );
        }

/**
		 * Retrieves or initializes cookie-based tracking data.
		 *
		 * Saved data is an array containing:
		 * - 'timestamp': the start timestamp in seconds.
		 * - 'created': the creation date in MySQL format.
		 * The key is built from the countdown ID and cookie value.
		 *
		 * @param string   $countdown_id Countdown ID.
		 * @param int|null $preferred_timestamp Preferred timestamp.
		 * @param int      $duration_seconds Countdown duration in seconds.
		 */
        public function get_cookie_data( $countdown_id, $preferred_timestamp = null, $duration_seconds = 0 ) {
            $option_key = $this->get_cookie_option_key( $countdown_id );
            $data       = $this->read_tracking_option( $option_key, $duration_seconds );

            if ( $data ) {
                if ( null !== $preferred_timestamp && intval( $preferred_timestamp ) < $data['timestamp'] ) {
                    $data = $this->update_tracking_timestamp( $option_key, $data, $preferred_timestamp, $duration_seconds );
                }
                return $data;
            }

            $timestamp = ( null !== $preferred_timestamp ) ? intval( $preferred_timestamp ) : time();
            return $this->write_tracking_option( $option_key, $timestamp, $duration_seconds );
        }

/**
		 * Retrieves data for combined "cookie_ip" tracking.
		 *
		 * - If the IP has already visited the site, all users on that IP start from that timestamp and the cookie is saved.
		 * - The cookie keeps the timestamp even when the user changes network/IP.
		 * - If both cookie and IP exist, the oldest timestamp is kept.
		 *
		 * @param string $countdown_id Countdown ID.
		 * @param int    $duration_seconds Countdown duration in seconds.
		 */
        public function get_cookie_or_ip_data( $countdown_id, $duration_seconds = 0 ) {
            $ip_key     = $this->get_ip_option_key( $countdown_id );
            $cookie_key = $this->get_cookie_option_key( $countdown_id );

            $ip_data     = $this->read_tracking_option( $ip_key, $duration_seconds );
            $cookie_data = $this->read_tracking_option( $cookie_key, $duration_seconds );

            if ( ! $ip_data && ! $cookie_data ) {
                $timestamp   = time();
                $ip_data     = $this->write_tracking_option( $ip_key, $timestamp, $duration_seconds );
                $cookie_data = $this->write_tracking_option( $cookie_key, $timestamp, $duration_seconds );

                return $cookie_data;
            }

            if ( $ip_data && ! $cookie_data ) {
                $cookie_data = $this->write_tracking_option( $cookie_key, $ip_data['timestamp'], $duration_seconds );

                return $ip_data;
            }

            if ( ! $ip_data && $cookie_data ) {
                $ip_data = $this->write_tracking_option( $ip_key, $cookie_data['timestamp'], $duration_seconds );

                return $cookie_data;
            }

            $earliest = min( intval( $ip_data['timestamp'] ), intval( $cookie_data['timestamp'] ) );

            if ( intval( $ip_data['timestamp'] ) !== $earliest ) {
                $ip_data = $this->update_tracking_timestamp( $ip_key, $ip_data, $earliest, $duration_seconds );
            }

            if ( intval( $cookie_data['timestamp'] ) !== $earliest ) {
                $cookie_data = $this->update_tracking_timestamp( $cookie_key, $cookie_data, $earliest, $duration_seconds );
            }

            return ( intval( $cookie_data['timestamp'] ) <= intval( $ip_data['timestamp'] ) ) ? $cookie_data : $ip_data;
        }

/**
		 * Returns the start timestamp based on the tracking parameter.
		 *
		 * Possible tracking values are "ip", "cookie", and "cookie_ip".
		 * The countdown ID distinguishes multiple countdowns for the same visitor.
		 *
		 * @param string $tracking Tracking method.
		 * @param string $countdown_id Countdown ID.
		 * @param int    $duration_seconds Countdown duration in seconds.
		 */
        public function get_start_time( $tracking, $countdown_id, $duration_seconds = 0 ) {
            $tracking = strtolower( $tracking );
            if ( 'ip' === $tracking ) {
                $data = $this->get_ip_data( $countdown_id, null, $duration_seconds );
            } elseif ( 'cookie' === $tracking ) {
                $data = $this->get_cookie_data( $countdown_id, null, $duration_seconds );
            } elseif ( 'cookie_ip' === $tracking ) {
                $data = $this->get_cookie_or_ip_data( $countdown_id, $duration_seconds );
            } else {
                // Default: cookie.
                $data = $this->get_cookie_data( $countdown_id, null, $duration_seconds );
            }
            return $data['timestamp'];
        }

	}

}
