<?php
if (!defined('ABSPATH')) {
    exit;
}

/**
 * Service for managing custom fields, database meta mapping, and complex data structures.
 */
class NEXDIRECTORY_Event_Meta_Service {

    /**
     * @var NEXDIRECTORY_User_Capabilities
     */
    private $user_caps;

    /**
     * @var NEXDIRECTORY_Date_Helper
     */
    private $date_helper;

    /**
     * @var NEXDIRECTORY_Media_Helper
     */
    private $media_helper;

    /**
     * Constructor.
     */
    public function __construct($user_caps, $date_helper, $media_helper) {
        $this->user_caps = $user_caps;
        $this->date_helper = $date_helper;
        $this->media_helper = $media_helper;
    }

    /**
     * Retrieves a map of all standard event meta fields and their expected data types.
     */
    public function get_event_meta_fields() {
        $fields = [
            'nexdirectory_start' => 'datetime-local',
            'nexdirectory_end' => 'datetime-local',
            'nexdirectory_timezone' => 'text',
            'nexdirectory_venue' => 'text',
            'nexdirectory_company_name' => 'text',
            'nexdirectory_address' => 'text',
            'nexdirectory_city' => 'text',
            'nexdirectory_country' => 'text',
            'nexdirectory_lat' => 'text',
            'nexdirectory_lng' => 'text',
            'nexdirectory_capacity' => 'number',
            'nexdirectory_attendee_visibility' => 'text',
            'nexdirectory_price' => 'text',
            'nexdirectory_price_simple' => 'text',
            'nexdirectory_ticket_sales_enabled' => 'text',
            'nexdirectory_ticket_price' => 'text',
            'nexdirectory_ticket_button_label' => 'text',
            'nexdirectory_meta_title' => 'text',
            'nexdirectory_meta_description' => 'textarea',
            'nexdirectory_external_url' => 'url',
            'nexdirectory_event_stream_url' => 'url',
            'nexdirectory_contact_email' => 'email',
            'nexdirectory_contact_phone' => 'text',
            'nexdirectory_contact_name' => 'text',
            'nexdirectory_directory_business_type' => 'text',
            'nexdirectory_directory_listing_status' => 'text',
            'nexdirectory_directory_contact_person' => 'text',
            'nexdirectory_directory_service_area' => 'text',
            'nexdirectory_directory_operating_hours' => 'text',
            'nexdirectory_directory_response_time' => 'text',
            'nexdirectory_directory_established_year' => 'number',
            'nexdirectory_directory_pricing_summary' => 'text',
            'nexdirectory_directory_appointment_url' => 'url',
            'nexdirectory_directory_whatsapp_url' => 'url',
            'nexdirectory_directory_services' => 'textarea',
            'nexdirectory_directory_highlights' => 'textarea',
            'nexdirectory_directory_certifications' => 'textarea',
            'nexdirectory_job_type' => 'text',
            'nexdirectory_job_work_mode' => 'text',
            'nexdirectory_job_posted_date' => 'date',
            'nexdirectory_job_compensation_type' => 'text',
            'nexdirectory_job_contract_duration' => 'text',
            'nexdirectory_job_working_hours' => 'text',
            'nexdirectory_job_salary' => 'text',
            'nexdirectory_job_apply_url' => 'url',
            'nexdirectory_job_application_email' => 'email',
            'nexdirectory_job_closing_date' => 'date',
            'nexdirectory_job_department' => 'text',
            'nexdirectory_job_experience_level' => 'text',
            'nexdirectory_job_reporting_to' => 'text',
            'nexdirectory_job_remote_policy' => 'text',
            'nexdirectory_job_visa_sponsorship' => 'text',
            'nexdirectory_job_open_positions' => 'number',
            'nexdirectory_job_interview_process' => 'textarea',
            'nexdirectory_job_expected_start_date' => 'date',
            'nexdirectory_job_skills' => 'text',
            'nexdirectory_job_responsibilities' => 'textarea',
            'nexdirectory_job_requirements' => 'textarea',
            'nexdirectory_job_benefits' => 'textarea',
            'nexdirectory_job_equity' => 'text',
            'nexdirectory_job_bonus' => 'text',
            'nexdirectory_job_recruitment_timeline' => 'textarea',
            'nexdirectory_job_company_intro_video' => 'url',
            'nexdirectory_job_role_overview_video' => 'url',
            'nexdirectory_job_company_website' => 'url',
            'nexdirectory_job_company_size' => 'text',
            'nexdirectory_job_industry' => 'text',
            'nexdirectory_market_condition' => 'text',
            'nexdirectory_market_price_type' => 'text',
            'nexdirectory_market_quantity' => 'number',
            'nexdirectory_market_sku' => 'text',
            'nexdirectory_market_brand' => 'text',
            'nexdirectory_market_model' => 'text',
            'nexdirectory_market_year' => 'number',
            'nexdirectory_market_seller_type' => 'text',
            'nexdirectory_market_listing_status' => 'text',
            'nexdirectory_market_expires_on' => 'date',
            'nexdirectory_market_delivery_methods' => 'text',
            'nexdirectory_market_returns_accepted' => 'text',
            'nexdirectory_market_accept_offers' => 'text',
            'nexdirectory_market_payment_methods' => 'text',
            'nexdirectory_market_condition_notes' => 'textarea',
            'nexdirectory_market_shipping_notes' => 'textarea',
            'nexdirectory_listing_profile' => 'text',
            'nexdirectory_cover_image_url' => 'url',
            'nexdirectory_supporting_docs' => 'textarea',
            'nexdirectory_official_event' => 'text',
            'nexdirectory_lock_pin' => 'text',
            'nexdirectory_recurrence' => 'text',
            'nexdirectory_recurrence_until' => 'date',
            'nexdirectory_youtube_video_id_1' => 'text',
            'nexdirectory_youtube_video_id_2' => 'text',
            'nexdirectory_video_urls' => 'textarea',
            'nexdirectory_gallery_urls' => 'textarea',
            'nexdirectory_email_subject_confirmation' => 'text',
            'nexdirectory_email_subject_reminder' => 'text',
            'nexdirectory_email_subject_update' => 'text',
            'nexdirectory_email_subject_cancellation' => 'text',
            'nexdirectory_email_body_confirmation' => 'textarea',
            'nexdirectory_email_body_reminder' => 'textarea',
            'nexdirectory_email_body_update' => 'textarea',
            'nexdirectory_email_body_cancellation' => 'textarea',
        ];

        if (
            !function_exists('nexdirectory_is_pro_feature_enabled')
            || !nexdirectory_is_pro_feature_enabled('notification_automation')
            || (string) get_option('nexdirectory_enable_per_event_email_templates', '0') !== '1'
        ) {
            unset(
                $fields['nexdirectory_email_subject_confirmation'],
                $fields['nexdirectory_email_subject_reminder'],
                $fields['nexdirectory_email_subject_update'],
                $fields['nexdirectory_email_subject_cancellation'],
                $fields['nexdirectory_email_body_confirmation'],
                $fields['nexdirectory_email_body_reminder'],
                $fields['nexdirectory_email_body_update'],
                $fields['nexdirectory_email_body_cancellation']
            );
        }

        return $fields;
    }

    /**
     * Normalizes a boolean-like official event status.
     */
    public function normalize_official_event_value($value) {
        if (is_array($value)) {
            $value = end($value);
        }
        $normalized = strtolower(trim((string) $value));
        return in_array($normalized, ['1', 'yes', 'true', 'official'], true) ? '1' : '0';
    }

    /**
     * Safely sanitizes standard post meta values depending on their key context.
     */
    public function sanitize_meta_value($key, $value) {
        if (is_array($value)) {
            if ($key === 'nexdirectory_market_delivery_methods') {
                $allowed = ['pickup', 'shipping', 'local-delivery', 'digital'];
                $normalized = array_values(array_unique(array_filter(array_map(static function ($item) use ($allowed) {
                    $item = sanitize_key((string) $item);
                    return in_array($item, $allowed, true) ? $item : '';
                }, $value))));
                return implode(',', $normalized);
            }
            $value = implode(', ', array_map('nexdirectory_sanitize_text', $value));
        }

        if (in_array($key, ['nexdirectory_capacity', 'nexdirectory_market_quantity', 'nexdirectory_market_year', 'nexdirectory_directory_established_year'])) {
            return absint($value);
        }

        if ($key === 'nexdirectory_contact_email' || $key === 'nexdirectory_job_application_email') {
            return sanitize_email($value);
        }

        if ($key === 'nexdirectory_timezone') {
            return $this->date_helper->sanitize_timezone_value($value);
        }

        if ($key === 'nexdirectory_attendee_visibility') {
            $visibility = sanitize_key((string) $value);
            return in_array($visibility, ['off', 'counts', 'members', 'public'], true) ? $visibility : 'counts';
        }

        if (in_array($key, [
            'nexdirectory_external_url', 'nexdirectory_cover_image_url', 'nexdirectory_event_stream_url', 
            'nexdirectory_job_apply_url', 'nexdirectory_directory_appointment_url', 'nexdirectory_directory_whatsapp_url'
        ])) {
            return esc_url_raw(nexdirectory_scalar_to_string($value));
        }

        if ($key === 'nexdirectory_lat' || $key === 'nexdirectory_lng') {
            return is_numeric($value) ? (string) $value : '';
        }

        if ($key === 'nexdirectory_video_urls' || $key === 'nexdirectory_gallery_urls') {
            return nexdirectory_sanitize_textarea(nexdirectory_scalar_to_string($value));
        }

        if (in_array($key, [
            'nexdirectory_supporting_docs', 'nexdirectory_market_shipping_notes', 'nexdirectory_directory_services', 
            'nexdirectory_directory_highlights', 'nexdirectory_directory_certifications'
        ])) {
            return nexdirectory_sanitize_textarea($value);
        }

        if ($key === 'nexdirectory_market_expires_on') {
            $value = sanitize_text_field((string) $value);
            if ($value === '' || preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
                return $value;
            }
            return '';
        }

        if ($key === 'nexdirectory_market_returns_accepted' || $key === 'nexdirectory_lock_pin') {
            return (string) $value === '1' ? '1' : '0';
        }

        if ($key === 'nexdirectory_ticket_sales_enabled') {
            return (string) $value === '1' ? '1' : '0';
        }

        if ($key === 'nexdirectory_ticket_price') {
            $normalized = str_replace(',', '.', trim((string) $value));
            if ($normalized === '') {
                return '';
            }
            if (!is_numeric($normalized)) {
                if (!preg_match('/\d+(?:\.\d{1,2})?/', $normalized, $matches)) {
                    return '';
                }
                $normalized = (string) ($matches[0] ?? '');
            }
            $amount = max(0.0, (float) $normalized);
            return number_format($amount, 2, '.', '');
        }

        if ($key === 'nexdirectory_ticket_button_label') {
            return nexdirectory_sanitize_text($value);
        }

        if ($key === 'nexdirectory_meta_title') {
            $title = sanitize_text_field((string) $value);
            if (function_exists('mb_substr')) {
                $title = mb_substr($title, 0, 60);
            } else {
                $title = substr($title, 0, 60);
            }
            return trim((string) $title);
        }

        if ($key === 'nexdirectory_meta_description') {
            $description = nexdirectory_sanitize_textarea((string) $value);
            $description = trim(preg_replace('/\s+/u', ' ', (string) $description));
            if (function_exists('mb_substr')) {
                $description = mb_substr((string) $description, 0, 158);
            } else {
                $description = substr((string) $description, 0, 158);
            }
            return trim((string) $description);
        }

        if ($key === 'nexdirectory_market_price_type') {
            $value = sanitize_key((string) $value);
            return in_array($value, ['fixed', 'negotiable', 'auction', 'free'], true) ? $value : 'fixed';
        }

        if ($key === 'nexdirectory_market_seller_type') {
            $value = sanitize_key((string) $value);
            return in_array($value, ['private', 'business'], true) ? $value : 'private';
        }

        if ($key === 'nexdirectory_market_listing_status') {
            $value = sanitize_key((string) $value);
            return in_array($value, ['active', 'reserved', 'sold', 'expired'], true) ? $value : 'active';
        }

        if ($key === 'nexdirectory_market_delivery_methods') {
            $parts = preg_split('/[\s,]+/', (string) $value) ?: [];
            $allowed = ['pickup', 'shipping', 'local-delivery', 'digital'];
            $normalized = array_values(array_unique(array_filter(array_map(static function ($item) use ($allowed) {
                $item = sanitize_key((string) $item);
                return in_array($item, $allowed, true) ? $item : '';
            }, $parts))));
            return implode(',', $normalized);
        }

        if ($key === 'nexdirectory_directory_listing_status') {
            $value = sanitize_key((string) $value);
            return in_array($value, ['active', 'temporarily-closed', 'permanently-closed'], true) ? $value : 'active';
        }

        if ($key === 'nexdirectory_directory_business_type') {
            $value = sanitize_key((string) $value);
            return in_array($value, ['independent-business', 'agency', 'private-owner', 'nonprofit', 'multi-location', 'franchise'], true)
                ? $value
                : 'independent-business';
        }

        $key = is_string($key) ? $key : '';

        if (strpos($key, 'nexdirectory_email_body_') === 0) {
            return nexdirectory_sanitize_textarea($value);
        }

        if (strpos($key, 'nexdirectory_email_subject_') === 0) {
            return nexdirectory_sanitize_text($value);
        }

        if ($key === 'nexdirectory_official_event') {
            return $this->normalize_official_event_value($value);
        }

        if ($key === 'nexdirectory_youtube_video_id_1' || $key === 'nexdirectory_youtube_video_id_2') {
            $normalized = preg_replace('/[^A-Za-z0-9_-]/', '', (string) $value);
            return substr((string) $normalized, 0, 32);
        }

        return nexdirectory_sanitize_text($value);
    }

    /**
     * Map of supported custom field types.
     */
    public function get_custom_field_types() {
        return [
            'text' => __('Text', 'nexdirectory'),
            'textarea' => __('Textarea', 'nexdirectory'),
            'wysiwyg' => __('Rich Text (WYSIWYG)', 'nexdirectory'),
            'url' => __('URL', 'nexdirectory'),
            'email' => __('Email', 'nexdirectory'),
            'number' => __('Number', 'nexdirectory'),
            'date' => __('Date', 'nexdirectory'),
            'datetime-local' => __('Date + Time', 'nexdirectory'),
            'select' => __('Select Dropdown', 'nexdirectory'),
            'checkbox' => __('Yes/No Toggle', 'nexdirectory'),
            'image' => __('Image Upload', 'nexdirectory'),
            'gallery' => __('Gallery Upload', 'nexdirectory'),
            'file' => __('File Upload', 'nexdirectory'),
        ];
    }

    /**
     * Sanitizes a custom field schema definition.
     */
    public function sanitize_custom_field_definition($field, $index = 0) {
        $field = is_array($field) ? $field : [];
        $type = sanitize_key((string) ($field['type'] ?? 'text'));
        $types = array_keys($this->get_custom_field_types());
        if (!in_array($type, $types, true)) {
            $type = 'text';
        }

        $id = sanitize_key((string) ($field['id'] ?? ''));
        if ($id === '') {
            $id = 'custom_field_' . max(1, (int) $index + 1);
        }
        $id = substr((string) $id, 0, 64);
        
        $label = sanitize_text_field((string) ($field['label'] ?? ''));
        if ($label === '') {
            $label = ucwords(str_replace('_', ' ', $id));
        }
        $label = substr((string) $label, 0, 140);
        
        $placeholder = sanitize_text_field((string) ($field['placeholder'] ?? ''));
        $help = sanitize_text_field((string) ($field['help'] ?? ''));
        $required = (string) ($field['required'] ?? '0') === '1' ? '1' : '0';
        $show_in_event = (string) ($field['show_in_event'] ?? '1') === '0' ? '0' : '1';
        $options = nexdirectory_sanitize_textarea((string) ($field['options'] ?? ''));

        return [
            'id' => $id,
            'label' => $label,
            'type' => $type,
            'placeholder' => $placeholder,
            'help' => $help,
            'required' => $required,
            'show_in_event' => $show_in_event,
            'options' => $options,
        ];
    }

    /**
     * Retrieves the global custom form field schema.
     */
    public function get_custom_form_fields() {
        $raw = get_option('nexdirectory_custom_form_fields', '[]');
        $decoded = is_string($raw) ? json_decode($raw, true) : (is_array($raw) ? $raw : []);
        if (!is_array($decoded)) {
            $decoded = [];
        }

        $sanitized = [];
        $seen_ids = [];
        foreach (array_values($decoded) as $index => $field) {
            $row = $this->sanitize_custom_field_definition($field, (int) $index);
            if ($row['id'] === '' || isset($seen_ids[$row['id']])) {
                continue;
            }
            $seen_ids[$row['id']] = true;
            $sanitized[] = $row;
        }
        return $sanitized;
    }

    /**
     * Retrieves the schema specifically attached to a post (or falls back to global).
     */
    public function get_post_custom_field_schema($post_id, $fallback_to_global = true) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return $fallback_to_global ? $this->get_custom_form_fields() : [];
        }

        $raw = (string) get_post_meta($post_id, 'nexdirectory_custom_fields_schema', true);
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return $fallback_to_global ? $this->get_custom_form_fields() : [];
        }

        $sanitized = [];
        $seen_ids = [];
        foreach (array_values($decoded) as $index => $field) {
            $row = $this->sanitize_custom_field_definition($field, (int) $index);
            if ($row['id'] === '' || isset($seen_ids[$row['id']])) {
                continue;
            }
            $seen_ids[$row['id']] = true;
            $sanitized[] = $row;
        }
        
        if (empty($sanitized) && $fallback_to_global) {
            return $this->get_custom_form_fields();
        }
        return $sanitized;
    }

    /**
     * Delegates premium builder payload parsing to an active extension.
     *
     * Free intentionally does not parse or authorize visual-builder
     * mutations. The empty default makes stale options and forged payloads
     * fail closed when the licensed Pro module is unavailable.
     */
    public function parse_custom_field_builder_payload($raw_payload) {
        $parsed = apply_filters(
            'nexdirectory_parse_custom_field_builder_payload',
            ['fields' => [], 'values' => []],
            $raw_payload,
            $this
        );
        $parsed = is_array($parsed) ? $parsed : [];
        return [
            'fields' => is_array($parsed['fields'] ?? null) ? $parsed['fields'] : [],
            'values' => is_array($parsed['values'] ?? null) ? $parsed['values'] : [],
        ];
    }

    /**
     * Helper to split line/comma delimited text into arrays.
     */
    public function parse_delimited_values($raw) {
        if (is_array($raw)) {
            $items = $raw;
        } else {
            $items = preg_split('/[\\r\\n|,]+/', (string) $raw) ?: [];
        }
        $items = array_map(static function ($v) {
            return sanitize_text_field((string) $v);
        }, (array) $items);
        
        return array_values(array_filter(array_map('trim', $items), static function ($v) {
            return $v !== '';
        }));
    }

    /**
     * Compatibility wrapper for media meta updates used by import flows.
     *
     * @param int $post_id
     * @param string $video_raw
     * @param string $gallery_raw
     * @return void
     */
    public function update_media_meta($post_id, $video_raw, $gallery_raw) {
        if (is_object($this->media_helper) && method_exists($this->media_helper, 'update_media_meta')) {
            $this->media_helper->update_media_meta($post_id, $video_raw, $gallery_raw);
            return;
        }

        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }
        update_post_meta($post_id, 'nexdirectory_video_urls', nexdirectory_sanitize_textarea((string) $video_raw));
        update_post_meta($post_id, 'nexdirectory_gallery_urls', nexdirectory_sanitize_textarea((string) $gallery_raw));
    }

    /**
     * Normalizes gallery custom fields by utilizing the Media Helper.
     */
    private function normalize_custom_gallery_value($value) {
        return implode("\n", $this->media_helper->parse_multiline_urls((string) $value));
    }

    /**
     * Safely sanitizes a single custom field value based on its schema type.
     */
    public function sanitize_custom_field_value($type, $value, $options = '') {
        $type = sanitize_key((string) $type);
        
        if ($type === 'checkbox') return (string) $value === '1' ? '1' : '0';
        if ($type === 'email') return sanitize_email((string) $value);
        if ($type === 'url') return esc_url_raw((string) $value);
        if ($type === 'image' || $type === 'file') return esc_url_raw((string) $value);
        if ($type === 'gallery') return $this->normalize_custom_gallery_value($value);
        if ($type === 'number') {
            $value = trim((string) $value);
            return is_numeric($value) ? (string) $value : '';
        }
        if ($type === 'date') {
            $value = trim((string) $value);
            return preg_match('/^\d{4}\-\d{2}\-\d{2}$/', $value) ? $value : '';
        }
        if ($type === 'datetime-local') {
            $value = str_replace('T', ' ', trim((string) $value));
            return preg_match('/^\d{4}\-\d{2}\-\d{2}\s\d{2}\:\d{2}$/', $value) ? $value : '';
        }
        if ($type === 'select') {
            $allowed = $this->parse_delimited_values($options);
            $candidate = sanitize_text_field((string) $value);
            return in_array($candidate, $allowed, true) ? $candidate : '';
        }
        if ($type === 'wysiwyg') return wp_kses_post((string) $value);
        if ($type === 'textarea') return nexdirectory_sanitize_textarea((string) $value);
        
        return sanitize_text_field((string) $value);
    }

    /**
     * Sanitizes a map of custom field key->values against their definitions.
     */
    public function sanitize_custom_form_fields_value_map($raw_values, $field_definitions = null) {
        $raw_values = is_array($raw_values) ? $raw_values : [];
        $field_definitions = is_array($field_definitions) ? $field_definitions : $this->get_custom_form_fields();
        $sanitized = [];
        
        foreach ($field_definitions as $field) {
            $field = is_array($field) ? $field : [];
            $id = sanitize_key((string) ($field['id'] ?? ''));
            if ($id === '') {
                continue;
            }
            $type = sanitize_key((string) ($field['type'] ?? 'text'));
            $options = (string) ($field['options'] ?? '');
            $value = array_key_exists($id, $raw_values) ? wp_unslash($raw_values[$id]) : '';
            $sanitized[$id] = $this->sanitize_custom_field_value($type, $value, $options);
        }
        return $sanitized;
    }

    /**
     * Validates that required custom fields are provided.
     */
    public function validate_custom_form_fields_required(array $field_definitions, array $value_map) {
        foreach ($field_definitions as $field) {
            $id = sanitize_key((string) ($field['id'] ?? ''));
            if ($id === '' || (string) ($field['required'] ?? '0') !== '1') {
                continue;
            }
            $type = sanitize_key((string) ($field['type'] ?? 'text'));
            $value = array_key_exists($id, $value_map) ? $value_map[$id] : '';
            
            $is_missing = trim((string) $value) === '';
            if ($type === 'checkbox') {
                $is_missing = (string) $value !== '1';
            }
            if ($is_missing) {
                $label = sanitize_text_field((string) ($field['label'] ?? $id));
                return new WP_Error(
                    'nexdirectory_custom_field_required',
                    /* translators: %s: Placeholder value. */
                    sprintf(__('Required field missing: %s', 'nexdirectory'), $label)
                );
            }
        }
        return true;
    }

    /**
     * Loads the fully populated data array for custom fields attached to a post.
     */
    public function get_post_custom_field_values($post_id, $field_definitions = null) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return [];
        }
        $field_definitions = is_array($field_definitions) ? $field_definitions : $this->get_post_custom_field_schema($post_id, true);
        $raw = (string) get_post_meta($post_id, 'nexdirectory_custom_fields_data', true);
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            $decoded = [];
        }
        return $this->sanitize_custom_form_fields_value_map($decoded, $field_definitions);
    }

    /**
     * Safely saves JSON stringified custom field values.
     */
    public function update_post_custom_field_values($post_id, $raw_values, $field_definitions = null) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) {
            return;
        }
        $field_definitions = is_array($field_definitions) ? $field_definitions : $this->get_post_custom_field_schema($post_id, true);
        do_action(
            'nexdirectory_persist_custom_field_value_map',
            $post_id,
            is_array($raw_values) ? $raw_values : [],
            $field_definitions,
            $this
        );
    }

    /**
     * Checks if the post is marked as official.
     */
    public function get_official_event_flag($post_id) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) return false;

        return $this->normalize_official_event_value(get_post_meta($post_id, 'nexdirectory_official_event', true)) === '1';
    }

    /**
     * Sets the official event status.
     */
    public function set_official_event_flag($post_id, $value) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) return;
        
        $normalized = $this->normalize_official_event_value($value);
        update_post_meta($post_id, 'nexdirectory_official_event', $normalized);
    }

    /**
     * Applies the official flag, restricting approval based on user caps.
     */
    public function apply_official_flag_with_admin_gate($post_id, $requested_value, $user_id = 0) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) return '0';

        $user_id = $this->user_caps->resolve_capability_user_id($user_id);
        $requested = $this->normalize_official_event_value($requested_value);
        $can_self_approve = $this->user_caps->user_can_moderate_events($user_id);
        
        if ($requested === '1' && !$can_self_approve) {
            update_post_meta($post_id, 'nexdirectory_official_requested', '1');
            $this->set_official_event_flag($post_id, '0');
            return '0';
        }

        update_post_meta($post_id, 'nexdirectory_official_requested', '0');
        if ($requested === '1' && $can_self_approve) {
            update_post_meta($post_id, 'nexdirectory_official_confirmed_at', current_time('mysql'));
        } else {
            delete_post_meta($post_id, 'nexdirectory_official_confirmed_at');
        }
        
        $this->set_official_event_flag($post_id, $requested);
        return $requested;
    }

    /**
     * Elevates a requested official event to confirmed status.
     */
    public function maybe_confirm_requested_official_event($post_id) {
        $post_id = absint((int) $post_id);
        if ($post_id <= 0) return;
        
        $requested = (string) get_post_meta($post_id, 'nexdirectory_official_requested', true);
        if ($requested !== '1') return;
        
        $this->set_official_event_flag($post_id, '1');
        update_post_meta($post_id, 'nexdirectory_official_requested', '0');
        update_post_meta($post_id, 'nexdirectory_official_confirmed_at', current_time('mysql'));
    }

    /**
     * Parses the JSON payload for sessions/itineraries.
     */
    public function parse_sessions($raw) {
        $decoded = json_decode((string) $raw, true);
        if (!is_array($decoded)) {
            return [];
        }
        
        $sessions = [];
        foreach ($decoded as $row) {
            if (!is_array($row)) {
                continue;
            }
            $title = sanitize_text_field((string) ($row['title'] ?? ''));
            $start = $this->date_helper->normalize_datetime_local((string) ($row['start'] ?? ''));
            $end = $this->date_helper->normalize_datetime_local((string) ($row['end'] ?? ''));
            $location = sanitize_text_field((string) ($row['location'] ?? ''));
            $speaker = sanitize_text_field((string) ($row['speaker'] ?? ''));
            $day = sanitize_text_field((string) ($row['day'] ?? ''));
            $notes = nexdirectory_sanitize_textarea((string) ($row['notes'] ?? ''));
            $session_key = sanitize_key((string) ($row['session_key'] ?? ($row['id'] ?? '')));

            $attachments_raw = $row['attachments'] ?? [];
            if (is_string($attachments_raw)) {
                $attachments_raw = preg_split('/[\r\n,]+/', $attachments_raw) ?: [];
            }
            if (!is_array($attachments_raw)) {
                $attachments_raw = [];
            }
            $attachments = [];
            foreach ($attachments_raw as $attachment_item) {
                $attachment_url = $this->normalize_session_attachment_url($attachment_item);
                if ($attachment_url !== '') {
                    $attachments[] = $attachment_url;
                }
            }
            $attachments = array_values(array_unique($attachments));
            
            if ($title === '' && $start === '' && $end === '' && $location === '' && $speaker === '' && $day === '' && $notes === '' && empty($attachments)) {
                continue;
            }
            
            $sessions[] = [
                'title' => $title,
                'start' => $start,
                'end' => $end,
                'location' => $location,
                'speaker' => $speaker,
                'day' => $day,
                'notes' => $notes,
                'attachments' => $attachments,
                'session_key' => $session_key,
            ];
        }
        return $sessions;
    }

    /**
     * Normalizes itinerary attachment input into an absolute URL.
     *
     * Supports attachment IDs, direct URLs, and bare YouTube IDs.
     */
    private function normalize_session_attachment_url($value) {
        $raw = trim((string) $value);
        if ($raw === '') {
            return '';
        }

        if (is_numeric($raw)) {
            $attachment_id = absint((int) $raw);
            if ($attachment_id > 0) {
                $resolved = wp_get_attachment_url($attachment_id);
                if (is_string($resolved) && $resolved !== '') {
                    $raw = $resolved;
                } else {
                    return '';
                }
            }
        }

        if (preg_match('/^[A-Za-z0-9_-]{11}$/', $raw)) {
            $raw = 'https://www.youtube.com/watch?v=' . $raw;
        } elseif (preg_match('/^(?:youtube\.com|youtu\.be|vimeo\.com)\//i', $raw)) {
            $raw = 'https://' . $raw;
        } elseif (preg_match('/^[a-z0-9.-]+\.[a-z]{2,}(?:\/|$)/i', $raw) && !preg_match('/^[a-z]+:\/\//i', $raw)) {
            $raw = 'https://' . $raw;
        }

        return esc_url_raw($raw);
    }

    /**
     * Parses the JSON payload for speakers.
     */
    public function parse_key_speakers($raw) {
        $decoded = json_decode((string) $raw, true);
        if (!is_array($decoded)) {
            return [];
        }
        
        $speakers = [];
        foreach ($decoded as $row) {
            if (!is_array($row)) {
                continue;
            }
            $name = sanitize_text_field((string) ($row['name'] ?? ''));
            $profession = sanitize_text_field((string) ($row['profession'] ?? ($row['professional'] ?? '')));
            $subject = sanitize_text_field((string) ($row['subject'] ?? ''));
            $image = esc_url_raw((string) ($row['image'] ?? ($row['image_url'] ?? '')));
            $details = nexdirectory_sanitize_textarea((string) ($row['details'] ?? ''));
            
            if ($name === '' && $profession === '' && $subject === '' && $image === '' && $details === '') {
                continue;
            }
            
            $speakers[] = [
                'name' => $name,
                'profession' => $profession,
                'subject' => $subject,
                'image' => $image,
                'details' => $details,
            ];
        }
        return $speakers;
    }
}
