<?php

declare(strict_types=1);

namespace App\Services;

use App\Jobs\IndexUserJob;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;

/**
 * UserStatsService – single source of truth for user_statistics counters.
 *
 * All counter updates MUST go through this service.
 * No direct increments in controllers or jobs.
 *
 * Design:
 * - Atomic SQL increments (no read-modify-write races).
 * - Negative counters are prevented at the SQL level (WHERE col > 0).
 * - ensureRow() upserts the row before any counter touch.
 * - recomputeUser() rebuilds all columns from authoritative tables.
 */
final class UserStatsService
{
    // ─── Row management ──────────────────────────────────────────────────────

    /**
     * Guarantee a user_statistics row exists for the given user.
     * Safe to call before every increment.
     */
    public function ensureRow(int $userId): void
    {
        DB::table('user_statistics')->insertOrIgnore([
            'user_id'    => $userId,
            'created_at' => now(),
            'updated_at' => now(),
        ]);
    }

    // ─── Increment helpers ────────────────────────────────────────────────────

    public function incrementUploads(int $userId, int $by = 1): void
    {
        $this->ensureRow($userId);
        $this->inc($userId, 'uploads_count', $by);
        $this->touchActive($userId);
        $this->reindex($userId);
    }

    public function decrementUploads(int $userId, int $by = 1): void
    {
        $this->dec($userId, 'uploads_count', $by);
        $this->reindex($userId);
    }

    public function incrementDownloadsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'downloads_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function incrementArtworkViewsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'artwork_views_received_count', $by);
        // Views are high-frequency – do NOT reindex on every view.
    }

    public function incrementAwardsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'awards_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function decrementAwardsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->dec($creatorUserId, 'awards_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function incrementFavoritesReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'favorites_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function decrementFavoritesReceived(int $creatorUserId, int $by = 1): void
    {
        $this->dec($creatorUserId, 'favorites_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function incrementCommentsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'comments_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function decrementCommentsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->dec($creatorUserId, 'comments_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function incrementReactionsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->ensureRow($creatorUserId);
        $this->inc($creatorUserId, 'reactions_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function decrementReactionsReceived(int $creatorUserId, int $by = 1): void
    {
        $this->dec($creatorUserId, 'reactions_received_count', $by);
        $this->reindex($creatorUserId);
    }

    public function incrementProfileViews(int $userId, int $by = 1): void
    {
        $this->ensureRow($userId);
        $this->inc($userId, 'profile_views_count', $by);
    }

    // ─── Timestamp helpers ────────────────────────────────────────────────────

    public function setLastUploadAt(int $userId, ?Carbon $timestamp = null): void
    {
        $this->ensureRow($userId);
        DB::table('user_statistics')
            ->where('user_id', $userId)
            ->update([
                'last_upload_at' => ($timestamp ?? now())->toDateTimeString(),
                'updated_at'     => now(),
            ]);
    }

    public function setLastActiveAt(int $userId, ?Carbon $timestamp = null): void
    {
        $this->ensureRow($userId);
        DB::table('user_statistics')
            ->where('user_id', $userId)
            ->update([
                'last_active_at' => ($timestamp ?? now())->toDateTimeString(),
                'updated_at'     => now(),
            ]);
    }

    // ─── Recompute ────────────────────────────────────────────────────────────

    /**
     * Recompute all counters for a single user from authoritative tables.
     * Returns the computed values (array) without writing when $dryRun=true.
     *
     * @return array<string, int|string|null>
     */
    public function recomputeUser(int $userId, bool $dryRun = false): array
    {
        $computed = [
            'uploads_count' => (int) DB::table('artworks')
                ->where('user_id', $userId)
                ->whereNull('deleted_at')
                ->count(),

            'downloads_received_count' => (int) DB::table('artwork_downloads as d')
                ->join('artworks as a', 'a.id', '=', 'd.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->count(),

            'artwork_views_received_count' => (int) DB::table('artwork_stats as s')
                ->join('artworks as a', 'a.id', '=', 's.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->sum('s.views'),

            'awards_received_count' => (int) DB::table('artwork_medals as aw')
                ->join('artworks as a', 'a.id', '=', 'aw.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->count(),

            'favorites_received_count' => (int) DB::table('artwork_favourites as f')
                ->join('artworks as a', 'a.id', '=', 'f.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->count(),

            'comments_received_count' => (int) DB::table('artwork_comments as c')
                ->join('artworks as a', 'a.id', '=', 'c.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->whereNull('c.deleted_at')
                ->count(),

            'reactions_received_count' => (int) DB::table('artwork_reactions as r')
                ->join('artworks as a', 'a.id', '=', 'r.artwork_id')
                ->where('a.user_id', $userId)
                ->whereNull('a.deleted_at')
                ->count(),

            'followers_count' => (int) DB::table('user_followers')
                ->where('user_id', $userId)
                ->count(),

            'following_count' => (int) DB::table('user_followers')
                ->where('follower_id', $userId)
                ->count(),

            'last_upload_at' => DB::table('artworks')
                ->where('user_id', $userId)
                ->whereNull('deleted_at')
                ->max('created_at'),
        ];

        if (! $dryRun) {
            $this->ensureRow($userId);

            DB::table('user_statistics')
                ->where('user_id', $userId)
                ->update(array_merge($computed, ['updated_at' => now()]));

            $this->reindex($userId);
        }

        return $computed;
    }

    /**
     * Recompute stats for all users in chunks.
     *
     * @param int $chunk  Users per chunk.
     */
    public function recomputeAll(int $chunk = 1000): void
    {
        DB::table('users')
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->chunk($chunk, function ($users) {
                foreach ($users as $user) {
                    $this->recomputeUser($user->id);
                }
            });
    }

    // ─── Private helpers ──────────────────────────────────────────────────────

    private function inc(int $userId, string $column, int $by = 1): void
    {
        DB::table('user_statistics')
            ->where('user_id', $userId)
            ->update([
                $column      => $this->nonNegativeCounterExpression($column, $by),
                'updated_at' => now(),
            ]);
    }

    private function dec(int $userId, string $column, int $by = 1): void
    {
        DB::table('user_statistics')
            ->where('user_id', $userId)
            ->where($column, '>', 0)
            ->update([
                $column      => $this->nonNegativeCounterExpression($column, -$by),
                'updated_at' => now(),
            ]);
    }

    private function touchActive(int $userId): void
    {
        DB::table('user_statistics')
            ->where('user_id', $userId)
            ->update([
                'last_active_at' => now(),
                'updated_at'     => now(),
            ]);
    }

    private function nonNegativeCounterExpression(string $column, int $delta)
    {
        if (! preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $column)) {
            throw new InvalidArgumentException('Invalid statistics column name.');
        }

        $driver = DB::connection()->getDriverName();
        $deltaSql = $delta >= 0 ? "+ {$delta}" : "- ".abs($delta);

        if ($driver === 'sqlite') {
            return DB::raw("max(0, COALESCE({$column}, 0) {$deltaSql})");
        }

        return DB::raw("GREATEST(0, COALESCE({$column}, 0) {$deltaSql})");
    }

    /**
     * Queue a Meilisearch reindex for the user.
     * Uses IndexUserJob to avoid blocking the request.
     */
    private function reindex(int $userId): void
    {
        IndexUserJob::dispatch($userId);
    }
}
