<?php

namespace FluentCrm\App\Services;

use FluentCrm\App\Models\Subscriber;
use FluentCrm\App\Models\SubscriberMeta;
use FluentCrm\Framework\Support\Arr;

class ContactsQuery
{
    private $args = [];

    private $model = null;

    public function __construct($args = [])
    {
        $this->args = wp_parse_args($args, [
            'with'               => ['tags', 'lists'],
            'filter_type'        => 'simple',
            'filters_groups'     => [],
            'filters_groups_raw' => [],
            'search'             => '',
            'sort_by'            => 'id',
            'sort_type'          => 'DESC',
            'tags'               => [],
            'lists'              => [],
            'statuses'           => [],
            'sms_statuses'           => [],
            'has_commerce'       => false,
            'custom_fields'      => false,
            'limit'              => false,
            'offset'             => false,
            'contact_status'     => '',
            'contact_type'       => '',
            'company_ids'        => [],
            'contact_ids'        => [],
            'emails'             => []
        ]);

        $this->setupQuery();
    }

    private function setupQuery()
    {
        if ($this->args['filters_groups_raw']) {
            $this->formatAdvancedFilters();
        }

        $subscribersQuery = Subscriber::with($this->args['with']);
        if ($search = $this->args['search']) {
            $subscribersQuery->searchBy($search, $this->args['custom_fields']);
        }

        if ($shortBy = $this->args['sort_by']) {
            // Allowlist of every real fc_subscribers column. The framework
            // rewrite (May 2025) made orderBy() throw LogicException for
            // names that don't match ^[a-zA-Z0-9_\.]+$, and unknown columns
            // would produce SQL errors — so the check has to happen before
            // we hand the value to the builder. getFillable() used to be
            // the gate but it omits id, contact_owner and a few others,
            // so legitimate sorts (e.g., contact_owner) silently dropped.
            $allowedSortBy = [
                'id', 'user_id', 'hash', 'contact_owner', 'company_id', 'prefix',
                'first_name', 'last_name', 'email', 'timezone', 'address_line_1',
                'address_line_2', 'postal_code', 'city', 'state', 'country', 'ip',
                'latitude', 'longitude', 'total_points', 'life_time_value', 'phone',
                'status', 'contact_type', 'source', 'avatar', 'date_of_birth',
                'created_at', 'last_activity', 'updated_at',
            ];
            if (in_array($shortBy, $allowedSortBy, true)) {
                $subscribersQuery->orderBy($shortBy, $this->args['sort_type']);
            }
        }

        if ($this->args['filter_type'] == 'advanced') {
            $filtersGroups = $this->args['filters_groups'];

            $subscribersQuery->where(function ($subscribersQueryGroup) use ($filtersGroups) {
                foreach ($filtersGroups as $groupIndex => $group) {
                    $method = 'orWhere';
                    if ($groupIndex == 0) {
                        $method = 'where';
                    }

                    $subscribersQueryGroup->{$method}(function ($q) use ($group) {
                        // Fail closed: a group whose constraints cannot be applied must match
                        // NOTHING. Constraints are added by action hooks, so a provider with no
                        // registered handler (Pro deactivated, license lapsed, feature toggled
                        // off) would otherwise add zero WHERE clauses and silently degrade the
                        // group to "match every subscriber" — the worst possible blast radius
                        // for a campaign send.
                        if (!$group) {
                            $q->whereRaw('1 = 0');
                            return;
                        }

                        foreach ($group as $providerName => $items) {
                            if (!has_action('fluentcrm_contacts_filter_' . $providerName)) {
                                $q->whereRaw('1 = 0');
                                continue;
                            }
                            do_action_ref_array('fluentcrm_contacts_filter_' . $providerName, [&$q, $items]);
                        }
                    });
                }
            });
        } else {
            if ($tags = $this->args['tags']) {
                $subscribersQuery->filterByTags($tags);
            }

            if ($lists = $this->args['lists']) {
                $subscribersQuery->filterByLists($lists);
            }

            if ($company_ids = $this->args['company_ids']) {
                $subscribersQuery->filterByCompanies($company_ids);
            }

            if ($statuses = $this->args['statuses']) {
                $statuses = (array) $statuses;
                $statuses = array_intersect($statuses, fluentcrm_subscriber_statuses());

                $subscribersQuery->filterByStatues($statuses);
            }

            if ($sms_statuses = $this->args['sms_statuses']) {
                $sms_statuses = (array) $sms_statuses;
                $subscribersQuery->where(function ($query) use ($sms_statuses) {
                    foreach ($sms_statuses as $sms_status) {
                        $query->orWhere('sms_status', $sms_status);
                    }
                });
            }
        }

        if ($this->args['has_commerce']) {
            /**
             * Filter the commerce provider for quering contacts in FluentCRM.
             *
             * This filter allows you to modify the commerce provider used in the Contact Query.
             *
             * @since 2.5.1
             *
             * @param string The commerce provider.
             */
            $commerceProvider = apply_filters('fluentcrm_commerce_provider', '');
            if ($commerceProvider) {
                $subscribersQuery->with(['commerce_by_provider' => function ($query) use ($commerceProvider) {
                    $query->where('provider', $commerceProvider);
                }]);
            }
        }

        if ($this->args['contact_status']) {
            $subscribersQuery->where('status', $this->args['contact_status']);
        }

        // Plain column filter, applied outside the simple/advanced branch so it
        // composes with either — callers may pass it alongside tags/lists or
        // alongside advanced filter groups.
        if ($this->args['contact_type']) {
            $subscribersQuery->where('contact_type', $this->args['contact_type']);
        }

        if ($this->args['company_ids']) {
            $subscribersQuery->filterByCompanies($this->args['company_ids']);
        }

        if ($this->args['contact_ids'] && is_array($this->args['contact_ids']) && !empty($this->args['contact_ids'])) {
            $subscribersQuery->whereIn('id', $this->args['contact_ids']);
        }

        // Direct address lookup — "give me the contacts for these emails".
        // Applied outside the simple/advanced branch (same as contact_ids), so
        // it narrows either branch rather than replacing it. Matching follows
        // the column collation, which is case-insensitive by default.
        if ($this->args['emails'] && is_array($this->args['emails'])) {
            $subscribersQuery->whereIn('email', $this->args['emails']);
        }

        $this->model = $subscribersQuery;
    }

    public function get()
    {
        $subscriberModel = $this->model;

        if ($limit = $this->args['limit']) {
            $subscriberModel = $subscriberModel->limit($limit);
        }

        if ($offset = $this->args['offset']) {
            $subscriberModel = $subscriberModel->offset($offset);
        }

        return $this->returnSubscribers($subscriberModel->get());
    }

    public function paginate()
    {
        return $this->returnSubscribers($this->model->paginate());
    }

    public function getModel()
    {
        return $this->model;
    }

    private function returnSubscribers($subscribers)
    {
        if ($this->args['custom_fields']) {
            // One meta query for the whole page — the per-subscriber
            // $subscriber->custom_fields() call is an N+1 (one query per row).
            $customFields = fluentcrm_get_custom_contact_fields();

            $keys = [];
            if ($customFields && is_array($customFields)) {
                $keys = array_map(function ($item) {
                    return $item['slug'];
                }, $customFields);
            }

            $subscriberIds = [];
            foreach ($subscribers as $subscriber) {
                $subscriberIds[] = $subscriber->id;
            }

            $valuesBySubscriber = [];
            if ($keys && $subscriberIds) {
                $metaRows = SubscriberMeta::where('object_type', 'custom_field')
                    ->whereIn('subscriber_id', $subscriberIds)
                    ->whereIn('key', $keys)
                    ->get();

                foreach ($metaRows as $metaRow) {
                    $valuesBySubscriber[$metaRow->subscriber_id][$metaRow->key] = apply_filters('fluent_crm/modify_custom_field_value', $metaRow->value);
                }
            }

            foreach ($subscribers as $subscriber) {
                $subscriber->custom_fields = isset($valuesBySubscriber[$subscriber->id]) ? $valuesBySubscriber[$subscriber->id] : [];
            }
        }

        return $subscribers;
    }

    private function formatAdvancedFilters()
    {
        $filters = $this->args['filters_groups_raw'];

        $groups = [];

        foreach ($filters as $filterGroup) {
            $group = [];
            foreach ($filterGroup as $filterItem) {
                if (count($filterItem['source']) != 2 || empty($filterItem['source'][0]) || empty($filterItem['source'][1]) || empty($filterItem['operator'])) {
                    continue;
                }
                $provider = $filterItem['source'][0];

                if (!isset($group[$provider])) {
                    $group[$provider] = [];
                }

                $property = $filterItem['source'][1];

                if ($property == 'purchased_groups') {
                    $property = 'purchased_items';
                }

                $group[$provider][] = [
                    'property'    => $property,
                    'operator'    => $filterItem['operator'],
                    'value'       => Arr::get($filterItem, 'value', ''),
                    'extra_value' => Arr::get($filterItem, 'extra_value')
                ];
            }

            if ($group) {
                $groups[] = $group;
            }
        }

        $this->args['filters_groups'] = $groups;
    }

}
