<?php

namespace App\Services;

use App\Services\UserStatsService;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Schema;
use Throwable;

/**
 * ArtworkStatsService
 *
 * Responsibilities:
 * - Increment views and downloads using DB transactions
 * - Optionally defer increments into Redis for async processing
 * - Provide a processor to drain queued deltas (job-friendly)
 */
class ArtworkStatsService
{
    protected string $redisKey = 'artwork_stats:deltas';

    /**
     * Increment views for an artwork.
     * Set $defer=true to push to Redis for async processing when available.
     * Both all-time (views) and windowed (views_24h, views_7d) are updated.
     */
    public function incrementViews(int $artworkId, int $by = 1, bool $defer = false): void
    {
        if ($defer && $this->redisAvailable()) {
            $this->pushDelta($artworkId, 'views',     $by);
            $this->pushDelta($artworkId, 'views_24h', $by);
            $this->pushDelta($artworkId, 'views_7d',  $by);
            return;
        }
        $this->applyDelta($artworkId, ['views' => $by, 'views_24h' => $by, 'views_7d' => $by]);
    }

    /**
     * Increment downloads for an artwork.
     * Both all-time (downloads) and windowed (downloads_24h, downloads_7d) are updated.
     */
    public function incrementDownloads(int $artworkId, int $by = 1, bool $defer = false): void
    {
        if ($defer && $this->redisAvailable()) {
            $this->pushDelta($artworkId, 'downloads',     $by);
            $this->pushDelta($artworkId, 'downloads_24h', $by);
            $this->pushDelta($artworkId, 'downloads_7d',  $by);
            return;
        }
        $this->applyDelta($artworkId, ['downloads' => $by, 'downloads_24h' => $by, 'downloads_7d' => $by]);
    }

    /**
     * Write one row to artwork_view_events (the persistent event log).
     *
     * Called from ArtworkViewController after session dedup passes.
     * Guests (unauthenticated) are recorded with user_id = null.
     * Rows are pruned after 90 days by skinbase:prune-view-events.
     */
    public function logViewEvent(int $artworkId, ?int $userId): void
    {
        try {
            DB::table('artwork_view_events')->insert([
                'artwork_id' => $artworkId,
                'user_id'    => $userId,
                'viewed_at'  => now(),
            ]);
        } catch (Throwable $e) {
            Log::warning('Failed to write artwork_view_events row', [
                'artwork_id' => $artworkId,
                'user_id'    => $userId,
                'error'      => $e->getMessage(),
            ]);
        }
    }

    /**
     * Increment views using an Artwork model.
     */
    public function incrementViewsForArtwork(\App\Models\Artwork $artwork, int $by = 1, bool $defer = true): void
    {
        $this->incrementViews((int) $artwork->id, $by, $defer);
    }

    /**
     * Increment downloads using an Artwork model.
     */
    public function incrementDownloadsForArtwork(\App\Models\Artwork $artwork, int $by = 1, bool $defer = true): void
    {
        $this->incrementDownloads((int) $artwork->id, $by, $defer);
    }

    /**
     * Recompute denormalized engagement counters from their source tables.
     *
     * This keeps single-artwork analytics fresh after favourites, likes,
     * comments, and shares without waiting for scheduled ranking jobs.
     */
    public function syncEngagementCounts(int $artworkId): void
    {
        if (! Schema::hasTable('artwork_stats')) {
            return;
        }

        try {
            $payload = [
                'favorites' => Schema::hasTable('artwork_favourites')
                    ? (int) DB::table('artwork_favourites')->where('artwork_id', $artworkId)->count()
                    : 0,
                'rating_count' => Schema::hasTable('artwork_likes')
                    ? (int) DB::table('artwork_likes')->where('artwork_id', $artworkId)->count()
                    : 0,
            ];

            if (Schema::hasColumn('artwork_stats', 'comments_count')) {
                $payload['comments_count'] = Schema::hasTable('artwork_comments')
                    ? (int) DB::table('artwork_comments')
                        ->where('artwork_id', $artworkId)
                        ->whereNull('deleted_at')
                        ->count()
                    : 0;
            }

            if (Schema::hasColumn('artwork_stats', 'shares_count')) {
                $payload['shares_count'] = Schema::hasTable('artwork_shares')
                    ? (int) DB::table('artwork_shares')->where('artwork_id', $artworkId)->count()
                    : 0;
            }

            DB::table('artwork_stats')->updateOrInsert(
                ['artwork_id' => $artworkId],
                $payload
            );
        } catch (Throwable $e) {
            Log::warning('Failed to sync artwork engagement counts', [
                'artwork_id' => $artworkId,
                'error' => $e->getMessage(),
            ]);
        }
    }

    /**
     * Apply a set of deltas to the artwork_stats row inside a transaction.
     * After updating artwork-level stats, forwards view/download counts to
     * UserStatsService so creator-level counters stay current.
     *
     * @param int $artworkId
     * @param array<string,int> $deltas
     */
    public function applyDelta(int $artworkId, array $deltas): void
    {
        try {
            DB::transaction(function () use ($artworkId, $deltas) {
                // Ensure a stats row exists — insert default zeros if missing.
                DB::table('artwork_stats')->insertOrIgnore([
                    'artwork_id'    => $artworkId,
                    'views'         => 0,
                    'views_24h'     => 0,
                    'views_7d'      => 0,
                    'downloads'     => 0,
                    'downloads_24h' => 0,
                    'downloads_7d'  => 0,
                    'favorites'     => 0,
                    'rating_avg'    => 0,
                    'rating_count'  => 0,
                ]);

                foreach ($deltas as $column => $value) {
                    // Only allow known columns to avoid SQL injection.
                    if (! in_array($column, ['views', 'views_24h', 'views_7d', 'downloads', 'downloads_24h', 'downloads_7d', 'favorites', 'rating_count'], true)) {
                        continue;
                    }

                    DB::table('artwork_stats')
                        ->where('artwork_id', $artworkId)
                        ->increment($column, (int) $value);
                }
            });

            // Forward creator-level counters outside the transaction.
            $this->forwardCreatorStats($artworkId, $deltas);
        } catch (Throwable $e) {
            Log::error('Failed to apply artwork stats delta', [
                'artwork_id' => $artworkId,
                'deltas'     => $deltas,
                'error'      => $e->getMessage(),
            ]);
        }
    }

    /**
     * After applying artwork-level deltas, forward relevant totals to the
     * creator's user_statistics row via UserStatsService.
     * Views skip Meilisearch reindex (high frequency — covered by recompute).
     *
     * @param int $artworkId
     * @param array<string,int> $deltas
     */
    protected function forwardCreatorStats(int $artworkId, array $deltas): void
    {
        $viewDelta     = (int) ($deltas['views'] ?? 0);
        $downloadDelta = (int) ($deltas['downloads'] ?? 0);

        if ($viewDelta <= 0 && $downloadDelta <= 0) {
            return;
        }

        try {
            $creatorId = (int) DB::table('artworks')->where('id', $artworkId)->value('user_id');
            if (! $creatorId) {
                return;
            }

            /** @var UserStatsService $svc */
            $svc = app(UserStatsService::class);

            if ($viewDelta > 0) {
                // High-frequency: increment counter but skip Meilisearch reindex.
                $svc->incrementArtworkViewsReceived($creatorId, $viewDelta);
            }

            if ($downloadDelta > 0) {
                $svc->incrementDownloadsReceived($creatorId, $downloadDelta);
            }
        } catch (Throwable $e) {
            Log::warning('Failed to forward creator stats from artwork delta', [
                'artwork_id' => $artworkId,
                'error'      => $e->getMessage(),
            ]);
        }
    }

    /**
     * Push a delta to Redis queue for async processing.
     */
    protected function pushDelta(int $artworkId, string $field, int $value): void
    {
        $payload = json_encode([
            'artwork_id' => $artworkId,
            'field'      => $field,
            'value'      => $value,
            'ts'         => time(),
        ]);

        try {
            Redis::rpush($this->redisKey, $payload);
        } catch (Throwable $e) {
            // If Redis is unavailable, fall back to immediate apply to avoid data loss.
            Log::warning('Redis unavailable for artwork stats; applying immediately', [
                'error' => $e->getMessage(),
            ]);
            $this->applyDelta($artworkId, [$field => $value]);
        }
    }

    /**
     * Drain and apply queued deltas from Redis. Returns number processed.
     * Designed to be invoked by a queued job or artisan command.
     */
    public function processPendingFromRedis(int $max = 1000): int
    {
        if (! $this->redisAvailable()) {
            return 0;
        }

        $processed = 0;

        try {
            while ($processed < $max) {
                $item = Redis::lpop($this->redisKey);
                if (! $item) {
                    break;
                }

                $decoded = json_decode($item, true);
                if (! is_array($decoded) || empty($decoded['artwork_id']) || empty($decoded['field'])) {
                    continue;
                }

                $this->applyDelta((int) $decoded['artwork_id'], [$decoded['field'] => (int) ($decoded['value'] ?? 1)]);
                $processed++;
            }
        } catch (Throwable $e) {
            Log::error('Error while processing artwork stats from Redis', ['error' => $e->getMessage()]);
        }

        return $processed;
    }

    protected function redisAvailable(): bool
    {
        try {
            $pong = Redis::connection()->ping();
            return (bool) $pong;
        } catch (Throwable $e) {
            return false;
        }
    }
}
