<?php

declare(strict_types=1);

namespace App\Service;

use App\Enums\TagMatchType;
use App\Enums\TimeEntryType;
use App\Models\Member;
use App\Models\TimeEntry;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;

class TimeEntryFilter
{
    public const string NONE_VALUE = 'none';

    /**
     * @var Builder<TimeEntry>
     */
    private Builder $builder;

    /**
     * @param  Builder<TimeEntry>  $builder
     */
    public function __construct(Builder $builder)
    {
        $this->builder = $builder;
    }

    public function addEndFilter(?string $dateTime): self
    {
        if ($dateTime === null) {
            return $this;
        }
        $this->addEnd(Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $dateTime, 'UTC'));

        return $this;
    }

    public function addEnd(?Carbon $end): self
    {
        if ($end === null) {
            return $this;
        }
        $this->builder->where('start', '<', $end);

        return $this;
    }

    public function addStartFilter(?string $dateTime): self
    {
        if ($dateTime === null) {
            return $this;
        }
        $this->addStart(Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $dateTime, 'UTC'));

        return $this;
    }

    public function addStart(?Carbon $start): self
    {
        if ($start === null) {
            return $this;
        }
        $this->builder->where('start', '>=', $start);

        return $this;
    }

    public function addActiveFilter(?string $active): self
    {
        if ($active === null) {
            return $this;
        }
        if ($active === 'true') {
            $this->addActive(true);
        } elseif ($active === 'false') {
            $this->addActive(false);
        } else {
            Log::warning('Invalid active filter value', ['value' => $active]);
        }

        return $this;
    }

    public function addActive(?bool $active): self
    {
        if ($active) {
            $this->builder->whereNull('end');
        } else {
            $this->builder->whereNotNull('end');
        }

        return $this;
    }

    public function addMemberIdFilter(?Member $member): self
    {
        if ($member === null) {
            return $this;
        }
        $this->builder->where('member_id', $member->getKey());

        return $this;
    }

    /**
     * @param  array<string>|null  $memberIds
     */
    public function addMemberIdsFilter(?array $memberIds): self
    {
        if ($memberIds === null) {
            return $this;
        }
        $this->builder->whereIn('member_id', $memberIds);

        return $this;
    }

    public function addBillableFilter(?string $billable): self
    {
        if ($billable === null) {
            return $this;
        }
        if ($billable === 'true') {
            $this->addBillable(true);
        } elseif ($billable === 'false') {
            $this->addBillable(false);
        } else {
            Log::warning('Invalid billable filter value', ['value' => $billable]);
        }

        return $this;
    }

    public function addBillable(?bool $billable): self
    {
        if ($billable === null) {
            return $this;
        }
        $this->builder->where('billable', '=', $billable);

        return $this;
    }

    public function addTypeFilter(?string $type): self
    {
        if ($type === null) {
            return $this;
        }
        $typeEnum = TimeEntryType::tryFrom($type);
        if ($typeEnum === null) {
            Log::warning('Invalid type filter value', ['value' => $type]);

            return $this;
        }
        $this->addType($typeEnum);

        return $this;
    }

    public function addType(?TimeEntryType $type): self
    {
        if ($type === null) {
            return $this;
        }
        $this->builder->where('type', '=', $type->value);

        return $this;
    }

    /**
     * @param  array<string>|null  $clientIds
     */
    public function addClientIdsFilter(?array $clientIds): self
    {
        if ($clientIds === null) {
            return $this;
        }
        $includeNone = in_array(self::NONE_VALUE, $clientIds, true);
        $clientIds = array_values(array_filter($clientIds, fn (string $id): bool => $id !== self::NONE_VALUE));

        $this->builder->where(function (Builder $builder) use ($clientIds, $includeNone): void {
            if (count($clientIds) > 0) {
                $builder->whereIn('client_id', $clientIds);
            }
            if ($includeNone) {
                $builder->orWhereNull('client_id');
            }
        });

        return $this;
    }

    /**
     * @param  array<string>|null  $projectIds
     */
    public function addProjectIdsFilter(?array $projectIds): self
    {
        if ($projectIds === null) {
            return $this;
        }
        $includeNone = in_array(self::NONE_VALUE, $projectIds, true);
        $projectIds = array_values(array_filter($projectIds, fn (string $id): bool => $id !== self::NONE_VALUE));

        $this->builder->where(function (Builder $builder) use ($projectIds, $includeNone): void {
            if (count($projectIds) > 0) {
                $builder->whereIn('project_id', $projectIds);
            }
            if ($includeNone) {
                $builder->orWhereNull('project_id');
            }
        });

        return $this;
    }

    /**
     * @param  array<string>|null  $tagIds
     */
    public function addTagIdsFilter(?array $tagIds, ?TagMatchType $tagMatchType = TagMatchType::Contains): self
    {
        if ($tagIds === null) {
            return $this;
        }
        $tagMatchType ??= TagMatchType::Contains;
        $includeNone = in_array(self::NONE_VALUE, $tagIds, true);
        $tagIds = array_values(array_filter($tagIds, fn (string $id): bool => $id !== self::NONE_VALUE));
        // An empty selection (no tag IDs and not filtering for "none") is no constraint, so apply nothing.
        // This also prevents the not-contains branch from collapsing into "only entries with null tags".
        if (count($tagIds) === 0 && ! $includeNone) {
            return $this;
        }

        $tagCondition = function (Builder $builder) use ($tagIds, $includeNone): void {
            foreach ($tagIds as $tagId) {
                $builder->orWhereJsonContains('tags', $tagId);
            }
            if ($includeNone) {
                $builder->orWhere(function (Builder $query): void {
                    $query->whereJsonLength('tags', 0)->orWhereNull('tags');
                });
            }
        };

        if ($tagMatchType === TagMatchType::NotContains) {
            $this->builder->where(function (Builder $builder) use ($tagCondition, $includeNone): void {
                $builder->whereNot($tagCondition);
                if (! $includeNone) {
                    $builder->orWhereNull('tags');
                }
            });
        } else {
            $this->builder->where($tagCondition);
        }

        return $this;
    }

    /**
     * @param  array<string>|null  $taskIds
     */
    public function addTaskIdsFilter(?array $taskIds): self
    {
        if ($taskIds === null) {
            return $this;
        }
        $includeNone = in_array(self::NONE_VALUE, $taskIds, true);
        $taskIds = array_values(array_filter($taskIds, fn (string $id): bool => $id !== self::NONE_VALUE));

        $this->builder->where(function (Builder $builder) use ($taskIds, $includeNone): void {
            if (count($taskIds) > 0) {
                $builder->whereIn('task_id', $taskIds);
            }
            if ($includeNone) {
                $builder->orWhereNull('task_id');
            }
        });

        return $this;
    }

    /**
     * @return Builder<TimeEntry>
     */
    public function get(): Builder
    {
        return $this->builder;
    }
}
