<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\ArtworkVersion;
use App\Models\ArtworkVersionEvent;
use App\Models\User;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;

/**
 * ArtworkVersioningService
 *
 * Manages non-destructive file replacement for artworks.
 *
 * Guarantees:
 *  - All replacements create a new version row; originals are never deleted.
 *  - Engagement (views, favourites, downloads) is never reset.
 *  - Ranking scores receive a small protective decay on each replacement.
 *  - Abusive rapid replacement is blocked by rate limits.
 *  - Major visual changes trigger requires_reapproval flag.
 */
final class ArtworkVersioningService
{
    // ── Rate-limit thresholds ─────────────────────────────────────────────
    private const MAX_PER_HOUR  = 3;
    private const MAX_PER_DAY   = 10;

    // ── Reapproval: flag when dimension changes beyond this fraction ──────
    private const DIMENSION_CHANGE_THRESHOLD = 0.5; // 50 % change triggers re-approval

    // ── Ranking decay applied per replacement ─────────────────────────────
    private const RANKING_DECAY_FACTOR = 0.93; // 7 % decay

    // ──────────────────────────────────────────────────────────────────────

    /**
     * Create a new version for an artwork after a file replacement.
     *
     * This is the primary entry-point called by the controller.
     *
     * @param Artwork  $artwork     The artwork being updated.
     * @param string   $filePath    Relative path stored for this version.
     * @param string   $fileHash    SHA-256 hex hash of the new file.
     * @param int      $width       New file width in pixels.
     * @param int      $height      New file height in pixels.
     * @param int      $fileSize    New file size in bytes.
     * @param int      $userId      ID of the acting user (for audit log).
     * @param string|null $changeNote Optional user-supplied change note.
     *
     * @throws TooManyRequestsHttpException  When rate limit is exceeded.
     * @throws \RuntimeException            When the hash is identical to the current file.
     */
    public function createNewVersion(
        Artwork $artwork,
        string  $filePath,
        string  $fileHash,
        int     $width,
        int     $height,
        int     $fileSize,
        int     $userId,
        ?string $changeNote = null,
    ): ArtworkVersion {
        // 1. Rate limit check
        $this->rateLimitCheck($userId, $artwork->id);

        // 2. Reject identical file
        if ($artwork->hash === $fileHash) {
            throw new \RuntimeException('The uploaded file is identical to the current version. No new version created.');
        }

        return DB::transaction(function () use (
            $artwork, $filePath, $fileHash, $width, $height, $fileSize, $userId, $changeNote
        ): ArtworkVersion {
            // 3. Determine next version number
            $nextNumber = ($artwork->version_count ?? 1) + 1;

            // 4. Mark all previous versions as not current
            $artwork->versions()->update(['is_current' => false]);

            // 5. Insert new version row
            $version = ArtworkVersion::create([
                'artwork_id'     => $artwork->id,
                'version_number' => $nextNumber,
                'file_path'      => $filePath,
                'file_hash'      => $fileHash,
                'width'          => $width,
                'height'         => $height,
                'file_size'      => $fileSize,
                'change_note'    => $changeNote,
                'is_current'     => true,
            ]);

            // 6. Check whether moderation re-review is required
            $needsReapproval = $this->shouldRequireReapproval($artwork, $width, $height);

            // 7. Update artwork metadata (no engagement data touched)
            $artwork->update([
                'current_version_id' => $version->id,
                'version_count'      => $nextNumber,
                'version_updated_at' => now(),
                'requires_reapproval' => $needsReapproval,
            ]);

            // 8. Ranking protection — apply small decay
            $this->applyRankingProtection($artwork);

            // 9. Audit log
            ArtworkVersionEvent::create([
                'artwork_id' => $artwork->id,
                'user_id'    => $userId,
                'action'     => 'create_version',
                'version_id' => $version->id,
            ]);

            // 10. Increment hourly/daily counters for rate limiting
            $this->incrementRateLimitCounters($userId, $artwork->id);

            return $version;
        });
    }

    /**
     * Restore a previous version by cloning it as a new (current) version.
     *
     * The restored file is treated as a brand-new version so the history
     * remains strictly append-only and the version counter always increases.
     *
     * @throws TooManyRequestsHttpException  When rate limit is exceeded.
     */
    public function restoreVersion(
        ArtworkVersion $version,
        Artwork        $artwork,
        int            $userId,
    ): ArtworkVersion {
        return $this->createNewVersion(
            $artwork,
            $version->file_path,
            $version->file_hash,
            (int) $version->width,
            (int) $version->height,
            (int) $version->file_size,
            $userId,
            "Restored from version {$version->version_number}",
        );
    }

    /**
     * Decide whether the new file warrants a moderation re-check.
     *
     * Triggers when either dimension changes by more than the threshold.
     */
    public function shouldRequireReapproval(Artwork $artwork, int $newWidth, int $newHeight): bool
    {
        // First version upload — no existing dimensions to compare
        if (!$artwork->width || !$artwork->height) {
            return false;
        }

        $widthChange  = abs($newWidth  - $artwork->width)  / max($artwork->width, 1);
        $heightChange = abs($newHeight - $artwork->height) / max($artwork->height, 1);

        return $widthChange  > self::DIMENSION_CHANGE_THRESHOLD
            || $heightChange > self::DIMENSION_CHANGE_THRESHOLD;
    }

    /**
     * Apply a small protective decay (7 %) to ranking and heat scores.
     *
     * This prevents creators from gaming the ranking algorithm by rapidly
     * cycling file versions to refresh discovery signals.
     * Engagement totals (views, favourites, downloads) are NOT touched.
     */
    public function applyRankingProtection(Artwork $artwork): void
    {
        try {
            DB::table('artwork_stats')
                ->where('artwork_id', $artwork->id)
                ->update([
                    'ranking_score'       => DB::raw('ranking_score * ' . self::RANKING_DECAY_FACTOR),
                    'heat_score'          => DB::raw('heat_score * ' . self::RANKING_DECAY_FACTOR),
                    'engagement_velocity' => DB::raw('engagement_velocity * ' . self::RANKING_DECAY_FACTOR),
                ]);
        } catch (\Throwable $e) {
            // Non-fatal — log and continue so the version is still saved.
            Log::warning('ArtworkVersioningService: ranking protection failed', [
                'artwork_id' => $artwork->id,
                'error'      => $e->getMessage(),
            ]);
        }
    }

    /**
     * Throw TooManyRequestsHttpException when the user has exceeded either:
     *   • 3 replacements per hour for this artwork
     *   • 10 replacements per day  for this user (across all artworks)
     */
    public function rateLimitCheck(int $userId, int $artworkId): void
    {
        $hourKey  = "artwork_version:hour:{$userId}:{$artworkId}";
        $dayKey   = "artwork_version:day:{$userId}";

        $hourCount = (int) Cache::get($hourKey, 0);
        $dayCount  = (int) Cache::get($dayKey,  0);

        if ($hourCount >= self::MAX_PER_HOUR) {
            throw new TooManyRequestsHttpException(
                3600,
                'You have replaced this artwork too many times in the last hour. Please wait before trying again.'
            );
        }

        if ($dayCount >= self::MAX_PER_DAY) {
            throw new TooManyRequestsHttpException(
                86400,
                'You have reached the daily replacement limit. Please wait until tomorrow.'
            );
        }
    }

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

    private function incrementRateLimitCounters(int $userId, int $artworkId): void
    {
        $hourKey = "artwork_version:hour:{$userId}:{$artworkId}";
        $dayKey  = "artwork_version:day:{$userId}";

        // Hourly counter — expires in 1 hour
        if (Cache::has($hourKey)) {
            Cache::increment($hourKey);
        } else {
            Cache::put($hourKey, 1, 3600);
        }

        // Daily counter — expires at midnight (or 24 hours from first hit)
        if (Cache::has($dayKey)) {
            Cache::increment($dayKey);
        } else {
            Cache::put($dayKey, 1, 86400);
        }
    }
}
