<?php

declare(strict_types=1);

namespace Engelsystem\Controllers\Metrics;

use Carbon\Carbon;
use Engelsystem\Database\Database;
use Engelsystem\Models\EventConfig;
use Engelsystem\Models\Faq;
use Engelsystem\Models\LogEntry;
use Engelsystem\Models\Message;
use Engelsystem\Models\News;
use Engelsystem\Models\NewsComment;
use Engelsystem\Models\OAuth;
use Engelsystem\Models\Question;
use Engelsystem\Models\Room;
use Engelsystem\Models\Shifts\Shift;
use Engelsystem\Models\User\License;
use Engelsystem\Models\User\PasswordReset;
use Engelsystem\Models\User\PersonalData;
use Engelsystem\Models\User\Settings;
use Engelsystem\Models\User\State;
use Engelsystem\Models\User\User;
use Engelsystem\Models\Worklog;
use Illuminate\Contracts\Database\Query\Builder as BuilderContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\Expression as QueryExpression;
use Illuminate\Support\Collection;

class Stats
{
    public function __construct(protected Database $db)
    {
    }

    /**
     * The number of not arrived users
     *
     * @param bool|null $working
     */
    public function arrivedUsers(bool $working = null): int
    {
        $query = State::whereArrived(true);

        if (!is_null($working)) {
            $query
                ->leftJoin('worklogs', 'worklogs.user_id', '=', 'users_state.user_id')
                ->leftJoin('shift_entries', 'shift_entries.user_id', '=', 'users_state.user_id')
                ->distinct();

            $query->where(function ($query) use ($working): void {
                /** @var QueryBuilder $query */
                if ($working) {
                    $query
                        ->whereNotNull('shift_entries.shift_id')
                        ->orWhereNotNull('worklogs.hours');

                    return;
                }

                $query
                    ->whereNull('shift_entries.shift_id')
                    ->whereNull('worklogs.hours');
            });
        }

        return $query->count('users_state.user_id');
    }

    /**
     * The number of not arrived users
     */
    public function newUsers(): int
    {
        return State::whereArrived(false)->count();
    }

    public function forceActiveUsers(): int
    {
        return State::whereForceActive(true)->count();
    }

    public function usersPronouns(): int
    {
        return PersonalData::where('pronoun', '!=', '')->count();
    }

    public function email(string $type): int
    {
        return match ($type) {
            'system' => Settings::whereEmailShiftinfo(true)->count(),
            'humans' => Settings::whereEmailHuman(true)->count(),
            'goody'  => Settings::whereEmailGoody(true)->count(),
            'news'   => Settings::whereEmailNews(true)->count(),
            default  => 0,
        };
    }

    /**
     * The number of currently working users
     *
     * @param bool|null $freeloaded
     */
    public function currentlyWorkingUsers(bool $freeloaded = null): int
    {
        $query = User::query()
            ->join('shift_entries', 'shift_entries.user_id', '=', 'users.id')
            ->join('shifts', 'shifts.id', '=', 'shift_entries.shift_id')
            ->where('shifts.start', '<=', Carbon::now())
            ->where('shifts.end', '>', Carbon::now());

        if (!is_null($freeloaded)) {
            $query->where('shift_entries.freeloaded', '=', $freeloaded);
        }

        return $query->count();
    }

    protected function vouchersQuery(): Builder
    {
        return State::query();
    }

    public function vouchers(): int
    {
        return (int) $this->vouchersQuery()->sum('got_voucher');
    }

    public function vouchersBuckets(array $buckets): array
    {
        $return = [];
        foreach ($buckets as $bucket) {
            $query = $this->vouchersQuery();

            if ($bucket !== '+Inf') {
                $query->where('got_voucher', '<=', $bucket);
            }

            $return[$bucket] = $query->count('got_voucher');
        }

        return $return;
    }

    public function tshirts(): int
    {
        return State::whereGotShirt(true)->count();
    }

    public function tshirtSizes(): Collection
    {
        return PersonalData::query()
            ->select(['shirt_size', $this->raw('COUNT(shirt_size) AS count')])
            ->whereNotNull('shirt_size')
            ->groupBy(['shirt_size'])
            ->get();
    }

    public function languages(): Collection
    {
        return Settings::query()
            ->select(['language', $this->raw('COUNT(language) AS count')])
            ->groupBy(['language'])
            ->get();
    }

    public function themes(): Collection
    {
        return Settings::query()
            ->select(['theme', $this->raw('COUNT(theme) AS count')])
            ->groupBy(['theme'])
            ->get();
    }

    /**
     * @param string|null $vehicle
     */
    public function licenses(string $vehicle): int
    {
        $mapping = [
            'has_car'  => 'has_car',
            'forklift' => 'drive_forklift',
            'car'      => 'drive_car',
            '3.5t'     => 'drive_3_5t',
            '7.5t'     => 'drive_7_5t',
            '12t'      => 'drive_12t',
        ];

        $query = (new License())
            ->getQuery()
            ->where($mapping[$vehicle], true);

        return $query->count();
    }

    /**
     * @param bool|null $done
     * @param bool|null $freeloaded
     *
     * @codeCoverageIgnore because it is only used in functions that use TIMESTAMPDIFF
     */
    protected function workSecondsQuery(bool $done = null, bool $freeloaded = null): QueryBuilder
    {
        $query = $this
            ->getQuery('shift_entries')
            ->join('shifts', 'shifts.id', '=', 'shift_entries.shift_id');

        if (!is_null($freeloaded)) {
            $query->where('freeloaded', '=', $freeloaded);
        }

        if (!is_null($done)) {
            $query->where('end', ($done ? '<' : '>='), Carbon::now());
        }

        return $query;
    }

    /**
     * The amount of worked seconds
     *
     * @param bool|null $done
     * @param bool|null $freeloaded
     *
     * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite
     */
    public function workSeconds(bool $done = null, bool $freeloaded = null): int
    {
        $query = $this->workSecondsQuery($done, $freeloaded);

        return (int) $query->sum($this->raw('TIMESTAMPDIFF(MINUTE, start, end) * 60'));
    }

    /**
     * The number of worked shifts
     *
     * @param bool|null $done
     * @param bool|null $freeloaded
     *
     * @codeCoverageIgnore as TIMESTAMPDIFF is not implemented in SQLite
     */
    public function workBuckets(array $buckets, bool $done = null, bool $freeloaded = null): array
    {
        return $this->getBuckets(
            $buckets,
            $this->workSecondsQuery($done, $freeloaded),
            'user_id',
            'SUM(TIMESTAMPDIFF(MINUTE, start, end) * 60)',
            'SUM(TIMESTAMPDIFF(MINUTE, start, end) * 60)'
        );
    }

    protected function getBuckets(
        array $buckets,
        BuilderContract $basicQuery,
        string $groupBy,
        string $having,
        string $count
    ): array {
        $return = [];

        foreach ($buckets as $bucket) {
            $query = clone $basicQuery;
            $query->groupBy($groupBy);

            if ($bucket !== '+Inf') {
                $query->having($this->raw($having), '<=', $bucket);
            }

            $return[$bucket] = count($query->get($this->raw($count)));
        }

        return $return;
    }

    public function worklogSeconds(): int
    {
        return (int) Worklog::query()
            ->sum($this->raw('hours * 60 * 60'));
    }

    public function worklogBuckets(array $buckets): array
    {
        return $this->getBuckets(
            $buckets,
            Worklog::query(),
            'user_id',
            'SUM(hours * 60 * 60)',
            'SUM(hours * 60 * 60)'
        );
    }

    public function rooms(): int
    {
        return Room::query()
            ->count();
    }

    public function shifts(): int
    {
        return Shift::count();
    }

    /**
     * @param bool|null $meeting
     */
    public function announcements(bool $meeting = null): int
    {
        $query = is_null($meeting) ? News::query() : News::whereIsMeeting($meeting);

        return $query->count();
    }

    public function comments(): int
    {
        return NewsComment::query()
            ->count();
    }

    /**
     * @param bool|null $answered
     */
    public function questions(bool $answered = null): int
    {
        $query = Question::query();
        if (!is_null($answered)) {
            if ($answered) {
                $query->whereNotNull('answerer_id');
            } else {
                $query->whereNull('answerer_id');
            }
        }

        return $query->count();
    }

    public function faq(): int
    {
        return Faq::query()->count();
    }

    public function messages(): int
    {
        return Message::query()->count();
    }

    public function sessions(): int
    {
        return $this
            ->getQuery('sessions')
            ->count();
    }

    public function oauth(): Collection
    {
        return OAuth::query()
            ->select(['provider', $this->raw('COUNT(provider) AS count')])
            ->groupBy(['provider'])
            ->get();
    }

    public function databaseRead(): float
    {
        $start = microtime(true);

        (new EventConfig())->findOrNew('last_metrics');

        return microtime(true) - $start;
    }

    public function databaseWrite(): float
    {
        $config = (new EventConfig())->findOrNew('last_metrics');
        $config
            ->setAttribute('name', 'last_metrics')
            ->setAttribute('value', new Carbon());

        $start = microtime(true);

        $config->save();

        return microtime(true) - $start;
    }

    /**
     * @param string|null $level
     */
    public function logEntries(string $level = null): int
    {
        $query = is_null($level) ? LogEntry::query() : LogEntry::whereLevel($level);

        return $query->count();
    }

    public function passwordResets(): int
    {
        return PasswordReset::query()->count();
    }

    protected function getQuery(string $table): QueryBuilder
    {
        return $this->db
            ->getConnection()
            ->table($table);
    }

    protected function raw(mixed $value): QueryExpression
    {
        return $this->db->getConnection()->raw($value);
    }
}