<?php
namespace BLR\Backline\Core;

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

use BLR\Backline\Modules\Contacts\Contacts;
use BLR\Backline\Modules\Contacts\NewsletterDoubleOptIn;
use BLR\Backline\Modules\Outreach\Module as OutreachModule;
use BLR\Backline\Modules\Tasks\Module as TasksModule;
use BLR\Backline\Modules\Events\Module as EventsModule;
use BLR\Backline\Modules\Finances\Module as FinancesModule;

final class FrontendFormHandler {
    public static function maybe_handle(array $ctx): array {
        $request_method = strtoupper(sanitize_text_field((string) wp_unslash($_SERVER['REQUEST_METHOD'] ?? 'GET')));
        if ($request_method !== 'POST') {
            return $ctx;
        }

        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Routing hint only; each handler verifies its own nonce before processing.
        $submitted_action = sanitize_text_field((string) wp_unslash($_POST['blr_ba_action'] ?? ''));
        if ($submitted_action === TrackBulkUpload::ACTION) {
            return self::handle_bulk_track_upload($ctx);
        }

        $page = (string) ($ctx['page'] ?? '');

        $module_map = [
            'contacts/new' => 'contacts',
            'contacts/edit' => 'contacts',
            'albums/new' => 'discography',
            'albums/edit' => 'discography',
            'discography/new' => 'discography',
            'discography/edit' => 'discography',
            'tracks/new' => 'discography',
            'tracks/edit' => 'discography',
            'events/new' => 'events',
            'events/edit' => 'events',
            'finances/new' => 'finances',
            'finances/edit' => 'finances',
            'outreach/new' => 'outreach',
            'outreach/edit' => 'outreach',
            'outreach/view' => 'outreach',
            'tasks/new' => 'tasks',
            'tasks/edit' => 'tasks',
        ];

        if (isset($module_map[$page]) && !SettingsPage::is_module_enabled($module_map[$page])) {
            return $ctx;
        }

        switch ($page) {
            case 'contacts/new':
            case 'contacts/edit':
                return self::handle_contact_form($ctx);

            case 'albums/new':
            case 'albums/edit':
            case 'discography/new':
            case 'discography/edit':
                return self::handle_release_form($ctx);

            case 'tracks/new':
            case 'tracks/edit':
                return self::handle_track_form($ctx);

            case 'events/new':
            case 'events/edit':
                return self::handle_event_form($ctx);

            case 'finances/new':
            case 'finances/edit':
                return self::handle_finance_form($ctx);

            case 'login':
                return self::handle_login($ctx);

            case 'outreach/new':
            case 'outreach/edit':
                return self::handle_outreach_form($ctx);

            case 'outreach/view':
                return self::handle_outreach_view($ctx);

            case 'tasks/new':
            case 'tasks/edit':
                return self::handle_task_form($ctx);

        }

        $filtered_ctx = apply_filters('blr_backline_frontend_form_context', $ctx, $submitted_action);
        return is_array($filtered_ctx) ? $filtered_ctx : $ctx;
    }

    private static function handle_contact_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_contact')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_contact')) {
            $errors[] = __('You do not have permission to edit this contact.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_contact')) {
            $errors[] = __('You do not have permission to create contacts.', 'blr-backline');
        } else {
            $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
            $notes = wp_kses_post(wp_unslash($_POST['notes'] ?? ''));
            if ($title === '') {
                $errors[] = __('Name is required.', 'blr-backline');
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Normalized immediately by Contacts::normalize_type().
            $contact_type_raw = isset($_POST['blr_contact_type']) ? (string) wp_unslash($_POST['blr_contact_type']) : '';
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Normalized immediately by NewsletterDoubleOptIn::normalize_status().
            $newsletter_status_raw = isset($_POST['blr_contact_newsletter_status']) ? (string) wp_unslash($_POST['blr_contact_newsletter_status']) : '';
            $newsletter_status = NewsletterDoubleOptIn::normalize_status($newsletter_status_raw);
            if ($newsletter_status === '') {
                $newsletter_status = !empty($_POST['blr_contact_newsletter_signup'])
                    ? NewsletterDoubleOptIn::STATUS_SUBSCRIBED
                    : NewsletterDoubleOptIn::STATUS_UNSUBSCRIBED;
            }

            $meta_fields = [
                'blr_contact_first_name' => sanitize_text_field(wp_unslash($_POST['blr_contact_first_name'] ?? '')),
                'blr_contact_role' => sanitize_text_field(wp_unslash($_POST['blr_contact_role'] ?? '')),
                'blr_contact_type' => Contacts::normalize_type($contact_type_raw),
                'blr_contact_company' => sanitize_text_field(wp_unslash($_POST['blr_contact_company'] ?? '')),
                'blr_contact_email' => sanitize_email(wp_unslash($_POST['blr_contact_email'] ?? '')),
                'blr_contact_phone' => sanitize_text_field(wp_unslash($_POST['blr_contact_phone'] ?? '')),
                'blr_contact_website' => esc_url_raw(wp_unslash($_POST['blr_contact_website'] ?? '')),
                'blr_contact_city' => sanitize_text_field(wp_unslash($_POST['blr_contact_city'] ?? '')),
                'blr_contact_state' => sanitize_text_field(wp_unslash($_POST['blr_contact_state'] ?? '')),
                'blr_contact_notes' => wp_kses_post(wp_unslash($_POST['blr_contact_notes'] ?? '')),
            ];

            if (!$errors) {
                $data = [
                    'post_title' => $title,
                    'post_content' => $notes,
                    'post_status' => 'publish',
                    'post_type' => 'blr_contact',
                ];

                if ($is_edit) {
                    $data['ID'] = $post_id;
                    $res = wp_update_post(wp_slash($data), true);
                } else {
                    $res = wp_insert_post(wp_slash($data), true);
                    if (!is_wp_error($res)) {
                        $post_id = (int) $res;
                    }
                }

                if (is_wp_error($res)) {
                    $errors[] = $res->get_error_message();
                } else {
                    foreach ($meta_fields as $k => $v) {
                        update_post_meta($post_id, $k, $v);
                    }

                    NewsletterDoubleOptIn::apply_status($post_id, $newsletter_status);
                    OutreachModule::flush_contact_meta_option_caches();
                    CrudHelpers::redirect(self::base_url($ctx) . 'contacts/edit/' . $post_id . '/?saved=1');
                }
            }
        }

        $ctx['errors'] = $errors;
        return $ctx;
    }

    private static function handle_bulk_track_upload(array $ctx): array {
        $base = self::base_url($ctx);
        $page = (string) ($ctx['page'] ?? 'dashboard');
        $redirect_url = $page === 'tracks' ? $base . 'tracks/' : $base;

        TrackBulkUpload::handle_submission($redirect_url);
        return $ctx;
    }

    private static function handle_release_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_release')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_release')) {
            $errors[] = __('You do not have permission to edit this album.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_release')) {
            $errors[] = __('You do not have permission to create albums.', 'blr-backline');
        } else {
            $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
            $content = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
            if ($title === '') {
                $errors[] = __('Title is required.', 'blr-backline');
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Normalized and validated by self::normalized_upc().
            $release_upc = self::normalized_upc(wp_unslash($_POST['blr_upc'] ?? ''));
            if ($release_upc['error'] !== '') {
                $errors[] = $release_upc['error'];
            }

            $release_date_input = sanitize_text_field(wp_unslash($_POST['blr_release_date'] ?? ''));
            $release_date = self::validated_date_input($release_date_input, __('Release date must be a valid date in YYYY-MM-DD format.', 'blr-backline'));
            if ($release_date['error'] !== '') {
                $errors[] = $release_date['error'];
            }

            $meta_fields = [
                'blr_release_date' => $release_date['value'],
                'blr_artist' => sanitize_text_field(wp_unslash($_POST['blr_artist'] ?? '')),
                'blr_upc' => $release_upc['value'],
                'blr_spotify_url' => esc_url_raw(wp_unslash($_POST['blr_spotify_url'] ?? '')),
                'blr_apple_music_url' => esc_url_raw(wp_unslash($_POST['blr_apple_music_url'] ?? '')),
                'blr_bandcamp_url' => esc_url_raw(wp_unslash($_POST['blr_bandcamp_url'] ?? '')),
                'blr_youtube_url' => esc_url_raw(wp_unslash($_POST['blr_youtube_url'] ?? '')),
                'blr_soundcloud_url' => esc_url_raw(wp_unslash($_POST['blr_soundcloud_url'] ?? '')),
                'blr_release_notes' => wp_kses_post(wp_unslash($_POST['blr_release_notes'] ?? '')),
            ];

            $release_type_term_id = self::validated_term_id(absint(wp_unslash($_POST['blr_release_type_term'] ?? 0)), 'blr_release_type');
            if (!empty($_POST['blr_release_type_term']) && $release_type_term_id === 0) {
                $errors[] = __('The selected release type is invalid.', 'blr-backline');
            }

            $release_status_term_id = self::validated_term_id(absint(wp_unslash($_POST['blr_release_status_term'] ?? 0)), 'blr_release_status');
            if (!empty($_POST['blr_release_status_term']) && $release_status_term_id === 0) {
                $errors[] = __('The selected release status is invalid.', 'blr-backline');
            }

            $selected_cover_image_id = absint(wp_unslash($_POST['cover_image_id'] ?? 0));
            if (
                !empty($_POST['cover_image_id'])
                && (
                    !Media::is_valid_image_attachment_id($selected_cover_image_id)
                    || !Media::current_user_can_use_attachment($selected_cover_image_id, $is_edit ? $post_id : null)
                )
            ) {
                $errors[] = __('The selected cover image is invalid or not available to your Backline account.', 'blr-backline');
            }

            $has_uploaded_audio = CrudHelpers::has_selected_uploads('audio_files');
            $has_cover_upload = CrudHelpers::has_selected_uploads('cover_image');
            if (!$errors && $has_uploaded_audio && !Access::can_create_post_type('blr_track')) {
                $errors[] = __('You do not have permission to create tracks from uploaded audio files.', 'blr-backline');
            }
            if ($has_uploaded_audio) {
                $errors = array_merge($errors, CrudHelpers::validate_upload_selection('audio_files', CrudHelpers::audio_mimes(), CrudHelpers::audio_max_file_size()));
            }
            if ($has_cover_upload) {
                $errors = array_merge($errors, CrudHelpers::validate_upload_selection('cover_image', CrudHelpers::image_mimes(), CrudHelpers::visual_max_file_size(), 1));
            }

            $selected_track_ids = [];
            $track_ids_submitted = isset($_POST['track_ids_submitted']) && sanitize_text_field((string) wp_unslash($_POST['track_ids_submitted'])) === '1';
            $posted_track_ids = (isset($_POST['track_ids']) && is_array($_POST['track_ids'])) ? array_map('absint', wp_unslash($_POST['track_ids'])) : [];
            if ($track_ids_submitted && !empty($posted_track_ids)) {
                $selected_track_ids = self::validated_post_type_ids($posted_track_ids, 'blr_track');
                if (count($selected_track_ids) !== count(array_values(array_unique(array_filter($posted_track_ids))))) {
                    $errors[] = __('One or more selected tracks are invalid.', 'blr-backline');
                }
            } elseif (!$track_ids_submitted && $is_edit) {
                $selected_track_ids = self::validated_post_type_ids((array) get_post_meta($post_id, 'blr_track_ids', true), 'blr_track');
            }

            $selected_track_ids = array_values(array_unique(array_map('absint', $selected_track_ids)));
            $ordered_ids = [];
            $raw_order = sanitize_text_field(wp_unslash($_POST['track_order'] ?? ''));
            if (count($selected_track_ids) > 1 && $raw_order === '') {
                $errors[] = __('Track order could not be determined. Please reorder the tracks again before saving.', 'blr-backline');
            }
            if ($raw_order !== '') {
                $parts = preg_split('/\s*,\s*/', $raw_order, -1, PREG_SPLIT_NO_EMPTY);
                foreach ($parts as $p) {
                    $tid = absint($p);
                    if ($tid) {
                        $ordered_ids[] = $tid;
                    }
                }
            }

            if (!$errors) {
                $previous_post_state = $is_edit ? self::capture_post_state($post_id, array_merge(array_keys($meta_fields), ['blr_release_type', 'blr_track_ids']), ['blr_release_type', 'blr_release_status']) : [];

                $data = [
                    'post_title' => $title,
                    'post_content' => $content,
                    'post_status' => 'publish',
                    'post_type' => 'blr_release',
                ];

                $created_post = false;
                if ($is_edit) {
                    $data['ID'] = $post_id;
                    $res = wp_update_post(wp_slash($data), true);
                } else {
                    $res = wp_insert_post(wp_slash($data), true);
                    if (!is_wp_error($res)) {
                        $post_id = (int) $res;
                        $created_post = true;
                    }
                }

                if (is_wp_error($res)) {
                    $errors[] = $res->get_error_message();
                } else {
                    foreach ($meta_fields as $k => $v) {
                        update_post_meta($post_id, $k, $v);
                    }

                    if ($release_type_term_id) {
                        $set = wp_set_object_terms($post_id, [$release_type_term_id], 'blr_release_type', false);
                        if (!is_wp_error($set)) {
                            $term = get_term($release_type_term_id, 'blr_release_type');
                            if ($term && !is_wp_error($term)) {
                                update_post_meta($post_id, 'blr_release_type', (string) $term->name);
                            }
                        }
                    } else {
                        wp_set_object_terms($post_id, [], 'blr_release_type', false);
                        update_post_meta($post_id, 'blr_release_type', '');
                    }

                    if ($release_status_term_id) {
                        wp_set_object_terms($post_id, [$release_status_term_id], 'blr_release_status', false);
                    } else {
                        wp_set_object_terms($post_id, [], 'blr_release_status', false);
                    }

                    $track_ids = [];
                    foreach ($ordered_ids as $tid) {
                        if (in_array($tid, $selected_track_ids, true) && !in_array($tid, $track_ids, true)) {
                            $track_ids[] = $tid;
                        }
                    }
                    foreach ($selected_track_ids as $tid) {
                        if (!in_array($tid, $track_ids, true)) {
                            $track_ids[] = $tid;
                        }
                    }

                    $created_track_ids = [];
                    $created_track_attachment_ids = [];
                    $created_cover_attachment_ids = [];

                    if (!$errors) {
                        $media_result = Media::assign_featured_image_result($post_id, $selected_cover_image_id, 'cover_image');
                        $created_cover_attachment_ids = array_map('intval', (array) ($media_result['created_attachment_ids'] ?? []));
                        foreach ((array) ($media_result['errors'] ?? []) as $media_error) {
                            $errors[] = (string) $media_error;
                        }
                    }

                    if (!$errors) {
                        $uploads = CrudHelpers::handle_multi_upload('audio_files', $post_id, CrudHelpers::audio_mimes(), CrudHelpers::audio_max_file_size());
                        $audio_upload_errors = CrudHelpers::get_last_upload_errors();
                        if (!empty($uploads)) {
                            foreach ($uploads as $u) {
                                $filename = (string) ($u['filename'] ?? '');
                                $base_title = $filename !== '' ? preg_replace('/\.[^.]+$/', '', $filename) : __('New Track', 'blr-backline');
                                $base_title = sanitize_text_field((string) $base_title);
                                if ($base_title === '') {
                                    $base_title = __('New Track', 'blr-backline');
                                }

                                $track_post_id = wp_insert_post(wp_slash([
                                    'post_title' => $base_title,
                                    'post_status' => 'publish',
                                    'post_type' => 'blr_track',
                                    'post_content' => '',
                                ]), true);
                                if (is_wp_error($track_post_id) || !$track_post_id) {
                                    /* translators: %s: uploaded audio file name. */
                                    $errors[] = sprintf(__('The uploaded audio file %s could not be turned into a track.', 'blr-backline'), $filename !== '' ? $filename : __('Unknown file', 'blr-backline'));
                                    CrudHelpers::cleanup_upload_result($u);
                                    continue;
                                }

                                $track_post_id = (int) $track_post_id;
                                $created_track_ids[] = $track_post_id;
                                if (!empty($u['url'])) {
                                    update_post_meta($track_post_id, 'blr_audio_url', (string) $u['url']);
                                }
                                if (!empty($u['attachment_id'])) {
                                    $attachment_id = (int) $u['attachment_id'];
                                    wp_update_post(wp_slash([
                                        'ID' => $attachment_id,
                                        'post_parent' => $track_post_id,
                                    ]));
                                    update_post_meta($track_post_id, 'blr_audio_attachment_id', $attachment_id);
                                    Media::mark_attachment_as_post_owned($attachment_id, $track_post_id, 'track_audio');
                                    $created_track_attachment_ids[] = $attachment_id;

                                    $duration = CrudHelpers::attachment_duration($attachment_id);
                                    if ($duration !== '') {
                                        update_post_meta($track_post_id, 'blr_duration', $duration);
                                    }
                                }
                                $track_ids[] = $track_post_id;
                            }
                        }

                        foreach ($audio_upload_errors as $upload_error) {
                            $errors[] = (string) $upload_error;
                        }
                    }

                    if ($errors) {
                        self::cleanup_created_tracks($created_track_ids, $created_track_attachment_ids);
                        if ($created_cover_attachment_ids) {
                            Media::cleanup_created_attachments($created_cover_attachment_ids);
                        }
                        if ($created_post) {
                            wp_delete_post($post_id, true);
                        } elseif (!empty($previous_post_state)) {
                            self::restore_post_state($post_id, $previous_post_state);
                        }
                    } else {
                        update_post_meta($post_id, 'blr_track_ids', array_values(array_unique(array_map('absint', $track_ids))));
                        CrudHelpers::redirect(self::base_url($ctx) . 'albums/edit/' . $post_id . '/?saved=1');
                    }
                }
            }
        }

        $ctx['errors'] = array_values(array_unique(array_filter(array_map('strval', $errors), 'strlen')));
        return $ctx;
    }

    private static function handle_track_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_track')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_track')) {
            $errors[] = __('You do not have permission to edit this track.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_track')) {
            $errors[] = __('You do not have permission to create tracks.', 'blr-backline');
        } else {
            $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
            $content = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
            if ($title === '') {
                $errors[] = __('Title is required.', 'blr-backline');
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Normalized and validated by self::normalized_isrc().
            $track_isrc = self::normalized_isrc(wp_unslash($_POST['blr_isrc'] ?? ''));
            if ($track_isrc['error'] !== '') {
                $errors[] = $track_isrc['error'];
            }

            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Normalized and validated by self::normalized_upc().
            $track_upc = self::normalized_upc(wp_unslash($_POST['blr_upc'] ?? ''));
            if ($track_upc['error'] !== '') {
                $errors[] = $track_upc['error'];
            }

            $release_date_input = sanitize_text_field(wp_unslash($_POST['blr_release_date'] ?? ''));
            $release_date = self::validated_date_input($release_date_input, __('Release date must be a valid date in YYYY-MM-DD format.', 'blr-backline'));
            if ($release_date['error'] !== '') {
                $errors[] = $release_date['error'];
            }

            $meta_fields = [
                'blr_track_number' => (string) absint(wp_unslash($_POST['blr_track_number'] ?? '')),
                'blr_duration' => sanitize_text_field(wp_unslash($_POST['blr_duration'] ?? '')),
                'blr_bpm' => (string) absint(wp_unslash($_POST['blr_bpm'] ?? '')),
                'blr_key' => sanitize_text_field(wp_unslash($_POST['blr_key'] ?? '')),
                'blr_isrc' => $track_isrc['value'],
                'blr_upc' => $track_upc['value'],
                'blr_audio_url' => esc_url_raw(wp_unslash($_POST['blr_audio_url'] ?? '')),
                'blr_release_date' => $release_date['value'],
                'blr_artist' => sanitize_text_field(wp_unslash($_POST['blr_artist'] ?? '')),
                'blr_spotify_url' => esc_url_raw(wp_unslash($_POST['blr_spotify_url'] ?? '')),
                'blr_apple_music_url' => esc_url_raw(wp_unslash($_POST['blr_apple_music_url'] ?? '')),
                'blr_bandcamp_url' => esc_url_raw(wp_unslash($_POST['blr_bandcamp_url'] ?? '')),
                'blr_youtube_url' => esc_url_raw(wp_unslash($_POST['blr_youtube_url'] ?? '')),
                'blr_soundcloud_url' => esc_url_raw(wp_unslash($_POST['blr_soundcloud_url'] ?? '')),
                'blr_release_notes' => wp_kses_post(wp_unslash($_POST['blr_release_notes'] ?? '')),
            ];
            $release_status_term_id = self::validated_term_id(absint(wp_unslash($_POST['blr_release_status_term'] ?? 0)), 'blr_release_status');
            if (!empty($_POST['blr_release_status_term']) && $release_status_term_id === 0) {
                $errors[] = __('The selected release status is invalid.', 'blr-backline');
            }

            $selected_track_image_id = absint(wp_unslash($_POST['track_image_id'] ?? 0));
            if (
                !empty($_POST['track_image_id'])
                && (
                    !Media::is_valid_image_attachment_id($selected_track_image_id)
                    || !Media::current_user_can_use_attachment($selected_track_image_id, $is_edit ? $post_id : null)
                )
            ) {
                $errors[] = __('The selected track image is invalid or not available to your Backline account.', 'blr-backline');
            }

            $audio_picker_submitted = array_key_exists('track_audio_attachment_id', $_POST);
            $selected_track_audio_attachment_id = absint(wp_unslash($_POST['track_audio_attachment_id'] ?? 0));
            if (
                !empty($_POST['track_audio_attachment_id'])
                && (
                    !Media::is_valid_audio_attachment_id($selected_track_audio_attachment_id)
                    || !Media::current_user_can_use_attachment($selected_track_audio_attachment_id, $is_edit ? $post_id : null)
                )
            ) {
                $errors[] = __('The selected audio file is invalid or not available to your Backline account.', 'blr-backline');
            }

            $has_audio_upload = CrudHelpers::has_selected_uploads('audio_file');
            $has_visual_upload = CrudHelpers::has_selected_uploads('track_image_file');
            if ($has_audio_upload) {
                $errors = array_merge($errors, CrudHelpers::validate_upload_selection('audio_file', CrudHelpers::audio_mimes(), CrudHelpers::audio_max_file_size(), 1));
            }
            if ($has_visual_upload) {
                $errors = array_merge($errors, CrudHelpers::validate_upload_selection('track_image_file', CrudHelpers::image_mimes(), CrudHelpers::visual_max_file_size(), 1));
            }

            if (!$errors) {
                $previous_post_state = $is_edit ? self::capture_post_state($post_id, array_merge(array_keys($meta_fields), ['blr_audio_attachment_id']), ['blr_release_status']) : [];

                $data = [
                    'post_title' => $title,
                    'post_content' => $content,
                    'post_status' => 'publish',
                    'post_type' => 'blr_track',
                ];
                $created_post = false;
                if ($is_edit) {
                    $data['ID'] = $post_id;
                    $res = wp_update_post(wp_slash($data), true);
                } else {
                    $res = wp_insert_post(wp_slash($data), true);
                    if (!is_wp_error($res)) {
                        $post_id = (int) $res;
                        $created_post = true;
                    }
                }

                if (is_wp_error($res)) {
                    $errors[] = $res->get_error_message();
                } else {
                    foreach ($meta_fields as $k => $v) {
                        update_post_meta($post_id, $k, $v);
                    }
                    if ($release_status_term_id) {
                        wp_set_object_terms($post_id, [$release_status_term_id], 'blr_release_status', false);
                    } else {
                        wp_set_object_terms($post_id, [], 'blr_release_status', false);
                    }
                    $previous_audio_attachment_id = $is_edit ? absint(get_post_meta($post_id, 'blr_audio_attachment_id', true)) : 0;
                    $replacement_audio_attachment_id = 0;
                    $replacement_audio_url = '';
                    $clear_audio_selection = false;
                    $created_track_image_attachment_ids = [];
                    $upload = CrudHelpers::handle_upload('audio_file', $post_id, CrudHelpers::audio_mimes());
                    $audio_upload_errors = CrudHelpers::get_last_upload_errors();
                    if ($upload) {
                        $replacement_audio_url = !empty($upload['url']) ? (string) $upload['url'] : '';
                        $replacement_audio_attachment_id = !empty($upload['attachment_id']) ? (int) $upload['attachment_id'] : 0;
                    }

                    if ($has_audio_upload && (!$upload || !empty($audio_upload_errors))) {
                        foreach ($audio_upload_errors as $upload_error) {
                            $errors[] = (string) $upload_error;
                        }
                        if (!$audio_upload_errors) {
                            $errors[] = __('The uploaded audio file could not be saved.', 'blr-backline');
                        }
                    }

                    if (!$errors && !$has_audio_upload) {
                        if ($selected_track_audio_attachment_id > 0) {
                            $replacement_audio_attachment_id = $selected_track_audio_attachment_id;
                            $replacement_audio_url = (string) wp_get_attachment_url($selected_track_audio_attachment_id);
                            if ($replacement_audio_url === '') {
                                $errors[] = __('The selected audio file could not be loaded.', 'blr-backline');
                            }
                        } elseif ($audio_picker_submitted && $previous_audio_attachment_id > 0) {
                            $clear_audio_selection = true;
                            $replacement_audio_attachment_id = 0;
                            $replacement_audio_url = '';
                        }
                    }

                    if (!$errors) {
                        $media_result = Media::assign_featured_image_result($post_id, $selected_track_image_id, 'track_image_file');
                        $created_track_image_attachment_ids = array_map('intval', (array) ($media_result['created_attachment_ids'] ?? []));
                        foreach ((array) ($media_result['errors'] ?? []) as $media_error) {
                            $errors[] = (string) $media_error;
                        }
                    }

                    if ($errors) {
                        if ($replacement_audio_attachment_id > 0 && $has_audio_upload) {
                            CrudHelpers::cleanup_upload_result($upload);
                        }
                        if ($created_track_image_attachment_ids) {
                            Media::cleanup_created_attachments($created_track_image_attachment_ids);
                        }
                        if ($created_post) {
                            wp_delete_post($post_id, true);
                        } elseif (!empty($previous_post_state)) {
                            self::restore_post_state($post_id, $previous_post_state);
                        }
                    } else {
                        if ($replacement_audio_url !== '') {
                            update_post_meta($post_id, 'blr_audio_url', $replacement_audio_url);
                        } elseif ($has_audio_upload || $clear_audio_selection) {
                            delete_post_meta($post_id, 'blr_audio_url');
                        }

                        if ($replacement_audio_attachment_id > 0) {
                            update_post_meta($post_id, 'blr_audio_attachment_id', $replacement_audio_attachment_id);
                            if ($has_audio_upload) {
                                Media::mark_attachment_as_post_owned($replacement_audio_attachment_id, $post_id, 'track_audio');
                            }

                            $replacement_duration = CrudHelpers::attachment_duration($replacement_audio_attachment_id);
                            if ($replacement_duration !== '') {
                                update_post_meta($post_id, 'blr_duration', $replacement_duration);
                            }
                        } elseif ($has_audio_upload || $clear_audio_selection) {
                            delete_post_meta($post_id, 'blr_audio_attachment_id');
                        }

                        if ($previous_audio_attachment_id > 0 && $previous_audio_attachment_id !== $replacement_audio_attachment_id && Media::is_post_owned_attachment($previous_audio_attachment_id, $post_id)) {
                            CrudHelpers::delete_attachment($previous_audio_attachment_id);
                        }
                        CrudHelpers::redirect(self::base_url($ctx) . 'tracks/edit/' . $post_id . '/?saved=1');
                    }
                }
            }
        }

        $ctx['errors'] = array_values(array_unique(array_filter(array_map('strval', $errors), 'strlen')));
        return $ctx;
    }

    private static function handle_event_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_event')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_event')) {
            $errors[] = __('You do not have permission to edit this event.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_event')) {
            $errors[] = __('You do not have permission to create events.', 'blr-backline');
        } else {
            $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
            $content = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
            if ($title === '') {
                $errors[] = __('Title is required.', 'blr-backline');
            }

            $raw_event_status = sanitize_text_field(wp_unslash($_POST['blr_event_status'] ?? ''));
            $normalized_event_status = EventsModule::normalize_status($raw_event_status);
            if (trim($raw_event_status) !== '' && $normalized_event_status === '') {
                $errors[] = __('The selected event status is invalid.', 'blr-backline');
            }

            $show_frontend_raw = isset($_POST['blr_show_frontend']) ? sanitize_text_field(wp_unslash($_POST['blr_show_frontend'])) : '0';
            $show_frontend = EventsModule::normalize_show_frontend($show_frontend_raw);

            $event_start_raw = isset($_POST['blr_event_start']) ? sanitize_text_field((string) wp_unslash($_POST['blr_event_start'])) : '';
            $event_end_raw = isset($_POST['blr_event_end']) ? sanitize_text_field((string) wp_unslash($_POST['blr_event_end'])) : '';
            $doors_raw = isset($_POST['blr_doors']) ? sanitize_text_field((string) wp_unslash($_POST['blr_doors'])) : '';
            $set_time_raw = isset($_POST['blr_set_time']) ? sanitize_text_field((string) wp_unslash($_POST['blr_set_time'])) : '';
            $load_in_raw = isset($_POST['blr_load_in']) ? sanitize_text_field((string) wp_unslash($_POST['blr_load_in'])) : '';
            $soundcheck_raw = isset($_POST['blr_soundcheck']) ? sanitize_text_field((string) wp_unslash($_POST['blr_soundcheck'])) : '';

            $event_start = self::validated_datetime_input($event_start_raw, __('Event start must use the format YYYY-MM-DD HH:MM.', 'blr-backline'));
            $event_end = self::validated_datetime_input($event_end_raw, __('Event end must use the format YYYY-MM-DD HH:MM.', 'blr-backline'));
            $doors = self::validated_time_input($doors_raw, __('Doors time must use 24-hour HH:MM.', 'blr-backline'));
            $set_time = self::validated_time_input($set_time_raw, __('Set time must use 24-hour HH:MM.', 'blr-backline'));
            $load_in = self::validated_time_input($load_in_raw, __('Load-in time must use 24-hour HH:MM.', 'blr-backline'));
            $soundcheck = self::validated_time_input($soundcheck_raw, __('Soundcheck time must use 24-hour HH:MM.', 'blr-backline'));

            foreach ([$event_start, $event_end, $doors, $set_time, $load_in, $soundcheck] as $validation_result) {
                if ($validation_result['error'] !== '') {
                    $errors[] = $validation_result['error'];
                }
            }

            if ($event_start['value'] !== '' && $event_end['value'] !== '' && $event_end['value'] < $event_start['value']) {
                $errors[] = __('Event end cannot be earlier than the event start.', 'blr-backline');
            }

            $meta_fields = [
                'blr_event_start' => $event_start['value'],
                'blr_event_end' => $event_end['value'],
                'blr_timezone' => sanitize_text_field(wp_unslash($_POST['blr_timezone'] ?? '')),
                'blr_venue_name' => sanitize_text_field(wp_unslash($_POST['blr_venue_name'] ?? '')),
                'blr_venue_website' => esc_url_raw(wp_unslash($_POST['blr_venue_website'] ?? '')),
                'blr_venue_address' => sanitize_text_field(wp_unslash($_POST['blr_venue_address'] ?? '')),
                'blr_venue_city' => sanitize_text_field(wp_unslash($_POST['blr_venue_city'] ?? '')),
                'blr_venue_state' => sanitize_text_field(wp_unslash($_POST['blr_venue_state'] ?? '')),
                'blr_venue_postal' => sanitize_text_field(wp_unslash($_POST['blr_venue_postal'] ?? '')),
                'blr_venue_country' => sanitize_text_field(wp_unslash($_POST['blr_venue_country'] ?? '')),
                'blr_ticket_url' => esc_url_raw(wp_unslash($_POST['blr_ticket_url'] ?? '')),
                'blr_event_url' => esc_url_raw(wp_unslash($_POST['blr_event_url'] ?? '')),
                'blr_event_status' => $normalized_event_status,
                'blr_age_restriction' => sanitize_text_field(wp_unslash($_POST['blr_age_restriction'] ?? '')),
                'blr_doors' => $doors['value'],
                'blr_set_time' => $set_time['value'],
                'blr_load_in' => $load_in['value'],
                'blr_soundcheck' => $soundcheck['value'],
                'blr_payout' => sanitize_text_field(wp_unslash($_POST['blr_payout'] ?? '')),
                'blr_notes' => wp_kses_post(wp_unslash($_POST['blr_notes'] ?? '')),
                'blr_show_frontend' => $show_frontend,
            ];

            $selected_event_image_id = absint(wp_unslash($_POST['event_image_id'] ?? 0));
            if (
                !empty($_POST['event_image_id'])
                && (
                    !Media::is_valid_image_attachment_id($selected_event_image_id)
                    || !Media::current_user_can_use_attachment($selected_event_image_id, $is_edit ? $post_id : null)
                )
            ) {
                $errors[] = __('The selected event image is invalid or not available to your Backline account.', 'blr-backline');
            }

            $has_visual_upload = CrudHelpers::has_selected_uploads('event_image_file');
            if ($has_visual_upload) {
                $errors = array_merge($errors, CrudHelpers::validate_upload_selection('event_image_file', CrudHelpers::image_mimes(), CrudHelpers::visual_max_file_size(), 1));
            }

            if (!$errors) {
                $data = [
                    'post_title' => $title,
                    'post_content' => $content,
                    'post_status' => 'publish',
                    'post_type' => 'blr_event',
                ];
                $created_post = false;
                $created_event_image_attachment_ids = [];
                $previous_thumbnail_id = 0;

                if ($is_edit) {
                    $previous_thumbnail_id = absint(get_post_thumbnail_id($post_id));
                    $media_result = Media::assign_featured_image_result($post_id, $selected_event_image_id, 'event_image_file');
                    $created_event_image_attachment_ids = array_map('intval', (array) ($media_result['created_attachment_ids'] ?? []));
                    foreach ((array) ($media_result['errors'] ?? []) as $media_error) {
                        $errors[] = (string) $media_error;
                    }
                }

                if (!$errors) {
                    if ($is_edit) {
                        $data['ID'] = $post_id;
                        $res = wp_update_post(wp_slash($data), true);
                    } else {
                        $res = wp_insert_post(wp_slash($data), true);
                        if (!is_wp_error($res)) {
                            $post_id = (int) $res;
                            $created_post = true;
                        }
                    }

                    if (is_wp_error($res)) {
                        $errors[] = $res->get_error_message();
                        if ($is_edit) {
                            if ($previous_thumbnail_id > 0) {
                                set_post_thumbnail($post_id, $previous_thumbnail_id);
                            } else {
                                delete_post_thumbnail($post_id);
                            }
                            if ($created_event_image_attachment_ids) {
                                Media::cleanup_created_attachments($created_event_image_attachment_ids);
                            }
                        }
                    } else {
                        foreach ($meta_fields as $k => $v) {
                            update_post_meta($post_id, $k, $v);
                        }

                        if (!$is_edit) {
                            $media_result = Media::assign_featured_image_result($post_id, $selected_event_image_id, 'event_image_file');
                            $created_event_image_attachment_ids = array_map('intval', (array) ($media_result['created_attachment_ids'] ?? []));
                            foreach ((array) ($media_result['errors'] ?? []) as $media_error) {
                                $errors[] = (string) $media_error;
                            }
                        }

                        if ($errors && $created_post) {
                            wp_delete_post($post_id, true);
                        }

                        if (!$errors) {
                            CrudHelpers::redirect(self::base_url($ctx) . 'events/edit/' . $post_id . '/?saved=1');
                        }
                    }
                }
            }
        }

        $ctx['errors'] = array_values(array_unique(array_filter(array_map('strval', $errors), 'strlen')));
        return $ctx;
    }


    private static function request_ip_address(): string {
        $remote_addr = self::server_value('REMOTE_ADDR');

        if (!self::should_trust_proxy_headers()) {
            return self::validated_ip_or_unknown($remote_addr);
        }

        if (!self::is_trusted_proxy($remote_addr)) {
            return self::validated_ip_or_unknown($remote_addr);
        }

        $cf_connecting_ip = self::server_value('HTTP_CF_CONNECTING_IP');
        $x_forwarded_for = self::server_value('HTTP_X_FORWARDED_FOR');

        $candidates = [
            $cf_connecting_ip,
            self::first_forwarded_ip($x_forwarded_for),
            $remote_addr,
        ];

        foreach ($candidates as $candidate) {
            $validated = filter_var($candidate, FILTER_VALIDATE_IP);
            if ($validated !== false) {
                return (string) $validated;
            }
        }

        return 'unknown';
    }

    private static function should_trust_proxy_headers(): bool {
        $default = false;

        if (defined('BLR_BA_TRUST_PROXY_HEADERS')) {
            $default = (bool) BLR_BA_TRUST_PROXY_HEADERS;
        }

        /**
         * Filters whether BLR Backline should trust forwarded proxy headers.
         *
         * WARNING: Enabling this without a real trusted reverse proxy or CDN can allow
         * clients to spoof IP-based rate limiting.
         *
         * @param bool $default Whether proxy headers should be trusted.
         */
        return (bool) apply_filters('blr_ba_trust_proxy_headers', $default);
    }

    private static function is_trusted_proxy(string $remote_addr): bool {
        $remote_addr = self::validated_ip_or_unknown($remote_addr);
        if ($remote_addr === 'unknown') {
            return false;
        }

        $trusted_proxies = [];

        if (defined('BLR_BA_TRUSTED_PROXIES') && is_array(BLR_BA_TRUSTED_PROXIES)) {
            $trusted_proxies = BLR_BA_TRUSTED_PROXIES;
        }

        /**
         * Filters the list of trusted proxy IPs or CIDR ranges.
         *
         * Example values:
         * - 203.0.113.10
         * - 203.0.113.0/24
         * - 2001:db8::/32
         *
         * @param string[] $trusted_proxies Trusted proxy IPs or CIDR ranges.
         */
        $trusted_proxies = apply_filters('blr_ba_trusted_proxies', $trusted_proxies);

        if (!is_array($trusted_proxies) || $trusted_proxies === []) {
            return false;
        }

        foreach ($trusted_proxies as $proxy) {
            if (!is_string($proxy) || $proxy === '') {
                continue;
            }

            $proxy = sanitize_text_field($proxy);

            if (self::ip_matches_proxy_rule($remote_addr, $proxy)) {
                return true;
            }
        }

        return false;
    }

    private static function ip_matches_proxy_rule(string $ip, string $rule): bool {
        if (strpos($rule, '/') === false) {
            return hash_equals($rule, $ip);
        }

        return self::ip_in_cidr($ip, $rule);
    }

    private static function ip_in_cidr(string $ip, string $cidr): bool {
        $parts = explode('/', $cidr, 2);
        if (count($parts) !== 2) {
            return false;
        }

        $subnet = $parts[0];
        $prefix = (int) $parts[1];

        $ip_bin = inet_pton($ip);
        $subnet_bin = inet_pton($subnet);

        if ($ip_bin === false || $subnet_bin === false) {
            return false;
        }

        if (strlen($ip_bin) !== strlen($subnet_bin)) {
            return false;
        }

        $max_prefix = strlen($ip_bin) * 8;
        if ($prefix < 0 || $prefix > $max_prefix) {
            return false;
        }

        $full_bytes = intdiv($prefix, 8);
        $extra_bits = $prefix % 8;

        if ($full_bytes > 0 && substr($ip_bin, 0, $full_bytes) !== substr($subnet_bin, 0, $full_bytes)) {
            return false;
        }

        if ($extra_bits === 0) {
            return true;
        }

        $mask = (0xFF << (8 - $extra_bits)) & 0xFF;
        $ip_byte = ord($ip_bin[$full_bytes]);
        $subnet_byte = ord($subnet_bin[$full_bytes]);

        return ($ip_byte & $mask) === ($subnet_byte & $mask);
    }

    private static function first_forwarded_ip(string $value): string {
        if ($value === '') {
            return '';
        }

        $parts = array_map('trim', explode(',', $value));

        return (string) ($parts[0] ?? '');
    }

    private static function server_value(string $key): string {
        if (!isset($_SERVER[$key]) || !is_string($_SERVER[$key])) {
            return '';
        }

        return sanitize_text_field(wp_unslash($_SERVER[$key]));
    }

    private static function validated_ip_or_unknown(string $value): string {
        $validated = filter_var($value, FILTER_VALIDATE_IP);

        return ($validated !== false) ? (string) $validated : 'unknown';
    }


    private static function handle_finance_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $previous_attachment_id = $is_edit ? absint(get_post_meta($post_id, FinancesModule::ATTACHMENT_META_KEY, true)) : 0;
        $errors = [];

        $values = [
            'title' => '',
            'content' => '',
            'blr_finance_kind' => '',
            'blr_finance_status' => '',
            'blr_finance_category' => '',
            'blr_finance_amount' => '',
            'blr_finance_currency' => 'USD',
            'blr_finance_payment_method' => '',
            'blr_finance_date' => '',
            'blr_finance_due_date' => '',
            'blr_finance_reference' => '',
            'blr_finance_contact_id' => '0',
            FinancesModule::ATTACHMENT_META_KEY => '0',
        ];

        if ($is_edit && $post_id > 0) {
            $post = get_post($post_id);
            if ($post instanceof \WP_Post && $post->post_type === FinancesModule::POST_TYPE) {
                $values['title'] = (string) $post->post_title;
                $values['content'] = (string) $post->post_content;
                foreach (array_keys($values) as $key) {
                    if (in_array($key, ['title', 'content'], true)) {
                        continue;
                    }
                    $values[$key] = (string) get_post_meta($post_id, $key, true);
                }
                if ($values['blr_finance_currency'] === '') {
                    $values['blr_finance_currency'] = 'USD';
                }
                if ($values['blr_finance_contact_id'] === '') {
                    $values['blr_finance_contact_id'] = '0';
                }
                if ($values[FinancesModule::ATTACHMENT_META_KEY] === '') {
                    $values[FinancesModule::ATTACHMENT_META_KEY] = '0';
                }
            }
        }

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_finance')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, FinancesModule::POST_TYPE)) {
            $errors[] = __('You do not have permission to edit this finance record.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type(FinancesModule::POST_TYPE)) {
            $errors[] = __('You do not have permission to create finance records.', 'blr-backline');
        } else {
            $values['title'] = sanitize_text_field((string) wp_unslash($_POST['title'] ?? ''));
            $values['content'] = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
            $kind_raw = sanitize_text_field((string) wp_unslash($_POST['blr_finance_kind'] ?? ''));
            $status_raw = sanitize_text_field((string) wp_unslash($_POST['blr_finance_status'] ?? ''));
            $amount_raw = sanitize_text_field((string) wp_unslash($_POST['blr_finance_amount'] ?? ''));
            $finance_date_raw = sanitize_text_field((string) wp_unslash($_POST['blr_finance_date'] ?? ''));
            $due_date_raw = sanitize_text_field((string) wp_unslash($_POST['blr_finance_due_date'] ?? ''));
            $values['blr_finance_kind'] = FinancesModule::normalize_kind($kind_raw);
            $values['blr_finance_status'] = FinancesModule::normalize_status($status_raw);
            $values['blr_finance_amount'] = Money::normalize_amount($amount_raw);
            $values['blr_finance_currency'] = strtoupper(sanitize_text_field((string) wp_unslash($_POST['blr_finance_currency'] ?? 'USD')));
            $values['blr_finance_date'] = Sanitizer::date_ymd($finance_date_raw);
            $values['blr_finance_due_date'] = Sanitizer::date_ymd($due_date_raw);
            $values['blr_finance_category'] = sanitize_text_field((string) wp_unslash($_POST['blr_finance_category'] ?? ''));
            $values['blr_finance_reference'] = sanitize_text_field((string) wp_unslash($_POST['blr_finance_reference'] ?? ''));
            $values['blr_finance_payment_method'] = sanitize_text_field((string) wp_unslash($_POST['blr_finance_payment_method'] ?? ''));
            $values['blr_finance_contact_id'] = (string) absint((string) wp_unslash($_POST['blr_finance_contact_id'] ?? 0));
            $posted_attachment_id = absint((string) wp_unslash($_POST[FinancesModule::ATTACHMENT_META_KEY] ?? 0));
            $attachment_id = FinancesModule::validate_attachment_id($posted_attachment_id);
            $values[FinancesModule::ATTACHMENT_META_KEY] = (string) $attachment_id;
            $has_uploaded_attachment = CrudHelpers::has_selected_uploads('finance_attachment_file');
            $created_uploads = [];
            $uploaded_attachment_id = 0;

            if ($values['title'] === '') {
                $errors[] = __('Title is required.', 'blr-backline');
            }

            if ($values['blr_finance_kind'] === '') {
                $errors[] = __('Type is required.', 'blr-backline');
            }

            if ($values['blr_finance_status'] === '') {
                $errors[] = __('Status is required.', 'blr-backline');
            }

            if ($values['blr_finance_amount'] === '') {
                $errors[] = __('Amount is required.', 'blr-backline');
            }

            if (!preg_match('/^[A-Z]{3}$/', $values['blr_finance_currency'])) {
                $errors[] = __('Currency must be a 3-letter code like USD.', 'blr-backline');
            }

            if ($values['blr_finance_date'] === '') {
                $errors[] = __('Date is required.', 'blr-backline');
            }

            $contact_id = (int) $values['blr_finance_contact_id'];
            if ($contact_id > 0) {
                $contact = get_post($contact_id);
                if (!$contact instanceof \WP_Post || $contact->post_type !== 'blr_contact') {
                    $errors[] = __('The selected contact is invalid.', 'blr-backline');
                }
            }

            if ($posted_attachment_id > 0 && $attachment_id <= 0) {
                $errors[] = __('The selected attachment must be an image or PDF from the WordPress media library.', 'blr-backline');
            }

            if ($has_uploaded_attachment) {
                $upload_validation_errors = CrudHelpers::validate_upload_selection(
                    'finance_attachment_file',
                    FinancesModule::attachment_upload_mimes(),
                    CrudHelpers::visual_max_file_size(),
                    1
                );

                foreach ($upload_validation_errors as $upload_validation_error) {
                    $errors[] = (string) $upload_validation_error;
                }
            }

            if (!$errors && $has_uploaded_attachment) {
                $created_uploads = CrudHelpers::handle_multi_upload(
                    'finance_attachment_file',
                    0,
                    FinancesModule::attachment_upload_mimes(),
                    CrudHelpers::visual_max_file_size(),
                    1
                );

                foreach (CrudHelpers::get_last_upload_errors() as $upload_error) {
                    $errors[] = (string) $upload_error;
                }

                $uploaded_attachment_id = !empty($created_uploads[0]['attachment_id']) ? FinancesModule::validate_attachment_id((int) $created_uploads[0]['attachment_id']) : 0;
                if (!$errors && $uploaded_attachment_id <= 0) {
                    $errors[] = __('The uploaded attachment must be an image or PDF.', 'blr-backline');
                }

                if (!$errors && $uploaded_attachment_id > 0) {
                    $attachment_id = $uploaded_attachment_id;
                    $values[FinancesModule::ATTACHMENT_META_KEY] = (string) $attachment_id;
                }

                if ($errors && $created_uploads) {
                    CrudHelpers::cleanup_upload_results($created_uploads);
                    $created_uploads = [];
                }
            }

            if (!$errors) {
                $data = [
                    'post_title' => $values['title'],
                    'post_content' => $values['content'],
                    'post_status' => 'publish',
                    'post_type' => FinancesModule::POST_TYPE,
                ];

                if ($is_edit) {
                    $data['ID'] = $post_id;
                    $result = wp_update_post(wp_slash($data), true);
                } else {
                    $result = wp_insert_post(wp_slash($data), true);
                    if (!is_wp_error($result)) {
                        $post_id = (int) $result;
                    }
                }

                if (is_wp_error($result)) {
                    if ($created_uploads) {
                        CrudHelpers::cleanup_upload_results($created_uploads);
                    }

                    $errors[] = $result->get_error_message();
                } else {
                    if ($attachment_id > 0) {
                        wp_update_post(wp_slash([
                            'ID' => $attachment_id,
                            'post_parent' => $post_id,
                        ]));
                    }

                    if ($uploaded_attachment_id > 0) {
                        FinancesModule::mark_uploaded_attachment_owned($post_id, $uploaded_attachment_id);
                    }

                    update_post_meta($post_id, 'blr_finance_kind', $values['blr_finance_kind']);
                    update_post_meta($post_id, 'blr_finance_status', $values['blr_finance_status']);
                    update_post_meta($post_id, 'blr_finance_amount', $values['blr_finance_amount']);
                    update_post_meta($post_id, 'blr_finance_currency', $values['blr_finance_currency']);
                    update_post_meta($post_id, 'blr_finance_date', $values['blr_finance_date']);
                    update_post_meta($post_id, 'blr_finance_due_date', $values['blr_finance_due_date']);
                    update_post_meta($post_id, 'blr_finance_category', $values['blr_finance_category']);
                    update_post_meta($post_id, 'blr_finance_reference', $values['blr_finance_reference']);
                    update_post_meta($post_id, 'blr_finance_payment_method', $values['blr_finance_payment_method']);
                    update_post_meta($post_id, 'blr_finance_contact_id', (int) $values['blr_finance_contact_id']);
                    update_post_meta($post_id, FinancesModule::ATTACHMENT_META_KEY, $attachment_id);

                    FinancesModule::cleanup_replaced_owned_attachment($post_id, $previous_attachment_id, $attachment_id);

                    CrudHelpers::redirect(self::base_url($ctx) . 'finances/edit/' . $post_id . '/?saved=1');
                }
            }
        }

        $ctx['errors'] = array_values(array_unique(array_filter(array_map('strval', $errors), 'strlen')));
        $ctx['finance_values'] = $values;
        return $ctx;
    }

    private static function handle_login(array $ctx): array {
        $base = self::base_url($ctx);
        $redirect = sanitize_text_field((string) wp_unslash($_GET['redirect'] ?? 'dashboard'));
        $redirect = trim(sanitize_text_field(wp_unslash($redirect)));
        $redirect = ltrim($redirect, '/');
        $redirect = preg_replace('~[^a-zA-Z0-9/_-]~', '', $redirect);
        if ($redirect === '' || strpos($redirect, '..') !== false) {
            $redirect = 'dashboard';
        }

        $remote_ip = self::request_ip_address();
        $rate_window = 15 * MINUTE_IN_SECONDS;
        $max_attempts = 5;
        $rate_key = static function (string $login) use ($remote_ip): string {
            $bucket = strtolower(trim($login));
            if ($bucket === '') {
                $bucket = 'unknown';
            }
            return 'blr_ba_login_' . md5($remote_ip . '|' . $bucket);
        };
        $read_rate = static function (string $login) use ($rate_key): array {
            $state = get_transient($rate_key($login));
            if (!is_array($state)) {
                return ['count' => 0, 'locked_until' => 0];
            }
            return [
                'count' => (int) ($state['count'] ?? 0),
                'locked_until' => (int) ($state['locked_until'] ?? 0),
            ];
        };
        $record_failure = static function (string $login) use ($rate_key, $rate_window, $max_attempts): void {
            $state = get_transient($rate_key($login));
            if (!is_array($state)) {
                $state = ['count' => 0, 'locked_until' => 0];
            }
            $state['count'] = (int) ($state['count'] ?? 0) + 1;
            if ($state['count'] >= $max_attempts) {
                $state['locked_until'] = time() + $rate_window;
            }
            set_transient($rate_key($login), $state, $rate_window);
        };
        $clear_failures = static function (string $login) use ($rate_key): void {
            delete_transient($rate_key($login));
        };

        $err = '';
        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_login')) {
            $err = __('Security check failed.', 'blr-backline');
        } else {
            $login = sanitize_text_field((string) wp_unslash($_POST['log'] ?? ''));
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Password must remain raw for authentication.
            $pass = isset($_POST['pwd']) ? (string) wp_unslash($_POST['pwd']) : '';
            $remember = !empty($_POST['rememberme']);

            $state = $read_rate($login);
            if ((int) $state['locked_until'] > time()) {
                $wait_minutes = max(1, (int) ceil(((int) $state['locked_until'] - time()) / MINUTE_IN_SECONDS));
                /* translators: %d: number of minutes to wait before trying again. */
                $err = sprintf(__('Too many login attempts. Try again in about %d minute(s).', 'blr-backline'), $wait_minutes);
            } else {
                $user = wp_signon([
                    'user_login' => $login,
                    'user_password' => $pass,
                    'remember' => $remember,
                ], is_ssl());

                if (is_wp_error($user)) {
                    $record_failure($login);
                    $err = __('Login failed. Check your credentials and try again.', 'blr-backline');
                } else {
                    $clear_failures($login);
                    $uid = (int) $user->ID;
                    if (!Access::user_can_access($uid)) {
                        wp_logout();
                        wp_safe_redirect(Router::not_allowed_url());
                        exit;
                    }
                    wp_safe_redirect($base . trailingslashit($redirect));
                    exit;
                }
            }
        }

        $ctx['login_error'] = $err;
        return $ctx;
    }

    private static function handle_outreach_form(array $ctx): array {
        $base = self::base_url($ctx);
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $campaign = $is_edit ? get_post($post_id) : null;
        $existing_filters = $campaign ? OutreachModule::filters_from_json((string) get_post_meta($campaign->ID, 'blr_outreach_filter_payload', true)) : OutreachModule::default_filters();
        // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately by OutreachModule::sanitize_id_list().
        $seed_contact_ids = isset($_GET['contacts']) ? OutreachModule::sanitize_id_list(wp_unslash($_GET['contacts'])) : [];
        $existing_manual_ids = $campaign ? OutreachModule::ids_from_csv((string) get_post_meta($campaign->ID, 'blr_outreach_manual_contact_ids', true)) : $seed_contact_ids;
        $existing_excluded_ids = $campaign ? OutreachModule::ids_from_csv((string) get_post_meta($campaign->ID, 'blr_outreach_excluded_contact_ids', true)) : [];

        $values = [
            'internal_title' => $campaign ? (string) $campaign->post_title : '',
            'subject' => $campaign ? (string) get_post_meta($campaign->ID, 'blr_outreach_subject', true) : '',
            'body' => $campaign ? (string) $campaign->post_content : '',
            'from_name' => $campaign ? (string) get_post_meta($campaign->ID, 'blr_outreach_from_name', true) : OutreachModule::default_from_name(),
            'from_email' => $campaign ? (string) get_post_meta($campaign->ID, 'blr_outreach_from_email', true) : OutreachModule::default_from_email(),
            'reply_to' => $campaign ? (string) get_post_meta($campaign->ID, 'blr_outreach_reply_to', true) : OutreachModule::default_reply_to(),
            'manual_contact_ids' => $existing_manual_ids,
            'excluded_contact_ids' => $existing_excluded_ids,
            'filters' => $existing_filters,
        ];
        $errors = [];
        $upload_errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_outreach')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_outreach')) {
            $errors[] = __('You do not have permission to edit this campaign.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_outreach')) {
            $errors[] = __('You do not have permission to create outreach campaigns.', 'blr-backline');
        } else {
            $values['internal_title'] = sanitize_text_field(wp_unslash($_POST['internal_title'] ?? ''));
            $values['subject'] = sanitize_text_field(wp_unslash($_POST['subject'] ?? ''));
            $values['body'] = sanitize_textarea_field(wp_unslash($_POST['body'] ?? ''));
            $values['from_name'] = sanitize_text_field(wp_unslash($_POST['from_name'] ?? ''));
            $values['from_email'] = sanitize_email(wp_unslash($_POST['from_email'] ?? ''));
            $values['reply_to'] = sanitize_email(wp_unslash($_POST['reply_to'] ?? ''));
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately by OutreachModule::sanitize_id_list().
            $values['manual_contact_ids'] = OutreachModule::sanitize_id_list(wp_unslash($_POST['manual_contact_ids'] ?? []));
            // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized immediately by OutreachModule::sanitize_id_list().
            $values['excluded_contact_ids'] = OutreachModule::sanitize_id_list(wp_unslash($_POST['excluded_contact_ids'] ?? []));
            $values['filters'] = OutreachModule::filters_from_request(wp_unslash($_POST));

            $send_now = !empty($_POST['send_now']);
            if ($values['subject'] === '') {
                $errors[] = __('Email subject is required.', 'blr-backline');
            }
            if ($send_now && trim($values['body']) === '') {
                $errors[] = __('Email body is required before sending.', 'blr-backline');
            }

            $recipients = [];
            if ($send_now && !$errors) {
                $recipients = OutreachModule::resolve_contacts($values['manual_contact_ids'], $values['filters'], $values['excluded_contact_ids']);
                if (!$recipients) {
                    $errors[] = __('No valid recipients matched your selection. Contacts need a valid email address to send.', 'blr-backline');
                }
            }

            $campaign_title = $values['internal_title'] !== '' ? $values['internal_title'] : ($values['subject'] !== '' ? $values['subject'] : __('Untitled Outreach', 'blr-backline'));
            $post_status = $send_now ? 'publish' : (($campaign && $campaign->post_status === 'publish') ? 'publish' : 'draft');

            if (!$errors) {
                $postarr = [
                    'post_type' => 'blr_outreach',
                    'post_status' => $post_status,
                    'post_title' => $campaign_title,
                    'post_content' => $values['body'],
                ];

                $created_post = false;
                if ($is_edit) {
                    $postarr['ID'] = $post_id;
                }


                if (!$errors) {
                    $res = $is_edit ? wp_update_post(wp_slash($postarr), true) : wp_insert_post(wp_slash($postarr), true);
                    if (!is_wp_error($res) && !$is_edit) {
                        $created_post = true;
                    }

                    if (is_wp_error($res)) {
                        $errors[] = $res->get_error_message();
                    } else {
                        $post_id = (int) $res;

                        update_post_meta($post_id, 'blr_outreach_subject', $values['subject']);
                        update_post_meta($post_id, 'blr_outreach_from_name', $values['from_name']);
                        update_post_meta($post_id, 'blr_outreach_from_email', $values['from_email']);
                        update_post_meta($post_id, 'blr_outreach_reply_to', $values['reply_to']);
                        update_post_meta($post_id, 'blr_outreach_status', $send_now ? 'Queued' : 'Draft');
                        OutreachModule::remove_deprecated_campaign_attachment_data($post_id);
                        update_post_meta($post_id, 'blr_outreach_manual_contact_ids', OutreachModule::csv_from_ids($values['manual_contact_ids']));
                        update_post_meta($post_id, 'blr_outreach_excluded_contact_ids', OutreachModule::csv_from_ids($values['excluded_contact_ids']));
                        update_post_meta($post_id, 'blr_outreach_filter_payload', OutreachModule::filters_to_json($values['filters']));


                        if ($errors && $created_post) {
                            wp_delete_post($post_id, true);
                        }

                        if (!$errors) {
                            if ($send_now) {
                                $report = [];
                                OutreachModule::send_campaign_now($post_id, $recipients, $report);
                                $qs = http_build_query([
                                    'queued' => 1,
                                    'total' => (int) ($report['total'] ?? 0),
                                    'queued_count' => (int) ($report['queued'] ?? 0),
                                    'skipped_count' => (int) ($report['skipped'] ?? 0),
                                    'invalid_count' => (int) ($report['invalid'] ?? 0),
                                    'suppressed_count' => (int) ($report['suppressed'] ?? 0),
                                    'limited_count' => (int) ($report['limited'] ?? 0),
                                ]);
                                CrudHelpers::redirect($base . 'outreach/view/' . $post_id . '/?' . $qs);
                            }

                            CrudHelpers::redirect($base . 'outreach/edit/' . $post_id . '/?saved=1');
                        }
                    }
                }
            }
        }

        $ctx['errors'] = array_values(array_unique(array_filter(array_map('strval', $errors), 'strlen')));
        $ctx['upload_errors'] = $upload_errors;
        $ctx['outreach_values'] = $values;
        return $ctx;
    }


    private static function handle_outreach_view(array $ctx): array {
        $base = self::base_url($ctx);
        $id = (int) ($ctx['id'] ?? 0);

        if (!empty($_POST['update_campaign_status'])) {
            $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
            if ($nonce && wp_verify_nonce($nonce, 'blr_ba_update_campaign_status_' . $id) && Access::can_edit_post($id, 'blr_outreach')) {
                $new_status = sanitize_text_field(wp_unslash($_POST['campaign_status'] ?? ''));
                if ($new_status === '') {
                    delete_post_meta($id, 'blr_outreach_workflow_status');
                } elseif (OutreachModule::is_valid_campaign_workflow_status($new_status)) {
                    update_post_meta($id, 'blr_outreach_workflow_status', $new_status);
                }
                OutreachModule::refresh_campaign_rollup_now($id);
                CrudHelpers::redirect($base . 'outreach/view/' . $id . '/?updated=1');
            }
        }

        if (!empty($_POST['resume_queue'])) {
            $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
            if ($nonce && wp_verify_nonce($nonce, 'blr_ba_resume_outreach_' . $id) && Access::can_edit_post($id, 'blr_outreach')) {
                $requeued = OutreachModule::requeue_failed_logs($id);
                $resume_stats = OutreachModule::get_campaign_stats($id);
                $resume_pending = (int) ($resume_stats['queued'] ?? 0) + (int) ($resume_stats['sending'] ?? 0);
                if ($resume_pending > 0 || $requeued > 0) {
                    OutreachModule::schedule_campaign_processing($id, 1);
                }
                CrudHelpers::redirect($base . 'outreach/view/' . $id . '/?queued=1&resumed=1&queued_count=' . $resume_pending);
            }
        }


        if (!empty($_POST['run_queue_now'])) {
            $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
            if ($nonce && wp_verify_nonce($nonce, 'blr_ba_run_outreach_queue_' . $id) && Access::can_edit_post($id, 'blr_outreach')) {
                $run_report = OutreachModule::manual_run_queue($id);
                CrudHelpers::redirect(add_query_arg([
                    'queue_run' => 1,
                    'processed_count' => (int) ($run_report['processed'] ?? 0),
                    'sent_count' => (int) ($run_report['sent'] ?? 0),
                    'failed_count' => (int) ($run_report['failed'] ?? 0),
                    'remaining_count' => (int) ($run_report['remaining'] ?? 0),
                    'queue_locked' => !empty($run_report['locked']) ? 1 : 0,
                ], $base . 'outreach/view/' . $id . '/'));
            }
        }

        if (!empty($_POST['update_log_status'])) {
            $log_id = absint(wp_unslash($_POST['log_id'] ?? 0));
            $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
            if ($log_id > 0 && $nonce && wp_verify_nonce($nonce, 'blr_ba_update_outreach_log_' . $log_id) && Access::can_edit_post($log_id, 'blr_outreach_log')) {
                $log = get_post($log_id);
                if ($log && $log->post_type === 'blr_outreach_log' && (int) $log->post_parent === $id) {
                    $status = sanitize_text_field(wp_unslash($_POST['log_status'] ?? ''));
                    $note = wp_kses_post(wp_unslash($_POST['log_note'] ?? ''));
                    if (in_array($status, OutreachModule::recipient_statuses(), true)) {
                        $contact_id = OutreachModule::validated_contact_id((int) get_post_meta($log_id, 'blr_outreach_log_contact_id', true));
                        $email = OutreachModule::sanitize_log_email((string) get_post_meta($log_id, 'blr_outreach_log_email', true));

                        if ($contact_id > 0 && $email !== '' && OutreachModule::can_current_user_edit_contact($contact_id)) {
                            $log_update = [
                                'status' => $status,
                                'note' => $note,
                            ];

                            if (OutreachModule::is_retryable_recipient_status($status)) {
                                $log_update['sent_at'] = '';
                            }

                            OutreachModule::update_log($log_id, $contact_id, $email, $log_update);
                            OutreachModule::sync_contact_status($contact_id, $status, false, true);
                            OutreachModule::refresh_campaign_rollup_now($id);
                            CrudHelpers::redirect($base . 'outreach/view/' . $id . '/?updated=1');
                        }
                    }
                }
                CrudHelpers::redirect($base . 'outreach/view/' . $id . '/?log_update_error=1');
            }
        }

        return $ctx;
    }
    private static function handle_task_form(array $ctx): array {
        $is_edit = ($ctx['action'] ?? '') === 'edit';
        $post_id = $is_edit ? (int) ($ctx['id'] ?? 0) : 0;
        $errors = [];

        $nonce = isset($_POST['_wpnonce']) ? sanitize_text_field((string) wp_unslash($_POST['_wpnonce'])) : '';
        if ($nonce === '' || !wp_verify_nonce($nonce, 'blr_ba_save_task')) {
            $errors[] = __('Security check failed.', 'blr-backline');
        } elseif ($is_edit && !Access::can_edit_post($post_id, 'blr_task')) {
            $errors[] = __('You do not have permission to edit this task.', 'blr-backline');
        } elseif (!$is_edit && !Access::can_create_post_type('blr_task')) {
            $errors[] = __('You do not have permission to create tasks.', 'blr-backline');
        } else {
            $title = sanitize_text_field(wp_unslash($_POST['title'] ?? ''));
            $content = wp_kses_post(wp_unslash($_POST['content'] ?? ''));
            if ($title === '') {
                $errors[] = __('Task title is required.', 'blr-backline');
            }

            $status = sanitize_text_field(wp_unslash($_POST['blr_task_status'] ?? 'Backlog'));
            if (!in_array($status, TasksModule::statuses(), true)) {
                $errors[] = __('The selected task status is invalid.', 'blr-backline');
            }

            $priority = sanitize_text_field(wp_unslash($_POST['blr_task_priority'] ?? 'Medium'));
            if (!in_array($priority, TasksModule::priorities(), true)) {
                $errors[] = __('The selected task priority is invalid.', 'blr-backline');
            }

            $assignee_user_id = absint(wp_unslash($_POST['blr_task_assignee_user_id'] ?? 0));
            if ($assignee_user_id > 0 && !get_userdata($assignee_user_id)) {
                $errors[] = __('The selected assignee is invalid.', 'blr-backline');
            }

            $related_post_id = absint(wp_unslash($_POST['blr_task_related_post_id'] ?? 0));
            $related_post = $related_post_id > 0 ? get_post($related_post_id) : null;
            if ($related_post_id > 0 && !$related_post instanceof \WP_Post) {
                $errors[] = __('The selected related record is invalid.', 'blr-backline');
            }

            $task_start_date_raw = isset($_POST['blr_task_start_date']) ? sanitize_text_field((string) wp_unslash($_POST['blr_task_start_date'])) : '';
            $task_due_date_raw = isset($_POST['blr_task_due_date']) ? sanitize_text_field((string) wp_unslash($_POST['blr_task_due_date'])) : '';

            $task_start_date = self::validated_date_input($task_start_date_raw, __('Task start date must be a valid date in YYYY-MM-DD format.', 'blr-backline'));
            $task_due_date = self::validated_date_input($task_due_date_raw, __('Task due date must be a valid date in YYYY-MM-DD format.', 'blr-backline'));
            if ($task_start_date['error'] !== '') {
                $errors[] = $task_start_date['error'];
            }
            if ($task_due_date['error'] !== '') {
                $errors[] = $task_due_date['error'];
            }
            if ($task_start_date['value'] !== '' && $task_due_date['value'] !== '' && $task_due_date['value'] < $task_start_date['value']) {
                $errors[] = __('Task due date cannot be earlier than the task start date.', 'blr-backline');
            }

            if (!$errors) {
                $data = [
                    'post_title' => $title,
                    'post_content' => $content,
                    'post_status' => 'publish',
                    'post_type' => 'blr_task',
                ];

                if ($is_edit) {
                    $data['ID'] = $post_id;
                    $res = wp_update_post(wp_slash($data), true);
                } else {
                    $res = wp_insert_post(wp_slash($data), true);
                    if (!is_wp_error($res)) {
                        $post_id = (int) $res;
                    }
                }

                if (is_wp_error($res)) {
                    $errors[] = $res->get_error_message();
                } else {
                    update_post_meta($post_id, 'blr_task_status', $status);
                    update_post_meta($post_id, 'blr_task_priority', $priority);
                    update_post_meta($post_id, 'blr_task_start_date', $task_start_date['value']);
                    update_post_meta($post_id, 'blr_task_due_date', $task_due_date['value']);
                    update_post_meta($post_id, 'blr_task_assignee_user_id', $assignee_user_id);
                    update_post_meta($post_id, 'blr_task_related_post_id', $related_post ? (int) $related_post->ID : 0);
                    update_post_meta($post_id, 'blr_task_related_post_type', $related_post ? (string) $related_post->post_type : '');
                    update_post_meta($post_id, 'blr_task_checklist', wp_kses_post(wp_unslash($_POST['blr_task_checklist'] ?? '')));

                    if ($status === 'Done') {
                        update_post_meta($post_id, 'blr_task_completed_at', current_time('mysql'));
                    } elseif ((string) get_post_meta($post_id, 'blr_task_completed_at', true) !== '') {
                        delete_post_meta($post_id, 'blr_task_completed_at');
                    }

                    CrudHelpers::redirect(self::base_url($ctx) . 'tasks/edit/' . $post_id . '/?saved=1');
                }
            }
        }

        $ctx['errors'] = $errors;
        return $ctx;
    }
    private static function capture_post_state(int $post_id, array $meta_keys, array $taxonomies = []): array {
        $post = get_post($post_id);
        if (!$post instanceof \WP_Post) {
            return [];
        }

        $state = [
            'post' => [
                'post_title' => (string) $post->post_title,
                'post_content' => (string) $post->post_content,
                'post_excerpt' => (string) $post->post_excerpt,
                'post_status' => (string) $post->post_status,
            ],
            'meta' => [],
            'terms' => [],
            'thumbnail_id' => (int) get_post_thumbnail_id($post_id),
        ];

        foreach (array_values(array_unique(array_filter(array_map('strval', $meta_keys), 'strlen'))) as $meta_key) {
            $state['meta'][$meta_key] = get_post_meta($post_id, $meta_key, false);
        }

        foreach (array_values(array_unique(array_filter(array_map('strval', $taxonomies), 'strlen'))) as $taxonomy) {
            if (!taxonomy_exists($taxonomy)) {
                continue;
            }

            $term_ids = wp_get_object_terms($post_id, $taxonomy, ['fields' => 'ids']);
            if (is_wp_error($term_ids)) {
                continue;
            }

            $state['terms'][$taxonomy] = array_values(array_unique(array_map('absint', $term_ids)));
        }

        return $state;
    }

    private static function restore_post_state(int $post_id, array $state): void {
        if ($post_id <= 0 || empty($state['post']) || !is_array($state['post'])) {
            return;
        }

        wp_update_post(wp_slash([
            'ID' => $post_id,
            'post_title' => (string) ($state['post']['post_title'] ?? ''),
            'post_content' => (string) ($state['post']['post_content'] ?? ''),
            'post_excerpt' => (string) ($state['post']['post_excerpt'] ?? ''),
            'post_status' => sanitize_key((string) ($state['post']['post_status'] ?? 'publish')),
        ]));

        $meta_state = isset($state['meta']) && is_array($state['meta']) ? $state['meta'] : [];
        foreach ($meta_state as $meta_key => $values) {
            $meta_key = (string) $meta_key;
            if ($meta_key === '') {
                continue;
            }

            delete_post_meta($post_id, $meta_key);
            if (!is_array($values)) {
                continue;
            }

            foreach ($values as $value) {
                add_post_meta($post_id, $meta_key, $value);
            }
        }

        $term_state = isset($state['terms']) && is_array($state['terms']) ? $state['terms'] : [];
        foreach ($term_state as $taxonomy => $term_ids) {
            $taxonomy = (string) $taxonomy;
            if ($taxonomy === '' || !taxonomy_exists($taxonomy)) {
                continue;
            }

            $clean_term_ids = is_array($term_ids) ? array_values(array_unique(array_map('absint', $term_ids))) : [];
            wp_set_object_terms($post_id, $clean_term_ids, $taxonomy, false);
        }

        $thumbnail_id = isset($state['thumbnail_id']) ? absint($state['thumbnail_id']) : 0;
        if ($thumbnail_id > 0) {
            set_post_thumbnail($post_id, $thumbnail_id);
        } else {
            delete_post_thumbnail($post_id);
        }
    }

    private static function cleanup_created_tracks(array $track_ids, array $attachment_ids = []): void {
        foreach (array_values(array_unique(array_map('absint', $attachment_ids))) as $attachment_id) {
            if ($attachment_id > 0) {
                CrudHelpers::delete_attachment($attachment_id);
            }
        }

        foreach (array_values(array_unique(array_map('absint', $track_ids))) as $track_id) {
            if ($track_id > 0) {
                wp_delete_post($track_id, true);
            }
        }
    }

    private static function validated_datetime_input($value, string $error_message): array {
        $raw_value = trim((string) $value);
        $value = Sanitizer::datetime_ymd_hi($value);
        if ($value === '' && $raw_value !== '') {
            return ['value' => '', 'error' => $error_message];
        }

        return ['value' => $value, 'error' => ''];
    }

    private static function validated_date_input($value, string $error_message): array {
        $raw_value = trim((string) $value);
        $value = Sanitizer::date_ymd($value);
        if ($value === '' && $raw_value !== '') {
            return ['value' => '', 'error' => $error_message];
        }

        return ['value' => $value, 'error' => ''];
    }

    private static function validated_time_input($value, string $error_message): array {
        $raw_value = trim((string) $value);
        $value = Sanitizer::time_hi($value);
        if ($value === '' && $raw_value !== '') {
            return ['value' => '', 'error' => $error_message];
        }

        return ['value' => $value, 'error' => ''];
    }

    private static function normalized_isrc($value): array {
        $value = strtoupper(sanitize_text_field((string) $value));
        $value = preg_replace('/[^A-Z0-9]/', '', $value);
        $value = is_string($value) ? $value : '';

        if ($value === '') {
            return ['value' => '', 'error' => ''];
        }

        if (!preg_match('/^[A-Z]{2}[A-Z0-9]{3}[0-9]{7}$/', $value)) {
            return ['value' => '', 'error' => __('ISRC must be 12 characters in a valid format, like USRC17607839.', 'blr-backline')];
        }

        return ['value' => $value, 'error' => ''];
    }

    private static function normalized_upc($value): array {
        $value = preg_replace('/\D+/', '', (string) $value);
        $value = is_string($value) ? $value : '';

        if ($value === '') {
            return ['value' => '', 'error' => ''];
        }

        if (!preg_match('/^[0-9]{12,13}$/', $value)) {
            return ['value' => '', 'error' => __('UPC/EAN must be 12 or 13 digits.', 'blr-backline')];
        }

        return ['value' => $value, 'error' => ''];
    }

    private static function validated_term_id(int $term_id, string $taxonomy): int {
        $term_id = absint($term_id);
        if ($term_id <= 0 || !taxonomy_exists($taxonomy)) {
            return 0;
        }

        $term = get_term($term_id, $taxonomy);
        if (!$term || is_wp_error($term) || $term->taxonomy !== $taxonomy) {
            return 0;
        }

        return (int) $term->term_id;
    }

    private static function validated_post_type_id(int $post_id, string $post_type): int {
        $post_id = absint($post_id);
        if ($post_id <= 0) {
            return 0;
        }

        $post = get_post($post_id);
        if (!$post instanceof \WP_Post || $post->post_type !== $post_type) {
            return 0;
        }

        return (int) $post->ID;
    }

    private static function validated_post_type_ids(array $ids, string $post_type): array {
        $valid_ids = [];
        foreach ($ids as $id) {
            $validated = self::validated_post_type_id((int) $id, $post_type);
            if ($validated > 0) {
                $valid_ids[] = $validated;
            }
        }

        return array_values(array_unique(array_map('intval', $valid_ids)));
    }

    private static function base_url(array $ctx): string {
        return rtrim((string) ($ctx['base_url'] ?? Router::base_url()), '/') . '/';
    }
}
