<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Branch;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;

/**
 * Applies the GET /members filter set to a query. Shared by the list
 * endpoint, /members/count, and (Phase 7) export so filters can't drift.
 */
class MemberFilterService
{
    public function __construct(private readonly MemberSearchService $search) {}

    /**
     * @param  array<string, mixed>  $filters
     */
    public function apply(Builder $query, array $filters): Builder
    {
        if (! empty($filters['q'])) {
            $this->search->apply($query, (string) $filters['q']);
        }

        if (! empty($filters['branch_id'])) {
            $query->whereIn('branch_id', $this->toIdList($filters['branch_id']));
        }

        if (! empty($filters['region_id'])) {
            $branchIds = Branch::query()
                ->whereIn('region_id', $this->toIdList($filters['region_id']))
                ->pluck('id');
            $query->whereIn('branch_id', $branchIds);
        }

        if (! empty($filters['status'])) {
            $query->whereIn('membership_status', $this->toStringList($filters['status']));
        }

        if (! empty($filters['gender'])) {
            $query->where('gender', $filters['gender']);
        }

        if (! empty($filters['marital_status'])) {
            $query->where('marital_status', $filters['marital_status']);
        }

        if (! empty($filters['age_max'])) {
            $query->where('date_of_birth', '>=', Carbon::today()->subYears((int) $filters['age_max'] + 1)->addDay());
        }

        if (! empty($filters['age_min'])) {
            $query->where('date_of_birth', '<=', Carbon::today()->subYears((int) $filters['age_min']));
        }

        if (! empty($filters['household_id'])) {
            $query->where('household_id', $filters['household_id']);
        }

        if (! empty($filters['group_id'])) {
            $query->whereHas('groups', fn (Builder $q) => $q->where('groups.id', $filters['group_id']));
        }

        if (! empty($filters['joined_after'])) {
            $query->where('date_joined', '>=', $filters['joined_after']);
        }

        if (! empty($filters['joined_before'])) {
            $query->where('date_joined', '<=', $filters['joined_before']);
        }

        if (! empty($filters['birth_month'])) {
            // whereMonth() wraps date_of_birth in MONTH(...), which is not
            // sargable and forces a full scan. The generated
            // date_of_birth_month_day column + a literal LIKE prefix keeps
            // this on an index (see docs/02-database-schema.md).
            $month = str_pad((string) (int) $filters['birth_month'], 2, '0', STR_PAD_LEFT);
            $query->where('date_of_birth_month_day', 'like', "{$month}-%");
        }

        if (! empty($filters['include_deleted'])) {
            $query->withTrashed();
        }

        return $query;
    }

    /**
     * Every sort ends with an `id` tiebreak so keyset pagination is stable.
     */
    public function applySort(Builder $query, ?string $sort, string $direction = 'asc'): Builder
    {
        $direction = $direction === 'desc' ? 'desc' : 'asc';

        return match ($sort) {
            'date_joined' => $query->orderBy('date_joined', $direction)->orderBy('id', $direction),
            'created_at' => $query->orderBy('created_at', $direction)->orderBy('id', $direction),
            default => $query->orderBy('last_name', $direction)->orderBy('first_name', $direction)->orderBy('id', $direction),
        };
    }

    /**
     * @return array<int, int>
     */
    private function toIdList(mixed $value): array
    {
        if (is_array($value)) {
            return array_map('intval', $value);
        }

        return array_map('intval', explode(',', (string) $value));
    }

    /**
     * @return array<int, string>
     */
    private function toStringList(mixed $value): array
    {
        if (is_array($value)) {
            return $value;
        }

        return explode(',', (string) $value);
    }
}
