<?php

namespace FluentCrm\App\Http\Controllers;

use FluentCrm\App\Hooks\Handlers\PurchaseHistory;
use FluentCrm\App\Models\CampaignEmail;
use FluentCrm\App\Models\CampaignUrlMetric;
use FluentCrm\App\Models\Company;
use FluentCrm\App\Models\CustomEmailCampaign;
use FluentCrm\App\Models\EventTracker;
use FluentCrm\App\Models\Funnel;
use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\SubscriberMeta;
use FluentCrm\App\Models\SubscriberNote;
use FluentCrm\App\Models\SubscriberPivot;
use FluentCrm\App\Services\AutoSubscribe;
use FluentCrm\App\Services\ContactsQuery;
use FluentCrm\App\Services\Funnel\FunnelProcessor;
use FluentCrm\App\Services\Helper;
use FluentCrm\App\Services\Sanitize;
use FluentCrm\Framework\Support\Arr;
use FluentCrm\Framework\Support\Collection;
use FluentCrm\Framework\Http\Request\Request;

/**
 *  SubscriberController - REST API Handler Class
 *
 *  REST API Handler
 *
 * @package FluentCrm\App\Http
 *
 * @version 1.0.0
 */
class SubscriberController extends Controller
{
    public function index()
    {
        $filterType = $this->request->get('filter_type', 'simple');

        $with = ['tags', 'lists'];

        if (Helper::isCompanyEnabled()) {
            $with[] = 'company';
            $with[] = 'companies';
        }

        if ($filterType == 'advanced') {
            $queryArgs = [
                'with'               => $with,
                'filter_type'        => 'advanced',
                'filters_groups_raw' => Helper::parseArrayOrJson($this->request->get('advanced_filters')),
                'search'             => trim(sanitize_text_field($this->request->get('search', ''))),
                'sort_by'            => sanitize_sql_orderby($this->request->get('sort_by', 'id')),
                'sort_type'          => sanitize_sql_orderby($this->request->get('sort_type', 'DESC')),
                'has_commerce'       => $this->request->get('has_commerce'),
                'custom_fields'      => $this->request->get('custom_fields') == 'true',
                'company_ids'        => $this->request->get('company_ids', []),
            ];
        } else {
            $queryArgs = [
                'with'          => $with,
                'filter_type'   => 'simple',
                'search'        => trim(sanitize_text_field($this->request->get('search', ''))),
                'sort_by'       => sanitize_sql_orderby($this->request->get('sort_by', 'id')),
                'sort_type'     => sanitize_sql_orderby($this->request->get('sort_type', 'DESC')),
                'has_commerce'  => $this->request->get('has_commerce'),
                'custom_fields' => $this->request->get('custom_fields') == 'true',
                'tags'          => $this->request->get('tags', []),
                'statuses'      => $this->request->get('statuses', []),
                'sms_statuses'  => $this->request->get('sms_statuses', []),
                'lists'         => $this->request->get('lists', []),
                'company_ids'   => $this->request->get('company_ids', []),
            ];
        }

        $subscribers = (new ContactsQuery($queryArgs))->paginate();

        return $this->sendSuccess([
            'subscribers' => $subscribers,
            'custom'      => $this->request->get('custom_fields')
        ]);
    }

    /**
     * Find a subscriber by id
     *
     * @return \WP_REST_Response $object
     */
    public function show()
    {
        $with = $this->request->get('with', []);

        $contactId = $this->request->get('id');

        $defaultWith = ['tags', 'lists'];

        if (Helper::isCompanyEnabled()) {
            $defaultWith[] = 'companies';
        }

        $subscriber = false;
        if ($contactId) {
            $subscriber = Subscriber::with($defaultWith)->find($contactId);
        } else if ($byEmail = $this->request->get('get_by_email')) {
            $subscriber = Subscriber::with($defaultWith)->where('email', sanitize_email($byEmail))->first();
        }

        if (!$subscriber) {
            return $this->sendError([
                'message' => __('Subscriber not found', 'fluent-crm')
            ]);
        }

        if (in_array('commerce_stat', $with)) {
            $subscriber->commerce_stat = [];
            /**
             * Determine the commerce provider for FluentCRM.
             *
             * This filter allows you to modify the commerce provider used in FluentCRM.
             *
             * @param string The current commerce provider.
             * @since 2.5.1
             *
             */
            $commerceProvider = apply_filters('fluentcrm_commerce_provider', '');
            if ($commerceProvider) {
                /**
                 * Determine the purchase statistics for a specific subscriber and commerce provider.
                 *
                 * This filter allows modification of the purchase statistics for a given subscriber
                 * based on the specified commerce provider.
                 *
                 * @param array  The current purchase statistics for the subscriber.
                 * @param int $subscriber_id The ID of the subscriber.
                 *
                 * @return array Modified purchase statistics for the subscriber.
                 * @since 2.7.0
                 *
                 */
                $subscriber->commerce_stat = apply_filters('fluent_crm/contact_purchase_stat_' . $commerceProvider, [], $subscriber->id);
            }
        }

        if ($wpUser = $subscriber->getWpUser()) {
            $subscriber->user_edit_url = get_edit_user_link($wpUser->ID);
            $subscriber->user_roles = array_values($wpUser->roles);
        }

        if (in_array('stats', $with)) {
            $subscriber->stats = $subscriber->stats();
        }

        if (in_array('subscriber.custom_values', $with)) {
            $subscriber->custom_values = (object)$subscriber->custom_fields();
        }

        if ($subscriber->date_of_birth == '0000-00-00' || empty($subscriber->date_of_birth)) {
            $subscriber->date_of_birth = '';
        }

        if ($subscriber->status == 'unsubscribed') {
            $subscriber->unsubscribe_reason = $subscriber->unsubscribeReason();
            $subscriber->unsubscribe_date = $subscriber->unsubscribeReasonDate();
        } else if ($subscriber->status == 'bounced' || $subscriber->status == 'complained') {
            $subscriber->unsubscribe_reason = $subscriber->unsubscribeReason('reason');
            $subscriber->unsubscribe_date = $subscriber->unsubscribeReasonDate('reason');
        }

        $data = [
            'subscriber' => $subscriber
        ];

        if (in_array('custom_fields', $with)) {
            $data['custom_fields'] = fluentcrm_get_option('contact_custom_fields', []);
        }


        return $this->sendSuccess($data);
    }

    public function updateProperty()
    {
        $column = $this->request->getSafe('property', 'sanitize_text_field');
        $value = $this->request->getSafe('value', 'sanitize_text_field');
        
        $subscriberIds = $this->request->get('subscribers');
        
        if (!is_array($subscriberIds)) {
            $subscriberIds = [$subscriberIds]; // say, this is single value, convert to array
        }

        $subscriberIds = array_map('intval', $subscriberIds);
        $subscriberIds = array_unique(array_filter($subscriberIds));

        // $validColumns = ['status', 'contact_type', 'avatar', 'company_id', 'sms_status', 'whatsapp_status'];
        $validColumns = ['status', 'contact_type', 'avatar', 'company_id', 'sms_status'];
        $subscriberStatuses = fluentcrm_subscriber_statuses();
        $leadStatuses = fluentcrm_contact_types();
        $smsStatuses = apply_filters('fluentcrm_sms_statuses', [
            'sms_subscribed',
            'sms_unsubscribed',
            'sms_pending',
            'sms_bounced'
        ]);
        // Resolve WhatsApp statuses through a filter so Pro/third-party
        // extensions can extend the vocabulary, mirroring how SMS statuses work.
        // $whatsappStatuses = apply_filters('fluent_crm/whatsapp_statuses', [
        //     'whatsapp_subscribed',
        //     'whatsapp_unsubscribed'
        // ]);

        $this->validate([
            'column'         => $column,
            'subscriber_ids' => $subscriberIds
        ], [
            'column'         => 'required',
            'subscriber_ids' => 'required'
        ]);

        if (!in_array($column, $validColumns)) {
            return $this->sendError([
                'message' => __('Column is not valid', 'fluent-crm')
            ]);
        }

        if ($column == 'status' && !in_array($value, $subscriberStatuses)) {
            return $this->sendError([
                'message' => __('Value is not valid', 'fluent-crm')
            ]);
        } else if ($column == 'contact_type' && !isset($leadStatuses[$value])) {
            return $this->sendError([
                'message' => __('Value is not valid', 'fluent-crm')
            ]);
        } else if ($column == 'company_id') {
            Company::findOrFail($value); // just a check
        } else if ($column == 'sms_status' && !in_array($value, $smsStatuses)) {
            return $this->sendError([
                'message' => __('Value is not valid', 'fluent-crm')
            ]);
        }
        // } else if ($column == 'whatsapp_status' && !in_array($value, $whatsappStatuses)) {
        //     return $this->sendError([
        //         'message' => __('Value is not valid', 'fluent-crm')
        //     ]);
        // }

        $subscribers = Subscriber::whereIn('id', $subscriberIds)->get();

        foreach ($subscribers as $subscriber) {
            $oldValue = $subscriber->{$column};
            if ($oldValue != $value) {
                $subscriber->{$column} = $value;
                $subscriber->save();
                // if (in_array($column, ['status', 'contact_type', 'sms_status', 'whatsapp_status'])) {
                if (in_array($column, ['status', 'contact_type', 'sms_status'])) {
                    do_action('fluentcrm_subscriber_' . $column . '_to_' . $value, $subscriber, $oldValue);

                    if ($column == 'status') {
                        /**
                         * Contact's Status has been changed
                         *
                         * @param Subscriber $subscriber Subscriber Model.
                         * @param string $oldStatus Old Status.
                         * @since 1.0
                         *
                         */
                        do_action('fluent_crm/subscriber_status_changed', $subscriber, $oldValue, $value);
                    } else if ($column == 'sms_status') {
                        /**
                         * Contact's SMS Status has been changed
                         *
                         * @param Subscriber $subscriber Subscriber Model.
                         * @param string $oldStatus Old SMS Status.
                         * @since 3.0.0
                         *
                         */
                        do_action('fluent_crm/subscriber_sms_status_changed', $subscriber, $oldValue, $value);
                    }
                    // } else if ($column == 'whatsapp_status') {
                    //     /**
                    //      * Contact's WhatsApp Status has been changed
                    //      *
                    //      * @param Subscriber $subscriber Subscriber Model.
                    //      * @param string $oldValue Old WhatsApp Status.
                    //      * @param string $value New WhatsApp Status.
                    //      */
                    //     if ($value === 'whatsapp_subscribed') {
                    //         do_action('fluent_crm/contact_whatsapp_subscribed', $subscriber, $oldValue, $value);
                    //     } else {
                    //         do_action('fluent_crm/contact_whatsapp_unsubscribed', $subscriber, $oldValue, $value);
                    //     }
                    // }

                }
                if ($column == 'avatar') {
                    do_action('fluent_crm/subscriber_avatar_update', $subscriber, $oldValue);
                }
            }
        }

        return $this->sendSuccess([
            'message' => __('Subscribers successfully updated', 'fluent-crm')
        ]);
    }

    public function deleteSubscriber(Request $request, $id)
    {
        $subscriber = Subscriber::findOrFail($id);

        Helper::deleteContacts([$subscriber->id]);

        return $this->sendSuccess([
            'message' => __('Selected Subscriber has been deleted successfully', 'fluent-crm')
        ]);
    }

    public function deleteSubscribers(Request $request)
    {
        $subscriberIds = $request->get('subscribers');

        $this->validate(
            ['subscriber_ids' => $subscriberIds],
            ['subscriber_ids' => 'required']
        );

        Helper::deleteContacts($subscriberIds);

        return $this->sendSuccess([
            'message' => __('Selected Subscribers have been deleted', 'fluent-crm')
        ]);
    }

    /**
     * Tag a subscriber with Tags or Lists.
     */
    public function tagger()
    {
        $model = $this->resolveModel();

        $subscribers = $this->request->get('subscribers');
        $attachments = $this->attachments($model, 'attach');
        $detachments = $this->attachments($model, 'detach');

        $type = $this->request->get('type');

        foreach ($subscribers as $subscriberId) {
            $subscriber = Subscriber::find($subscriberId);

            // A selected contact may have been deleted concurrently
            if (!$subscriber) {
                continue;
            }

            if ($attachments) {
                if ($type == 'tags') {
                    $subscriber->attachTags($attachments);
                } else {
                    $subscriber->attachLists($attachments);
                }
            }

            if ($detachments) {
                if ($type == 'tags') {
                    $subscriber->detachTags($detachments);
                } else {
                    $subscriber->detachLists($detachments);
                }
            }
        }

        return $this->sendSuccess([
            'message'     => __('Successfully updated the ', 'fluent-crm') . _n('subscriber', 'subscribers', count($subscribers), 'fluent-crm') . '.',
            'subscribers' => Subscriber::with('tags', 'lists')->whereIn('id', $subscribers)->get()
        ]);
    }

    /**
     * Store a subscriber.
     *
     * @return array
     */
    public function store(Request $request)
    {
        $forceUpdate = $request->get('__force_update') == 'yes';

        if (!$forceUpdate) {
            $data = $this->validate($request->all(), [
                'email'  => 'required|email|unique:fc_subscribers',
                'status' => 'required'
            ], [
                'email.unique' => __('Provided email already assigned to another subscriber.', 'fluent-crm')
            ]);
        } else {
            $data = $this->validate($request->all(), [
                'email'  => 'required|email',
                'status' => 'required'
            ]);
        }

        unset($data['__force_update']);

        $data = Sanitize::contact($data);

        $user = get_user_by('email', $data['email']);

        if ($user) {
            $data['user_id'] = $user->ID;
        } else {
            $data['user_id'] = '';
        }

        if ($this->isNew()) {
            $data['created_at'] = current_time('mysql');

            $contact = FluentCrmApi('contacts')->createOrUpdate($data, false, false);

            /**
             * new Contact has been created
             *
             * @param Subscriber $contact Subscriber Model.
             * @param array $data Original raw subscriber.
             * @since 3.30.2
             */
//            do_action('fluentcrm_contact_created', $contact); // @deprecated since 2.8.0. Use fluent_crm/contact_created instead
//            do_action('fluent_crm/contact_created', $contact);
            // no need these action hooks because those are already in the updateOrCreate method in Subscriber model

            $double_optin = filter_var($request->get('double_optin'), FILTER_VALIDATE_BOOLEAN);

            if ($double_optin && $contact->status == 'pending') {
                $contact->sendDoubleOptinEmail();
            }

            return [
                'message'     => __('Successfully added the subscriber.', 'fluent-crm'),
                'contact'     => $contact,
                'action_type' => 'created'
            ];

        } else if ($forceUpdate) {
            // The admin explicitly confirmed "update the existing contact" (__force_update).
            // Force only when the existing contact is NOT currently subscribed: that lets
            // the confirmed dialog overwrite a suppressed status (explicit operator
            // decision — forced updates are honored unconditionally) without letting it
            // demote a subscribed contact to the add-form's default 'pending'.
            $existingContact = Subscriber::where('email', $data['email'])->first();
            $shouldForce = $existingContact && $existingContact->status != 'subscribed';

            $contact = FluentCrmApi('contacts')->createOrUpdate($data, $shouldForce, false);

            if ($contact && $contact->status == 'pending') {
                $contact->sendDoubleOptinEmail();
            }

            return $this->sendSuccess([
                'message'     => __('Contact has been successfully updated.', 'fluent-crm'),
                'contact'     => $contact,
                'action_type' => 'updated'
            ]);
        }

        return $this->sendError([
            'message' => __('Sorry, contact already exists', 'fluent-crm')
        ], 422);
    }

    public function bulkAddUpdate(Request $request)
    {
        $contacts = Helper::parseArrayOrJson($request->get('contacts'));
        $invalids = [];
        $created = [];
        $updated = [];

        $double_optin = filter_var($request->get('double_optin'), FILTER_VALIDATE_BOOLEAN);
        $forceUpdate = filter_var($request->get('force_update'), FILTER_VALIDATE_BOOLEAN);

        foreach ($contacts as $contact) {
            $contactData = Sanitize::contact($contact);
            // Link the contact to a WP user by email only (see updateSubscriber);
            // Subscriber::updateOrCreate() trusts a supplied user_id, so a client value
            // here would allow the same admin-linking mass-assignment. WP-user syncing
            // that legitimately needs a server-derived user_id uses UsersController.
            unset($contactData['user_id']);
            if (empty($contactData['email']) || !is_email($contactData['email'])) {
                $invalids[] = $contactData;
                continue;
            }

            $contactData['tags'] = Arr::get($contact, 'tags', []);
            $contactData['lists'] = Arr::get($contact, 'lists', []);
            $createdContact = FluentCrmApi('contacts')->createOrUpdate($contactData, $forceUpdate, false);

            if (!$createdContact) {
                $invalids[] = $contactData;
                continue;
            }

            if ($createdContact->status == 'pending' && $double_optin) {
                $createdContact->sendDoubleOptinEmail();
            }

            if ($createdContact->wasRecentlyCreated) {
                $created[] = [
                    'id'     => $createdContact->id,
                    'email'  => $createdContact->email,
                    'status' => $createdContact->status,
                ];
            } else {
                $updated[] = [
                    'id'     => $createdContact->id,
                    'email'  => $createdContact->email,
                    'status' => $createdContact->status,
                ];
            }
        }

        return [
            'message'  => __('Successfully added/updated the subscribers.', 'fluent-crm'),
            'created'  => $created,
            'updated'  => $updated,
            'invalids' => $invalids
        ];
    }

    public function updateSubscriber(Request $request, $id)
    {
        $subscriber = Subscriber::findOrFail($id);
        $originalData = Helper::parseArrayOrJson($request->get('subscriber'));

        if (!$originalData) {
            $originalData = $request->all();
        }

        $data = [];
        if (isset($originalData['email'])) {
            $data = $this->validate($originalData, [
                'email' => 'required|email|unique:fc_subscribers,email,' . $id,
            ], [
                'email.unique' => __('Provided email already assigned to another subscriber.', 'fluent-crm')
            ]);
        } else {
            $data = $originalData;
        }

        // Never trust a client-supplied user_id. It links the contact to a WordPress
        // user account and must be derived solely from the validated email below.
        // Accepting it from the request lets a contact be linked to an arbitrary WP
        // user (e.g. an administrator); chained with {{user.*}} smartcodes on a custom
        // email send, that enables administrator account takeover.
        unset($data['user_id']);

        if (isset($data['email'])) {
            // Maybe update user id
            $user = get_user_by('email', $data['email']);
            /**
             * Determine whether to update the WordPress user email on change.
             *
             * This filter allows you to control whether the WordPress user email should be updated
             * when there is a change in the FluentCRM subscriber email.
             *
             * @param bool Whether to update the WordPress user email on change. Default false.
             * @since 2.9.25
             *
             */
            if (!$user && apply_filters('fluentcrm_update_wp_user_email_on_change', false)) {
                // Resolve the currently linked WP user from the stored link, not from
                // request input, so the email-on-change update targets this contact's
                // own WP user rather than an attacker-chosen id.
                $user = get_user_by('ID', $subscriber->user_id);
            }

            $data['user_id'] = $user ? $user->ID : NULL;
        }

        if (!empty($data['user_id'])) {
            $data['user_id'] = (int)$data['user_id'];
        }

        if (isset($data['date_of_birth']) && empty($data['date_of_birth'])) {
            $data['date_of_birth'] = NULL;
        }

        $validData = Sanitize::contact($data);

        unset($validData['created_at']);
        unset($validData['last_activity']);
        $customValues = Arr::get($originalData, 'custom_values', []);

        $oldEmail = $subscriber->email;

        $oldSubscriber = clone $subscriber;

        // Route status changes through updateStatus() so the status-transition hooks fire
        // (Cleanup@handleUnsubscribe cancels queued emails/funnels for suppressed statuses,
        // FunnelHandler resumes funnels on re-subscribe). fill()+save() would flip the
        // column silently and leave automation state diverged from the contact status.
        $requestedStatus = Arr::get($validData, 'status');
        unset($validData['status']);

        if ($requestedStatus && (!in_array($requestedStatus, fluentcrm_subscriber_statuses()) || $requestedStatus == $subscriber->status)) {
            $requestedStatus = null;
        }

        $statusChangedEarly = false;

        // A SUPPRESSING status change is applied BEFORE the tag/list sync below, so
        // tag-applied automations fired within this same request already see the contact
        // as suppressed and park instead of executing. A non-suppressing change (e.g. a
        // re-subscribe) is applied AFTER the sync — see below — so funnel-resume and
        // 'Status Changed' listeners evaluate the contact's final tag/list state.
        if ($requestedStatus && in_array($requestedStatus, fluentcrm_strict_statues())) {
            $subscriber->updateStatus($requestedStatus);
            $statusChangedEarly = true;
            $requestedStatus = null;
        }

        $subscriber->fill($validData);

        $dirtyFields = $subscriber->getDirty();

        if ($dirtyFields) {
            $subscriber->save();
        }

        if ($statusChangedEarly) {
            $dirtyFields['status'] = $subscriber->status;
        }

        if ($customValues) {
            $originalCustomFields = $subscriber->custom_fields();
            $subscriber->syncCustomFieldValues($customValues, true);
            $dirtyCustomValues = $oldSubscriber->custom_fields();

            do_action('fluent_crm/contact_updated_with_changes', $subscriber, $dirtyCustomValues, $originalCustomFields, ['source' => 'web', 'type' => 'custom_fields_only']);
        }

        if ($tags = Arr::get($originalData, 'attach_tags', [])) {
            $subscriber->attachTags($tags);
        }

        if ($lists = Arr::get($originalData, 'attach_lists', [])) {
            $subscriber->attachLists($lists);
        }

        if ($detachTags = Arr::get($originalData, 'detach_tags', [])) {
            $subscriber->detachTags($detachTags);
        }

        if ($detachLists = Arr::get($originalData, 'detach_lists', [])) {
            $subscriber->detachLists($detachLists);
        }

        // Non-suppressing status transitions run AFTER the tag/list sync above so the
        // status-change hooks (funnel resume, 'Status Changed' triggers) evaluate the
        // contact's final tag/list state. (Suppressing transitions already ran before
        // the sync — see $statusChangedEarly above.)
        if ($requestedStatus && $requestedStatus != $subscriber->status) {
            $subscriber->updateStatus($requestedStatus);
            $dirtyFields['status'] = $requestedStatus;
        }

        if ($dirtyFields) {

            if (isset($dirtyFields['email'])) {
                /**
                 * Contact's Email address has been updated
                 *
                 * @param Subscriber $subscriber Subscriber Model.
                 * @param string $oldEmail Old Email Address.
                 * @since 1.0
                 *
                 */
                do_action('fluent_crm/contact_email_changed', $subscriber, $oldEmail);
            }

            do_action('fluentcrm_contact_updated', $subscriber, $dirtyFields);
            do_action('fluent_crm/contact_updated', $subscriber, $dirtyFields);

            do_action('fluent_crm/contact_updated_with_changes', $subscriber, $dirtyFields, $oldSubscriber, ['source' => 'web', 'type' => 'all_fields']);

        }

        return $this->sendSuccess([
            'message' => __('Subscriber successfully updated', 'fluent-crm'),
            'contact' => $subscriber,
            'isDirty' => !!$dirtyFields,
            'values'  => $customValues
        ], 200);
    }

    /**
     * Resolve the appropriate model e.g. Tag or, Lists
     *
     * @return string
     */
    private function resolveModel()
    {
        $type = $this->request->type;

        return 'FluentCrm\App\Models\\' . ($type === 'tags' ? 'Tag' : 'Lists');
    }

    /**
     * Get the attachment options e.g. attach or, detach
     *
     * @param \FluentCrm\App\Models\Model $model
     * @param string $type
     * @return array
     */
    private function attachments($model, $type = 'attach')
    {
        $attachments = $this->request->get($type, []);
        $findBy = sanitize_text_field($this->request->get('find_by', 'slug'));

        if ($attachments) {
            $items = $model::select('id')->whereIn($findBy, $attachments)->get();

            if (!$items->isEmpty()) {
                return array_map(function ($item) {
                    return $item['id'];
                }, $items->toArray());
            }
        }

        return [];
    }

    /**
     * Handles if subscriber already exists.
     *
     * @return bool
     */
    private function isNew()
    {
        $subscriber = Subscriber::where(
            'email', $this->request->getSafe('email', 'sanitize_email', '')
        )->first();

        if ($subscriber) {
            return false;
        }

        return true;
    }

    public function emails(Request $request, $subscriberId)
    {
        $filter = sanitize_text_field(Arr::get($request->get(), 'filter'));

        $emailsQuery = CampaignEmail::where('subscriber_id', $subscriberId)
            ->orderBy('id', 'DESC');

        // Apply filter if present
        if ($filter == 'open') {
            $emailsQuery->where('is_open', '1');
        } elseif ($filter == 'click') {
            $emailsQuery->whereNotNull('click_counter');
        } elseif ($filter == 'unopened') {
            $emailsQuery->where('is_open', '==', 0);
        }

        $emails = $emailsQuery->paginate();
        $tab = Arr::get($request->all(), 'tab', 'crm');

        if (defined('FLUENTMAIL_PLUGIN_FILE') && $tab == 'fluentsmtp') {
            $getLogsByCurrentUser = Subscriber::where('id', $subscriberId)->pluck('email')->first();

            $emails = [];

            if (!empty($getLogsByCurrentUser)) {
                global $wpdb;
                $emails = fluentMailDb()->table(FLUENT_MAIL_DB_PREFIX . 'email_logs')
                    ->where('to', 'LIKE', '%' . $wpdb->esc_like($getLogsByCurrentUser) . '%')
                    ->orderBy('id', 'DESC')
                    ->paginate();

                // Format the email log results. The FluentSMTP log stores full rendered
                // bodies (including password-reset links and other secrets sent to the
                // address); formatResult() redacts any body containing sensitive data so
                // a contact manager cannot read, e.g., an admin's reset link from a
                // contact created with the admin's email address.
                $emails['data'] = $this->formatResult($emails['data']);
            }
        }

        /**
         * Determine and retrieve emails for a subscriber.
         *
         * This filter allows modifying the list of emails associated with a subscriber.
         *
         * @param array {
         *     Array containing the email data.
         *
         * @type array $emails List of email records or paginated results.
         * }
         * @param int $subscriberId The ID of the subscriber.
         *
         * @return array Filtered email data for the subscriber.
         * @since 1.0.0
         *
         */
        return apply_filters('fluentcrm_contact_emails', [
            'emails' => $emails
        ], $subscriberId);
    }

    protected function formatResult($result)
    {
        $result = is_array($result) ? $result : func_get_args();

        foreach ($result as $key => $row) {
            $result[$key] = array_map('maybe_unserialize', (array)$row);
            $result[$key]['id'] = (int)$result[$key]['id'];
            $result[$key]['retries'] = (int)$result[$key]['retries'];
            $result[$key]['from'] = htmlspecialchars($result[$key]['from']);
            $result[$key]['subject'] = wp_kses_post(wp_unslash($result[$key]['subject']));

            // Redact bodies that contain secrets (e.g. password-reset links) so they are
            // not exposed in the contact email-log viewer, which is reachable by
            // non-admin contact managers. See emailLogBodyHasSensitiveData().
            if (isset($result[$key]['body']) && $this->emailLogBodyHasSensitiveData($result[$key]['body'])) {
                $result[$key]['body'] = $this->getRedactedEmailBodyNotice();
                $result[$key]['is_redacted'] = true;
            }
        }

        return $result;
    }

    /**
     * Detects whether a rendered email body contains sensitive material that must not be
     * surfaced in the contact email-log viewer (accessible to non-admin contact managers).
     *
     * Currently matches WordPress password-reset / set-password links, which grant
     * account takeover if read by anyone other than the recipient. The pattern list is
     * filterable so integrations can register additional secrets (magic links, OTP URLs).
     *
     * @param string $body Rendered email HTML/text.
     * @return bool
     */
    protected function emailLogBodyHasSensitiveData($body)
    {
        if (!$body || !is_string($body)) {
            return false;
        }

        // WordPress reset links look like: wp-login.php?action=rp&key=...&login=...
        $patterns = [
            '/wp-login\.php\?action=(rp|resetpass)/i',
            '/[?&]action=rp&(amp;)?key=/i',
            '/[?&]key=[^&"\'\s]+&(amp;)?login=/i',
        ];

        /**
         * Filter the regex patterns used to detect sensitive content in email-log bodies.
         *
         * @param array  $patterns Array of PCRE patterns.
         * @param string $body     The rendered email body being inspected.
         * @since 3.1.9
         */
        $patterns = apply_filters('fluent_crm/email_log_sensitive_patterns', $patterns, $body);

        foreach ($patterns as $pattern) {
            if (@preg_match($pattern, $body)) {
                return true;
            }
        }

        return false;
    }

    /**
     * The placeholder shown in place of a redacted email-log body.
     *
     * @return string
     */
    protected function getRedactedEmailBodyNotice()
    {
        return '<div class="fc_redacted_email_body" style="padding:16px;border:1px solid #E1E4EA;border-radius:6px;background:#F5F7FA;color:#525866;">'
            . esc_html__('This email contains sensitive information (such as a password reset or login link) and its content has been hidden for security reasons.', 'fluent-crm')
            . '</div>';
    }

    public function deleteEmails(Request $request, $subscriberId)
    {
        $emailIds = array_map('intval', $request->get('email_ids'));
        CampaignEmail::where('subscriber_id', $subscriberId)
            ->whereIn('id', $emailIds)
            ->delete();
        return [
            'message' => __('Selected emails have been deleted', 'fluent-crm')
        ];
    }

    public function getNotes()
    {
        $subscriberId = $this->request->get('id');
        $search = $this->request->get('search');
        $includeId = intval($this->request->get('include_id', 0));

        $notes = SubscriberNote::where('subscriber_id', $subscriberId);

        if (!empty($search)) {
            global $wpdb;
            $notes = $notes->where('title', 'LIKE', '%' . $wpdb->esc_like(sanitize_text_field($search)) . '%');
        }

        $notes = $notes->orderBy('id', 'DESC')
            ->paginate();

        foreach ($notes as $note) {
            $note->added_by = $note->createdBy();
        }
        $fields['fields'] = Helper::getNoteSyncFields();

        $response = [
            'notes'  => $notes,
            'fields' => $fields
        ];

        if ($includeId) {
            $noteIds = (new Collection($notes->items()))->pluck('id')->toArray();
            if (!in_array($includeId, $noteIds)) {
                $includedNote = SubscriberNote::where('id', $includeId)
                    ->where('subscriber_id', $subscriberId)
                    ->first();
                if ($includedNote) {
                    $includedNote->added_by = $includedNote->createdBy();
                    $response['included_note'] = $includedNote;
                }
            }
        }

        return $this->sendSuccess($response);
    }

    public function addNote(Request $request, $id)
    {
        $subscriber = Subscriber::findOrFail($id);

        $note = $this->validate($request->get('note'), [
            'title'       => 'required',
            'description' => 'required',
            'type'        => 'required',
            'created_at'  => 'nullable|date'
        ]);

        if (empty($note['created_at'])) {
            $note['created_at'] = current_time('mysql');
        }

        /**
         * Parse the Subscriber's Note Description.
         *
         * @param string $note ['description'] The subscriber's note description.
         * @param object $subscriber The subscriber object.
         * @since 2.8.44
         *
         */
        $note['description'] = apply_filters('fluent_crm/parse_campaign_email_text', $note['description'], $subscriber);

        $note['subscriber_id'] = $id;

        $note = Sanitize::contactNote($note);

        // Only persist server-trusted fields on this endpoint (mirrors updateNote): authorship
        // is always the current user, and note metadata like parent_id/status is not
        // client-settable here. Prevents forging note authorship / re-threading via the payload.
        $note = Arr::only($note, ['subscriber_id', 'title', 'description', 'type', 'created_at']);
        $note['created_by'] = get_current_user_id();

        $subscriberNote = SubscriberNote::create(wp_unslash($note));

        /**
         * Subscriber's Note Added
         *
         * @param SubscriberNote $subscriberNote Note Model.
         * @param Subscriber $subscriber Contact Model.
         * @param array $note Contact Note Data Array.
         * @since 1.0
         */
        do_action('fluent_crm/note_added', $subscriberNote, $subscriber, $note);

        return $this->sendSuccess([
            'note'    => $subscriberNote,
            'message' => __('Note successfully added', 'fluent-crm')
        ]);
    }

    public function updateNote(Request $request, $id, $noteId)
    {
        $subscriber = Subscriber::findOrFail($id);

        $note = $this->validate($request->get('note'), [
            'title'       => 'required',
            'description' => 'required',
            'type'        => 'required',
            'created_at'  => 'sometimes|date'
        ]);

        $note = Arr::only(wp_unslash($note), ['title', 'description', 'type', 'created_at']);

        if (empty($note['created_at'])) {
            unset($note['created_at']);
        }

        /**
         * Parse the campaign email text for Subscriber's Note Description.
         *
         * This filter allows you to modify the campaign email text before it is processed for a Subscriber's Note Description.
         *
         * @param string $note ['description'] Subscriber's Note Description from parsed campaign email text.
         * @param object $subscriber The subscriber object data.
         * @since 2.8.44
         *
         */
        $note['description'] = apply_filters('fluent_crm/parse_campaign_email_text', $note['description'], $subscriber);

        $note = Sanitize::contactNote($note);

        // Scope the note to this contact so a note belonging to another contact cannot be
        // edited by pairing its id with a different (accessible) contact route id.
        $subsciberNote = SubscriberNote::where('id', $noteId)
            ->where('subscriber_id', $subscriber->id)
            ->firstOrFail();
        $subsciberNote->fill($note);
        $subsciberNote->save();

        /**
         * Subscriber's Note Updated
         *
         * @param SubscriberNote $subscriberNote Note Model.
         * @param Subscriber $subscriber Contact Model.
         * @param array $note Contact Note Data Array.
         * @since 1.0
         */
        do_action('fluent_crm/note_updated', $subsciberNote, $subscriber, $note);

        return $this->sendSuccess([
            'note'    => $subsciberNote,
            'message' => __('Note successfully updated', 'fluent-crm')
        ]);
    }

    public function deleteNote($id, $noteId)
    {
        $subscriber = Subscriber::findOrFail($id);
        // Scope the delete to this contact so a note belonging to another contact cannot be
        // deleted by pairing its id with a different (accessible) contact route id.
        $deleted = SubscriberNote::where('id', $noteId)
            ->where('subscriber_id', $subscriber->id)
            ->delete();

        if ($deleted) {
            /**
             * Subscriber's Note Delete
             *
             * @param SubscriberNote $subscriberNote Note Model.
             * @param Subscriber $subscriber Contact Model.
             * @since 1.0
             */
            do_action('fluent_crm/note_delete', $noteId, $subscriber);
        }

        return $this->sendSuccess([
            'message' => __('Note successfully deleted', 'fluent-crm')
        ]);
    }

    public function bulkDeleteNotes(Request $request, $id)
    {
        $subscriber = Subscriber::findOrFail($id);
        $noteIds = array_filter(array_map('intval', (array) $request->get('note_ids', [])));

        if (empty($noteIds)) {
            return $this->sendError([
                'message' => __('No note IDs provided', 'fluent-crm')
            ]);
        }

        if (count($noteIds) > 200) {
            return $this->sendError([
                'message' => __('Too many notes selected. Please delete 200 or fewer notes at a time.', 'fluent-crm')
            ]);
        }

        // Scope delete to this subscriber so users cannot delete notes belonging to other contacts.
        $deletableNoteIds = SubscriberNote::where('subscriber_id', $subscriber->id)
            ->whereIn('id', $noteIds)
            ->pluck('id')
            ->toArray();

        $deletedCount = 0;
        if ($deletableNoteIds) {
            $deletedCount = SubscriberNote::whereIn('id', $deletableNoteIds)->delete();

            foreach ($deletableNoteIds as $deletedNoteId) {
                do_action('fluent_crm/note_delete', $deletedNoteId, $subscriber);
            }
        }

        return $this->sendSuccess([
            'message' => sprintf(
                /* translators: %d: number of deleted notes */
                _n('%d note deleted', '%d notes deleted', $deletedCount, 'fluent-crm'),
                $deletedCount
            )
        ]);
    }

    public function getFormSubmissions()
    {
        $provider = $this->request->get('provider');
        $subscriberId = intval($this->request->get('id'));
        $subscriber = Subscriber::findOrFail($subscriberId);

        /**
         * Filter the form submissions data for a specific provider.
         *
         * The dynamic portion of the hook name, `$provider`, refers to the form provider.
         *
         * @param array {
         *     An array of form submissions data.
         *
         * @type array $data The form submissions data.
         * @type int $total The total number of form submissions.
         * }
         * @param object $subscriber The subscriber object.
         * @since 2.5.1
         *
         * Security: the returned column values are rendered as raw HTML in the admin UI
         * (Vue v-html) without client-side sanitization. Producers hooking this filter MUST
         * escape any subscriber-authored data (e.g. via esc_html() / wp_kses_post()) before
         * returning it, to prevent stored XSS in the admin. Structural markup and intentional
         * rich content are allowed by design.
         */
        $data = apply_filters('fluentcrm_get_form_submissions_' . $provider, [
            'data'  => [],
            'total' => 0
        ], $subscriber);

        return $this->sendSuccess([
            'submissions' => $data
        ]);
    }

    public function getSupportTickets()
    {
        $provider = $this->request->get('provider');
        $subscriberId = intval($this->request->get('id'));
        $subscriber = Subscriber::where('id', $subscriberId)->first();

        /**
         * Determine the support tickets data for a specific provider and subscriber.
         *
         * The dynamic portion of the hook name, `$provider`, refers to the support ticket provider.
         *
         * @param array {
         *     An array of support tickets data.
         *
         * @type array $data The support tickets data.
         * @type int $total The total number of support tickets.
         * }
         * @param object $subscriber The subscriber object.
         * @since 2.5.1
         *
         * Security: the returned column values are rendered as raw HTML in the admin UI
         * (Vue v-html) without client-side sanitization. Producers hooking this filter MUST
         * escape any subscriber-authored data (e.g. via esc_html() / wp_kses_post()) before
         * returning it, to prevent stored XSS in the admin. Structural markup and intentional
         * rich content are allowed by design.
         */
        $data = apply_filters('fluentcrm-get_support_tickets_' . $provider, [
            'data'  => [],
            'total' => 0
        ], $subscriber);

        $data['columns_config'] = [
            'id'           => [
                'label' => __('ID', 'fluent-crm'),
                'width' => '100px'
            ],
            'status'       => [
                'label' => 'Status',
                'width' => '120px'
            ],
            'Submitted at' => [
                'label' => 'Submitted at',
                'width' => '150px'
            ],
            'action'       => [
                'label' => 'Action',
                'width' => '150px'
            ]
        ];

        return $this->sendSuccess([
            'tickets' => $data
        ]);
    }

    public function sendDoubleOptinEmail(Request $request, $id)
    {
        $subscriber = Subscriber::findOrFail($id);

        if ($subscriber->status == 'subscribed') {
            return $this->sendError([
                'message' => __('Contact Already Subscribed', 'fluent-crm')
            ]);
        }

        // The admin explicitly asked for an opt-in send, and the opt-in email is
        // strictly gated on status == 'pending' — so this caller moves the contact
        // into the opt-in pipeline first (that status decision is the caller's).
        if ($subscriber->status != 'pending') {
            $subscriber->updateStatus('pending');
        }

        if (!$subscriber->sendDoubleOptinEmail()) {
            return $this->sendError([
                'message' => __('Double opt-in email could not be sent. Please check the double opt-in settings; resends are also throttled for a couple of minutes.', 'fluent-crm')
            ]);
        }

        return $this->sendSuccess([
            'message' => __('Double OptIn email has been sent', 'fluent-crm')
        ]);
    }

    public function getTemplateMock(Request $request, $id)
    {
        $emailMock = CustomEmailCampaign::getMock();
        $emailMock['title'] = __('Custom Email to Contact', 'fluent-crm');
        return [
            'email_mock' => $emailMock
        ];
    }

    public function sendCustomEmail(Request $request, $contactId)
    {
        $contact = Subscriber::findOrFail($contactId);

        $validStatuses = ['subscribed', 'transactional'];

        if (!in_array($contact->status, $validStatuses)) {
            return $this->sendError([
                'message' => __('Subscriber\'s status need to be subscribed.', 'fluent-crm')
            ]);
        }

        add_action('wp_mail_failed', function ($wpError) {
            Helper::debugLog(
                'Custom Email failed',
                $wpError->get_error_message(),
                'error'
            );
        }, 10, 1);

        $newCampaign = $request->get('campaign');
        unset($newCampaign['id']);

        $newCampaign = Sanitize::campaign($newCampaign);

        // Title and status are decided server side, not by the client: the
        // one-off composer has no title field (it only edits subject/body), so
        // whatever the client posts is the hardcoded mock string. Deriving both
        // here keeps every one-off row identifiable and keeps this path in
        // lockstep with the MCP send-email-to-contact tool, which writes the
        // same status. See Helper::oneOffEmailTitle() for the title shape.
        $newCampaign['title'] = Helper::oneOffEmailTitle($contact->email);
        $newCampaign['status'] = 'published';

        $campaign = CustomEmailCampaign::create($newCampaign);

        $campaign->subscribe([$contactId], [
            'status'       => 'scheduled',
            'scheduled_at' => current_time('mysql')
        ]);

        do_action('fluentcrm_process_contact_jobs', $contact);

        return [
            'message' => __('Custom Email has been successfully sent', 'fluent-crm')
        ];
    }

    public function getExternalView(Request $request, $subscriberId)
    {
        $subscriber = Subscriber::findOrFail($subscriberId);
        $sectionId = $request->get('section_provider');

        /**
         * Filter the profile section content for a specific section ID.
         *
         * The dynamic portion of the hook name, `$sectionId`, refers to the ID of the profile section.
         *
         * @param array {
         *     An array of profile section data.
         *
         * @type string $heading The heading of the profile section.
         * @type string $content_html The HTML content of the profile section.
         * }
         * @param object $subscriber The subscriber object.
         * @since 2.5.1
         *
         * Security: `content_html` is rendered as raw HTML in the admin UI (Vue v-html)
         * without client-side sanitization. Producers hooking this filter MUST escape any
         * subscriber-authored data (e.g. via esc_html() / wp_kses_post()) before returning it,
         * to prevent stored XSS in the admin. Structural markup and intentional rich content
         * (styles, iframes, scripts) are allowed by design.
         */
        return apply_filters('fluencrm_profile_section_' . $sectionId, [
            'heading'      => '',
            'content_html' => ''
        ], $subscriber);
    }

    public function saveExternalViewData(Request $request, $subscriberId)
    {
        $subscriber = Subscriber::findOrFail($subscriberId);
        $sectionId = $request->get('section_provider');

        /**
         * Filter the data being saved for a specific profile section.
         *
         * This filter allows modifying the data before saving it for a profile section
         * identified by the `$sectionId`.
         *
         * @param mixed  The data to be saved for the profile section. Defaults to an empty string.
         * @param array  The input data received from the request. Defaults to an empty array.
         * @param object $subscriber The subscriber object for which the profile section is being updated.
         *
         * @return mixed Filtered data to be saved for the profile section.
         * @since 2.8.44
         *
         */
        $response = apply_filters('fluencrm_profile_section_save_' . $sectionId, '', $request->get('data', []), $subscriber);

        if (!$response) {
            return $this->sendError([
                'message' => __('Handled could not be found.', 'fluent-crm')
            ]);
        }

        return $response;
    }

    public function handleBulkActions(Request $request)
    {
        $actionName = sanitize_text_field($request->get('action_name', ''));
        $doingAllBulk = $request->get('is_all') == 'yes';

        if ($doingAllBulk) {

            $contactQuery = $request->get('contact_query', []);

            $filterType = Arr::get($contactQuery, 'filter_type', 'simple');

            $with = [];

            if ($filterType == 'advanced') {

                $rawGroup = json_decode(Arr::get($contactQuery, 'advanced_filters', ''), true);

                if (!$rawGroup || !is_array($rawGroup)) {
                    return $this->sendError([
                        'message' => __('Invalid Advanced Filters', 'fluent-crm')
                    ]);
                }

                $queryArgs = [
                    'with'               => $with,
                    'filter_type'        => 'advanced',
                    'filters_groups_raw' => $rawGroup,
                    'search'             => trim(Arr::get($contactQuery, 'search', '')),
                    'sort_by'            => 'id',
                    'sort_type'          => 'ASC',
                    'has_commerce'       => false,
                    'custom_fields'      => false,
                    'company_ids'        => Arr::get($contactQuery, 'company_ids', []),
                ];
            } else {
                $queryArgs = [
                    'with'          => $with,
                    'filter_type'   => 'simple',
                    'search'        => trim(Arr::get($contactQuery, 'search', '')),
                    'sort_by'       => 'id',
                    'sort_type'     => 'ASC',
                    'has_commerce'  => false,
                    'custom_fields' => false,
                    'tags'          => Arr::get($contactQuery, 'tags', []),
                    'statuses'      => Arr::get($contactQuery, 'statuses', []),
                    'lists'         => Arr::get($contactQuery, 'lists', []),
                    'company_ids'   => Arr::get($contactQuery, 'company_ids', []),
                ];
            }

            $subscribersModel = (new ContactsQuery($queryArgs))->getModel();
            $lastId = $request->get('last_id', 0);
            $bulkActionLimit = (int)apply_filters('fluent_crm/contact_bulk_action_limit', 400, $request);
            if ($bulkActionLimit < 1) {
                $bulkActionLimit = 1;
            }

            $subscribersModel = $subscribersModel->select(['id'])
                ->limit($bulkActionLimit)
                ->where('id', '>', $lastId)
                ->get();

            if ($subscribersModel->isEmpty()) {
                return [
                    'is_completed'       => true,
                    'completed_contacts' => 0,
                    'message'            => __('All contacts have been processed', 'fluent-crm')
                ];
            }

            $subscriberIds = $subscribersModel->pluck('id')->toArray();

        } else {
            $subscriberIds = array_map('intval', $request->get('subscriber_ids', []));
            $subscriberIds = array_filter($subscriberIds);
        }

        $lastContactId = end($subscriberIds);

        if (!$subscriberIds) {
            return $this->sendError([
                'message' => __('Subscribers selection is required', 'fluent-crm')
            ]);
        }

        if ($actionName == 'delete_contacts') {
            Helper::deleteContacts($subscriberIds);
            return $this->sendSuccess([
                'completed_contacts' => count($subscriberIds),
                'last_contact_id'    => $lastContactId,
                'message'            => __('Selected Contacts have been deleted permanently', 'fluent-crm'),
            ]);
        } elseif ($actionName == 'send_double_optin') {
            Helper::sendDoubleOptin($subscriberIds);
            return $this->sendSuccess([
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                'message'            => __('Double optin sent to selected contacts', 'fluent-crm'),
            ]);
        } elseif ($actionName == 'add_to_email_sequence') {
            if (!defined('FLUENTCAMPAIGN')) {
                return $this->sendError([
                    'message' => __('This action requires FluentCRM Pro', 'fluent-crm')
                ]);
            }

            $sequenceId = (int)$request->get('new_status', '');

            if (!$sequenceId) {
                return $this->sendError([
                    'message' => __('Invalid Email Sequence ID', 'fluent-crm')
                ]);
            }

            $sequence = \FluentCampaign\App\Models\Sequence::findOrFail($sequenceId);

            $validSubscribers = Subscriber::whereIn('id', $subscriberIds)
                ->whereDoesntHave('sequences', function ($q) use ($sequenceId) {
                    $q->where('fc_campaigns.id', $sequenceId);
                })
                ->where('status', 'subscribed')
                ->get();

            if ($validSubscribers->isEmpty()) {
                if ($doingAllBulk) {
                    return $this->sendSuccess([
                        'last_contact_id'    => $lastContactId,
                        'completed_contacts' => count($subscriberIds),
                        'message'            => __('No valid active subscribers found for this chunk', 'fluent-crm')
                    ]);
                }
                return $this->sendError([
                    'message' => __('No valid active subscribers found for this sequence', 'fluent-crm')
                ]);
            }

            $sequence->subscribe($validSubscribers);
            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                /* translators: 1. Number of subscribers */
                'message'            => sprintf(__('%d subscribers have been attached to the selected email sequence', 'fluent-crm'), count($validSubscribers))
            ];

        } elseif ($actionName == 'add_to_company') {
            $companyId = (int)$request->get('new_status', '');

            if (!$companyId) {
                return $this->sendError([
                    'message' => __('Invalid Company ID', 'fluent-crm')
                ]);
            }

            $company = Company::findOrFail($companyId);

            $validSubscribers = Subscriber::whereIn('id', $subscriberIds)
                ->whereDoesntHave('companies', function ($q) use ($companyId) {
                    $q->where('fc_companies.id', $companyId);
                })
                ->get();

            if ($validSubscribers->isEmpty()) {

                if ($doingAllBulk) {
                    return $this->sendSuccess([
                        'last_contact_id'    => $lastContactId,
                        'completed_contacts' => count($subscriberIds),
                        'message'            => __('No valid active subscribers found for this chunk', 'fluent-crm')
                    ]);
                }

                return $this->sendError([
                    'message' => __('No valid active subscribers found for this company', 'fluent-crm')
                ]);
            }

            foreach ($validSubscribers as $contact) {
                if ($contact) {
                    $contact->attachCompanies([$company->id]);
                    if (!$contact->company_id) {
                        $contact->company_id = $company->id;
                        $contact->save();
                    }
                }
            }
            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                /* translators: %d is the number of subscribers */
                'message'            => sprintf(__('%d subscribers have been attached to the selected company', 'fluent-crm'), count($validSubscribers))
            ];

        } elseif ($actionName == 'remove_from_company') {
            $companyId = (int)$request->get('new_status', '');

            if (!$companyId) {
                return $this->sendError([
                    'message' => __('Invalid Company ID', 'fluent-crm')
                ]);
            }

            $company = Company::findOrFail($companyId);

            $validSubscribers = Subscriber::whereIn('id', $subscriberIds)
                ->whereHas('companies', function ($q) use ($companyId) {
                    $q->where('fc_companies.id', $companyId);
                })
                ->get();

            if ($validSubscribers->isEmpty()) {

                if ($doingAllBulk) {
                    return $this->sendSuccess([
                        'last_contact_id'    => $lastContactId,
                        'completed_contacts' => count($subscriberIds),
                        'message'            => __('No valid active subscribers found for this chunk', 'fluent-crm')
                    ]);
                }

                return $this->sendError([
                    'message' => __('No valid active subscribers found for this company', 'fluent-crm')
                ]);
            }

            foreach ($validSubscribers as $contact) {
                if ($contact) {
                    $contact->detachCompanies([$company->id]);
                    if ($contact->company_id == $company->id) {
                        $contact->company_id = null;
                        $contact->save();
                    }
                }
            }
            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                /* translators: %d is the number of subscribers */
                'message'            => sprintf(__('%d subscribers have been detached from the selected company', 'fluent-crm'), count($validSubscribers))
            ];

        } elseif ($actionName == 'change_contact_status') {
            $newStatus = sanitize_text_field($request->get('new_status', ''));
            if (!$newStatus) {
                return $this->sendError([
                    'message' => __('Please select status', 'fluent-crm')
                ]);
            }

            $subscribers = Subscriber::whereIn('id', $subscriberIds)->get();

            foreach ($subscribers as $subscriber) {
                $oldStatus = $subscriber->status;
                if ($oldStatus != $newStatus) {
                    $subscriber->updateStatus($newStatus);
                }
            }

            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                'message'            => __('Status has been changed for the selected subscribers', 'fluent-crm')
            ];
        } elseif ($actionName == 'add_to_automation') {
            if (!defined('FLUENTCAMPAIGN')) {
                return $this->sendError([
                    'message' => __('This action requires FluentCRM Pro', 'fluent-crm')
                ]);
            }

            $automationId = (int)$request->get('new_status', '');

            if (!$automationId) {
                return $this->sendError([
                    'message' => __('Invalid Automation Funnel ID', 'fluent-crm')
                ]);
            }

            $automation = Funnel::findOrFail($automationId);

            $validSubscribers = Subscriber::whereIn('id', $subscriberIds)
                ->whereDoesntHave('funnels', function ($q) use ($automationId) {
                    $q->where('fc_funnels.id', $automationId);
                })
                ->get();

            if ($validSubscribers->isEmpty()) {

                if ($doingAllBulk) {
                    return $this->sendSuccess([
                        'last_contact_id'    => $lastContactId,
                        'completed_contacts' => count($subscriberIds),
                        'message'            => __('No valid active subscribers found for this chunk', 'fluent-crm')
                    ]);
                }

                return $this->sendError([
                    'message' => __('No valid active subscribers found for this funnel', 'fluent-crm')
                ]);
            }

            foreach ($validSubscribers as $subscriber) {
                (new FunnelProcessor())->startFunnelSequence($automation, [], [
                    'source_trigger_name' => 'fcrm_manual_attach'
                ], $subscriber);
            }

            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                /* translators: %d is the number of subscribers */
                'message'            => sprintf(__('%d subscribers have been attached to the selected automation funnel', 'fluent-crm'), count($validSubscribers)),
                'subscribers'        => $validSubscribers
            ];
        } else if ($actionName == 'change_contact_type') {
            $subscribers = Subscriber::whereIn('id', $subscriberIds)->get();
            $newType = sanitize_text_field($request->get('new_status', ''));
            if (!$newType) {
                return $this->sendError([
                    'message' => __('Please select new type', 'fluent-crm')
                ]);
            }
            foreach ($subscribers as $subscriber) {
                $oldType = $subscriber->contact_type;
                if ($oldType != $newType) {
                    $subscriber->contact_type = $newType;
                    $subscriber->save();
                    do_action('fluent_crm/subscriber_contact_type_to_' . $newType, $subscriber, $oldType);
                }
            }

            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                'message'            => __('Contact Type has been updated for the selected subscribers', 'fluent-crm')
            ];
        } else if ($actionName == 'update_custom_fields') {
            $customField = $request->get('custom_field');
            $customFieldKey = Arr::get($customField, 'key');
            $customFieldValue = Arr::get($customField, 'value');
            $subscribers = Subscriber::whereIn('id', $subscriberIds)->get();

            if (empty($customFieldKey)) {
                return $this->sendError([
                    'message' => __('Please provide a valid custom field key', 'fluent-crm')
                ]);
            }

            foreach ($subscribers as $subscriber) {
                // Scope to object_type = 'custom_field' (Behavioral Rule 11) so a field
                // slugged like an internal meta key can't overwrite the internal row.
                $existField = SubscriberMeta::where('key', $customFieldKey)
                    ->where('subscriber_id', $subscriber->id)
                    ->where('object_type', 'custom_field')
                    ->first();

                // check if exists
                if ($existField) {
                    if ($existField->value == $customFieldValue) {
                        continue;
                    }
                    $existField->fill(['value' => $customFieldValue])->save();
                } else {
                    $customFieldMeta = new SubscriberMeta();
                    $customFieldMeta->fill([
                        'subscriber_id' => $subscriber->id,
                        'object_type'   => 'custom_field',
                        'key'           => $customFieldKey,
                        'value'         => $customFieldValue,
                        'created_by'    => get_current_user_id()
                    ]);
                    $customFieldMeta->save();
                }
            }

            return [
                'last_contact_id'    => $lastContactId,
                'completed_contacts' => count($subscriberIds),
                'message'            => __('Custom Fields has been updated for the selected subscribers', 'fluent-crm')
            ];
        }

        $validActions = [
            'add_to_tags'       => 'attachTags',
            'add_to_lists'      => 'attachLists',
            'remove_from_tags'  => 'detachTags',
            'remove_from_lists' => 'detachLists'
        ];

        if (!isset($validActions[$actionName])) {
            $response = $this->sendError([
                'message' => __('Selected Action is not valid', 'fluent-crm')
            ]);

            /**
             * Filter the result of a bulk action performed on FluentCRM contacts.
             *
             * The dynamic portion of the hook name, `$actionName`, refers to the specific bulk action being performed.
             *
             * @param mixed $response The initial response for the bulk action. Can be modified by the filter.
             * @param array $subscriberIds An array of subscriber IDs targeted by the bulk action.
             * @param array $request ->all()   The full request data as an associative array.
             *
             * @return mixed Filtered response for the bulk action.
             * @since 2.9.0
             *
             */
            $result = apply_filters('fluent_crm/contact_bulk_action_' . $actionName, $response, $subscriberIds, $request->all());

            if (is_array($result)) {
                $result['last_contact_id'] = $lastContactId;
                $result['completed_contacts'] = count($subscriberIds);
            }

            return $result;
        }

        $options = $request->get('action_options', []);

        $options = array_map(function ($id) {
            return intval($id);
        }, $options);

        $options = array_filter($options);

        if (!$options) {
            return $this->sendError([
                'message' => __('Please provide bulk options', 'fluent-crm')
            ]);
        }

        $method = $validActions[$actionName];

        $subscribers = Subscriber::whereIn('id', $subscriberIds)->get();

        foreach ($subscribers as $subscriber) {
            $subscriber->{$method}($options);
        }

        return [
            'last_contact_id'    => $lastContactId,
            'completed_contacts' => count($subscriberIds),
            'message'            => __('Selected bulk action has been successfully completed', 'fluent-crm')
        ];
    }

    public function getPrevNextIds(Request $request)
    {
        $filterType = $this->request->get('filter_type');

        $currentId = (int)$this->request->get('current_id');

        if (!$filterType || !$currentId) {
            return $this->sendError([
                'message' => __('filter_type and current_id are required', 'fluent-crm')
            ]);
        }

        $sortType = sanitize_sql_orderby($this->request->get('sort_type', 'DESC'));
        $prevSortType = ($sortType == 'DESC') ? 'ASC' : 'DESC';

        if ($filterType == 'advanced') {
            $queryArgs = [
                'filter_type'        => 'advanced',
                'filters_groups_raw' => Helper::parseArrayOrJson($this->request->get('advanced_filters')),
                'search'             => trim(sanitize_text_field($this->request->get('search', ''))),
                'sort_by'            => 'id',
                'sort_type'          => $sortType,
                'with'               => []
            ];
        } else {
            $queryArgs = [
                'filter_type' => 'simple',
                'search'      => trim(sanitize_text_field($this->request->get('search', ''))),
                'sort_by'     => 'id',
                'sort_type'   => $sortType,
                'tags'        => $this->request->get('tags', []),
                'statuses'    => $this->request->get('statuses', []),
                'lists'       => $this->request->get('lists', []),
                'with'        => []
            ];
        }

        $prevQueryArgs = $queryArgs;

        $prevQueryArgs['sort_type'] = ($sortType == 'DESC') ? 'ASC' : 'DESC';

        $prevItems = (new ContactsQuery($prevQueryArgs))
            ->getModel()
            ->select(['id'])
            ->limit(10)
            ->where('id', ($sortType == 'DESC') ? '>' : '<', $currentId)
            ->get();

        $nextItems = (new ContactsQuery($queryArgs))
            ->getModel()
            ->select(['id'])
            ->limit(10)
            ->where('id', ($sortType == 'DESC') ? '<' : '>', $currentId)
            ->get();

        $formattedNext = [];
        foreach ($nextItems as $nextItem) {
            $formattedNext[] = $nextItem->id;
        }

        $formattedPrev = [];
        foreach ($prevItems as $prevItem) {
            $formattedPrev[] = $prevItem->id;
        }

        return [
            'navigation' => [
                'next' => $formattedNext,
                'prev' => $formattedPrev
            ],
            'has_next'   => count($formattedNext) == 10,
            'has_prev'   => count($formattedPrev) == 10
        ];

    }

    public function searchContacts(Request $request)
    {
        $search = trim($request->getSafe('search', 'sanitize_text_field', ''));
        $limit = absint($request->get('limit', 20));
        if (!$limit) {
            $limit = 20;
        }
        $offset = absint($request->get('offset', 0));

        $loadDefault = $request->get('load_default');
        $loadDefault = in_array($loadDefault, [true, 1, '1', 'true', 'yes'], true);

        $contacts = [];

        if ($search) {
            $subscribers = Subscriber::searchBy($search)->offset($offset)->limit($limit)->get();
            foreach ($subscribers as $subscriber) {
                $contacts[$subscriber->id] = [
                    'first_name' => $subscriber->first_name,
                    'last_name'  => $subscriber->last_name,
                    'full_name'  => $subscriber->full_name,
                    'email'      => $subscriber->email,
                    'id'         => (string)$subscriber->id,
                    'photo'      => $subscriber->photo
                ];
            }
        } else if ($loadDefault) {
            $subscribers = Subscriber::orderBy('id', 'DESC')->offset($offset)->limit($limit)->get();
            foreach ($subscribers as $subscriber) {
                $contacts[$subscriber->id] = [
                    'first_name' => $subscriber->first_name,
                    'last_name'  => $subscriber->last_name,
                    'full_name'  => $subscriber->full_name,
                    'email'      => $subscriber->email,
                    'id'         => (string)$subscriber->id,
                    'photo'      => $subscriber->photo
                ];
            }
        }

        $values = (array)$request->get('values', []);

        if ($values) {
            $pushedIds = array_keys($contacts);
            $includedIds = array_diff($values, $pushedIds);
            if ($includedIds) {
                $subscribers = Subscriber::whereIn('id', $includedIds)->get();
                foreach ($subscribers as $subscriber) {
                    $contacts[$subscriber->id] = [
                        'first_name' => $subscriber->first_name,
                        'last_name'  => $subscriber->last_name,
                        'full_name'  => $subscriber->full_name,
                        'email'      => $subscriber->email,
                        'id'         => (string)$subscriber->id,
                        'photo'      => $subscriber->photo
                    ];
                }
            }
        }

        return [
            'contacts' => (object)$contacts
        ];
    }

    public function getInfoWidgets(Request $request, $subscriber)
    {
        if (is_numeric($subscriber)) {
            $subscriber = Subscriber::findOrFail($subscriber);
        }


        if ($byWidget = $request->get('by_widget')) {
            /**
             * Filter the subscriber info widget.
             *
             * This filter allows modification of the subscriber info widget based on the widget type.
             *
             * @param array The array of widgets.
             * @param object $subscriber The subscriber object data.
             * @since 2.8.40
             *
             */
            $widgets = apply_filters('fluent_crm/subscriber_info_widget_' . $byWidget, [], $subscriber);
            $widgets = array_values($widgets);

            if (isset($widgets[0])) {
                $widget = $widgets[0];
            } else {
                $widget = [
                    'content' => 'No content found'
                ];
            }

            return [
                'widget' => $widget
            ];
        }

        $commerce = (new PurchaseHistory())->getCommerceStatWidget($subscriber);

        /**
         * Filter the top widgets for a subscriber.
         *
         * This filter allows modification of the top widgets displayed for a subscriber.
         *
         * @param array The array of top widgets.
         * @param object $subscriber The subscriber object.
         * @since 2.8.0
         *
         * Security: each widget's `content` is rendered as raw HTML in the admin UI (Vue
         * v-html) without client-side sanitization. Producers hooking this filter MUST escape
         * any subscriber-authored data (e.g. via esc_html() / wp_kses_post()) before returning
         * it, to prevent stored XSS in the admin. Structural markup and intentional rich
         * widget content (styles, iframes, scripts) are allowed by design.
         */
        $topWidgets = array_filter(apply_filters('fluent_crm/subscriber_top_widgets', array_filter([$commerce]), $subscriber));

        /**
         * Filter the subscriber info widgets.
         *
         * This filter allows modification of the subscriber info widgets.
         *
         * @param array  An array of existing widgets.
         * @param object $subscriber The subscriber object.
         * @since 2.8.0
         *
         * Security: each widget's `content` is rendered as raw HTML in the admin UI (Vue
         * v-html) without client-side sanitization. Producers hooking this filter MUST escape
         * any subscriber-authored data (e.g. via esc_html() / wp_kses_post()) before returning
         * it, to prevent stored XSS in the admin. Structural markup and intentional rich
         * widget content (styles, iframes, scripts) are allowed by design.
         */
        $otherWidgets = apply_filters('fluent_crm/subscriber_info_widgets', [], $subscriber);

        return [
            'widgets' => [
                'top_widgets'   => $topWidgets,
                'other_widgets' => $otherWidgets,
                'widgets_count' => count($topWidgets) + count($otherWidgets)
            ]
        ];
    }

    public function getTrackingEvents(Request $request, $subscriberId)
    {
        if (!Helper::isExperimentalEnabled('event_tracking')) {
            return $this->sendError([
                'message'    => __('Event Tracker is not enabled', 'fluent-crm'),
                'error_code' => 'not_enabled'
            ]);
        }

        $subscriber = Subscriber::findOrFail($subscriberId);
        $events = EventTracker::where('subscriber_id', $subscriber->id)
            ->orderBy('id', 'DESC')
            ->paginate();

        return [
            'events' => $events
        ];
    }

    public function trackEvent(Request $request)
    {
        $data = $request->all();

        $this->validate($data, [
            'event_key' => 'required',
            'title'     => 'required'
        ]);

        $isUnique = $request->get('repeatable', true);
        $result = FluentCrmApi('event_tracker')->track($data, $isUnique);

        if (is_wp_error($result)) {
            return $this->sendError([
                'message'    => $result->get_error_message(),
                'error_code' => $result->get_error_code()
            ]);
        }

        return $this->sendSuccess([
            'message' => __('Event has been tracked', 'fluent-crm'),
            'id'      => $result->id
        ]);
    }

    public function getUrlMetrics(Request $request, $id)
    {
        $sort_by = sanitize_sql_orderby($this->request->get('sort_by', 'id'));
        $sort_type = sanitize_sql_orderby($this->request->get('sort_type', 'DESC'));
        $subscriber = Subscriber::findOrFail($id);

        $urlActivityQuery = CampaignUrlMetric::with('url_stores')
            ->where('subscriber_id', $subscriber->id)
            ->where('type', 'click');

        // Apply custom sorting if provided
        if (!empty($sort_by) && !empty($sort_type)) {
            $urlActivityQuery->orderBy($sort_by, $sort_type);
        } else {
            $urlActivityQuery->orderBy('id', 'DESC');
        }

        $urlActivity = $urlActivityQuery->paginate();

        $urlMetrics = $urlActivity->toArray();

        if (!empty($urlMetrics['data'])) {
            $urlMetrics['data'] = $this->formatUrlActivityData($urlMetrics['data']);
        }

        return [
            'urlMetrics' => $urlMetrics
        ];
    }

    public function formatUrlActivityData($data)
    {
        $result = [];
        foreach ($data as $item) {
            $result[] = [
                'url'   => $item['url_stores']['url'],
                'count' => $item['counter']
            ];
        }
        return $result;
    }

    public function getDynamicItemView(Request $request, $subscriberId)
    {
        $subscriber = Subscriber::findOrFail($subscriberId);
        $provider = (string)$request->get('provider');
        $params = $request->get('params', []);

        /**
         * Filter the dynamic item view for a specific provider.
         *
         * The dynamic portion of the hook name, `$provider`, refers to the specific provider for which the view is being fetched.
         *
         * @param array  {
         *     An array containing the dynamic item view data.
         *
         *      'type' => (string) The type of the dynamic item.
         *      'title' => (string) The title of the dynamic item.
         *      'content_html' => (string) The HTML content of the dynamic item.
         *      'footer_content' => (string) The footer content associated with the dynamic item.
         *
         * }
         * @param object $subscriber The subscriber object.
         * @param array $params Additional parameters passed in the request.
         * @since 3.0.0
         *
         */

        $data = apply_filters('fluent_crm/dynamic_contact_item_view_' . $provider, [
            'type'           => 'html',
            'title'          => 'no title',
            'content_html'   => 'sorry, no content found',
            'footer_content' => ''
        ], $params, $subscriber);

        return [
            'data_view' => $data
        ];
    }
}
