<?php

namespace App\Services\AdvancedFilter;

use App\Models\Campaign;
use App\Models\Contact;
use App\Models\ContactList;
use App\Models\Tag;
use Illuminate\Database\Eloquent\Builder;

/**
 * Turns a validated {groups:[{conditions:[...]}]} tree into an Eloquent
 * query. Groups are OR'd together; conditions within a group are AND'd.
 * Every field/operator/relation reference is checked against
 * FilterFieldRegistry before touching the query builder — nothing from the
 * tree is ever used directly as a column or relation name.
 */
class FilterQueryBuilder
{
    private const SUPPRESSED_STATUSES = ['unsubscribed', 'bounced', 'spam_reported'];

    public function __construct(private readonly int $userId)
    {
    }

    public function build(array $tree): Builder
    {
        $groups = $tree['groups'] ?? [];

        if (empty($groups)) {
            throw new InvalidFilterException('Filter has no groups.');
        }

        $query = Contact::query()
            ->where('user_id', $this->userId)
            ->whereNotIn('status', self::SUPPRESSED_STATUSES);

        $query->where(function (Builder $outer) use ($groups) {
            foreach ($groups as $group) {
                $conditions = $group['conditions'] ?? [];

                if (empty($conditions)) {
                    throw new InvalidFilterException('Empty condition group.');
                }

                $outer->orWhere(function (Builder $groupQuery) use ($conditions) {
                    foreach ($conditions as $condition) {
                        $this->applyCondition($groupQuery, $condition);
                    }
                });
            }
        });

        return $query;
    }

    private function applyCondition(Builder $q, array $condition): void
    {
        $key = $condition['field'] ?? null;
        $def = is_string($key) ? FilterFieldRegistry::get($key) : null;

        if (!$def) {
            throw new InvalidFilterException('Unknown field: '.(is_scalar($key) ? (string) $key : json_encode($key)));
        }

        $operator = $condition['operator'] ?? null;
        if (!is_string($operator) || !FilterFieldRegistry::isOperatorAllowed($def['type'], $operator)) {
            throw new InvalidFilterException("Operator '{$operator}' not allowed for '{$key}'");
        }

        $value = $condition['value'] ?? null;

        match ($def['type']) {
            'string'              => $this->applyString($q, $def['column'], $operator, $value),
            'date'                => $this->applyDate($q, $def['column'], $operator, $value),
            'enum'                => $this->applyEnum($q, $def['column'], $operator, $value, $def['allowed']),
            'relation'            => $this->applyRelation($q, $def['relation'], $operator, $value),
            'activity'            => $this->applyActivity($q, $def['event_type'], $operator, $value),
            'campaign_activity'   => $this->applyCampaignActivity($q, $operator, $condition),
            'automation_activity' => $this->applyAutomationActivity($q, $operator, $condition),
            default               => throw new InvalidFilterException("Unhandled field type: {$def['type']}"),
        };
    }

    private function applyString(Builder $q, string $column, string $operator, mixed $value): void
    {
        match ($operator) {
            'equals'       => $q->where($column, '=', (string) $value),
            'not_equals'   => $q->where($column, '!=', (string) $value),
            'contains'     => $q->where($column, 'like', '%'.self::escapeLike((string) $value).'%'),
            'not_contains' => $q->where(fn ($w) => $w->whereNull($column)->orWhere($column, 'not like', '%'.self::escapeLike((string) $value).'%')),
            'starts_with'  => $q->where($column, 'like', self::escapeLike((string) $value).'%'),
            'ends_with'    => $q->where($column, 'like', '%'.self::escapeLike((string) $value)),
            'is_empty'     => $q->where(fn ($w) => $w->whereNull($column)->orWhere($column, '')),
            'is_not_empty' => $q->whereNotNull($column)->where($column, '!=', ''),
            default        => throw new InvalidFilterException("Unhandled string operator: {$operator}"),
        };
    }

    private function applyDate(Builder $q, string $column, string $operator, mixed $value): void
    {
        match ($operator) {
            'equals'       => $q->whereDate($column, '=', self::requireDate($value)),
            'before'       => $q->whereDate($column, '<', self::requireDate($value)),
            'after'        => $q->whereDate($column, '>', self::requireDate($value)),
            'between'      => $this->applyBetweenDates($q, $column, $value),
            'is_empty'     => $q->whereNull($column),
            'is_not_empty' => $q->whereNotNull($column),
            default        => throw new InvalidFilterException("Unhandled date operator: {$operator}"),
        };
    }

    private function applyBetweenDates(Builder $q, string $column, mixed $value): void
    {
        if (!is_array($value) || count($value) !== 2) {
            throw new InvalidFilterException('between requires a [start, end] value.');
        }

        $q->whereDate($column, '>=', self::requireDate($value[0]))
            ->whereDate($column, '<=', self::requireDate($value[1]));
    }

    private function applyEnum(Builder $q, string $column, string $operator, mixed $value, array $allowed): void
    {
        match ($operator) {
            'is'         => $q->where($column, self::requireAllowed($value, $allowed)),
            'is_not'     => $q->where($column, '!=', self::requireAllowed($value, $allowed)),
            'is_any_of'  => $q->whereIn($column, self::requireAllowedList($value, $allowed)),
            'is_none_of' => $q->whereNotIn($column, self::requireAllowedList($value, $allowed)),
            default      => throw new InvalidFilterException("Unhandled enum operator: {$operator}"),
        };
    }

    private function applyRelation(Builder $q, string $relation, string $operator, mixed $value): void
    {
        $ids   = $this->tenantOwnedIds($relation, $value);
        $idCol = $relation === 'tags' ? 'tags.id' : 'lists.id';

        match ($operator) {
            'has_any'  => $q->whereHas($relation, fn ($r) => $r->whereIn($idCol, $ids)),
            'has_all'  => collect($ids)->each(fn ($id) => $q->whereHas($relation, fn ($r) => $r->where($idCol, $id))),
            'has_none' => $q->whereDoesntHave($relation, fn ($r) => $r->whereIn($idCol, $ids)),
            default    => throw new InvalidFilterException("Unhandled relation operator: {$operator}"),
        };
    }

    private function applyActivity(Builder $q, string $eventType, string $operator, mixed $value): void
    {
        match ($operator) {
            'within_days'     => $q->whereHas('campaignEvents', fn ($e) => $e->where('event_type', $eventType)->where('created_at', '>=', now()->subDays(self::requireDays($value)))),
            'not_within_days' => $q->whereDoesntHave('campaignEvents', fn ($e) => $e->where('event_type', $eventType)->where('created_at', '>=', now()->subDays(self::requireDays($value)))),
            'never'           => $q->whereDoesntHave('campaignEvents', fn ($e) => $e->where('event_type', $eventType)),
            default           => throw new InvalidFilterException("Unhandled activity operator: {$operator}"),
        };
    }

    private function applyCampaignActivity(Builder $q, string $operator, array $condition): void
    {
        $campaignId = $condition['campaign_id'] ?? null;

        if (!$campaignId || !Campaign::where('user_id', $this->userId)->whereIn('type', ['campaign', 'recurring', 'recurring_occurrence'])->where('id', $campaignId)->exists()) {
            throw new InvalidFilterException('Unknown or unauthorized campaign_id for campaign_email condition.');
        }

        $eventType = match ($operator) {
            'sent', 'not_sent'       => 'sent',
            'opened', 'not_opened'   => 'open',
            'clicked', 'not_clicked' => 'click',
            'bounced'                => 'bounced',
            'unsubscribed'           => 'unsubscribe',
            default                  => throw new InvalidFilterException("Unhandled campaign_email operator: {$operator}"),
        };

        $negated = str_starts_with($operator, 'not_');
        $clause  = fn ($e) => $e->where('campaign_id', $campaignId)->where('event_type', $eventType);

        $negated ? $q->whereDoesntHave('campaignEvents', $clause) : $q->whereHas('campaignEvents', $clause);
    }

    private function applyAutomationActivity(Builder $q, string $operator, array $condition): void
    {
        $automationId = $condition['automation_id'] ?? null;

        if (!$automationId || !Campaign::where('user_id', $this->userId)->whereIn('type', ['automation', 'sequence'])->where('id', $automationId)->exists()) {
            throw new InvalidFilterException('Unknown or unauthorized automation_id for automation condition.');
        }

        $eventType = match ($operator) {
            'enrolled', 'not_enrolled'   => 'enrolled',
            'completed', 'not_completed' => 'completed',
            default                      => throw new InvalidFilterException("Unhandled automation operator: {$operator}"),
        };

        $negated = str_starts_with($operator, 'not_');
        $clause  = fn ($e) => $e->where('campaign_id', $automationId)->where('event_type', $eventType);

        $negated ? $q->whereDoesntHave('campaignEvents', $clause) : $q->whereHas('campaignEvents', $clause);
    }

    private function tenantOwnedIds(string $relation, mixed $value): array
    {
        if (!is_array($value) || empty($value)) {
            throw new InvalidFilterException("'{$relation}' condition requires a non-empty array of IDs.");
        }

        $model = $relation === 'tags' ? Tag::class : ContactList::class;
        $ids   = $model::where('user_id', $this->userId)->whereIn('id', $value)->pluck('id')->all();

        if (empty($ids)) {
            throw new InvalidFilterException("None of the given '{$relation}' IDs belong to this account.");
        }

        return $ids;
    }

    private static function requireDate(mixed $value): string
    {
        if (!is_string($value) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
            throw new InvalidFilterException('Expected a YYYY-MM-DD date value.');
        }

        return $value;
    }

    private static function requireDays(mixed $value): int
    {
        if (!is_numeric($value) || (int) $value < 0) {
            throw new InvalidFilterException('Expected a non-negative number of days.');
        }

        return (int) $value;
    }

    private static function requireAllowed(mixed $value, array $allowed): string
    {
        if (!is_string($value) || !in_array($value, $allowed, true)) {
            throw new InvalidFilterException('Value not in allowed set: '.json_encode($value));
        }

        return $value;
    }

    private static function requireAllowedList(mixed $value, array $allowed): array
    {
        if (!is_array($value) || empty($value)) {
            throw new InvalidFilterException('Expected a non-empty array of values.');
        }

        foreach ($value as $v) {
            self::requireAllowed($v, $allowed);
        }

        return $value;
    }

    private static function escapeLike(string $value): string
    {
        return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value);
    }
}
