<?php

declare(strict_types=1);

namespace App\Services;

use App\Models\Artwork;
use App\Models\ArtworkVersion;
use App\Models\ArtworkVersionEvent;
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

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

    /**
     * Capture the current artwork media state as a revision snapshot.
     *
     * @return array{artwork: array<string, mixed>, files: array<int, array<string, mixed>>}
     */
    public function captureArtworkSnapshot(Artwork $artwork): array
    {
        return [
            'artwork' => [
                'file_name' => (string) ($artwork->file_name ?? ''),
                'file_path' => (string) ($artwork->file_path ?? ''),
                'hash' => (string) ($artwork->hash ?? ''),
                'file_ext' => (string) ($artwork->file_ext ?? ''),
                'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
                'file_size' => (int) ($artwork->file_size ?? 0),
                'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
                'width' => (int) ($artwork->width ?? 0),
                'height' => (int) ($artwork->height ?? 0),
            ],
            'files' => DB::table('artwork_files')
                ->where('artwork_id', $artwork->id)
                ->orderBy('variant')
                ->get(['variant', 'path', 'mime', 'size'])
                ->map(fn ($row): array => [
                    'variant' => (string) ($row->variant ?? ''),
                    'path' => (string) ($row->path ?? ''),
                    'mime' => (string) ($row->mime ?? 'application/octet-stream'),
                    'size' => (int) ($row->size ?? 0),
                ])
                ->values()
                ->all(),
        ];
    }

    /**
     * Create a new immutable revision from a fully materialized artwork snapshot.
     *
     * @param array<string, mixed> $snapshot
     * @param array<string, mixed>|null $previousSnapshot
     */
    public function createVersionFromSnapshot(
        Artwork $artwork,
        array $snapshot,
        int $userId,
        ?string $changeNote = null,
        ?array $previousSnapshot = null,
    ): ArtworkVersion {
        $normalizedSnapshot = $this->normalizeSnapshot($snapshot);
        $previous = $this->normalizeSnapshot($previousSnapshot ?? $this->captureArtworkSnapshot($artwork));

        $this->rateLimitCheck($userId, $artwork->id);

        return DB::transaction(function () use ($artwork, $normalizedSnapshot, $previous, $userId, $changeNote): ArtworkVersion {
            $this->ensureBaselineVersion($artwork, $previous, $userId);

            $nextNumber = max((int) $artwork->versions()->max('version_number'), 0) + 1;
            $meta = $this->snapshotArtworkMeta($normalizedSnapshot);

            $artwork->versions()->update(['is_current' => false]);

            $version = ArtworkVersion::create([
                'artwork_id' => $artwork->id,
                'version_number' => $nextNumber,
                'file_path' => $meta['file_path'],
                'file_hash' => $meta['hash'],
                'width' => $meta['width'],
                'height' => $meta['height'],
                'file_size' => $meta['file_size'],
                'change_note' => $changeNote,
                'snapshot_json' => $normalizedSnapshot,
                'is_current' => true,
            ]);

            $needsReapproval = $this->shouldRequireReapprovalFromSnapshots($previous, $normalizedSnapshot);

            $artwork->update([
                'current_version_id' => $version->id,
                'version_count' => $nextNumber,
                'version_updated_at' => now(),
                'requires_reapproval' => $needsReapproval,
            ]);

            $this->applyRankingProtection($artwork);

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

            $this->incrementRateLimitCounters($userId, $artwork->id);

            return $version;
        });
    }

    /**
     * Apply a stored snapshot back onto the artwork row and artwork_files table.
     *
     * @param array<string, mixed> $snapshot
     */
    public function applySnapshot(Artwork $artwork, array $snapshot): void
    {
        $normalizedSnapshot = $this->normalizeSnapshot($snapshot);
        $meta = $this->snapshotArtworkMeta($normalizedSnapshot);

        DB::transaction(function () use ($artwork, $normalizedSnapshot, $meta): void {
            DB::table('artwork_files')
                ->where('artwork_id', $artwork->id)
                ->delete();

            $rows = collect($normalizedSnapshot['files'] ?? [])
                ->filter(fn (array $row): bool => ($row['variant'] ?? '') !== '' && ($row['path'] ?? '') !== '')
                ->map(fn (array $row): array => [
                    'artwork_id' => $artwork->id,
                    'variant' => (string) $row['variant'],
                    'path' => (string) $row['path'],
                    'mime' => (string) ($row['mime'] ?? 'application/octet-stream'),
                    'size' => (int) ($row['size'] ?? 0),
                ])
                ->values()
                ->all();

            if ($rows !== []) {
                DB::table('artwork_files')->insert($rows);
            }

            $artwork->update([
                'file_name' => $meta['file_name'],
                'file_path' => $meta['file_path'],
                'hash' => $meta['hash'],
                'file_ext' => $meta['file_ext'],
                'thumb_ext' => $meta['thumb_ext'],
                'file_size' => $meta['file_size'],
                'mime_type' => $meta['mime_type'],
                'width' => $meta['width'],
                'height' => $meta['height'],
            ]);
        });
    }

    /**
     * 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 {
        $snapshot = [
            'artwork' => [
                'file_name' => (string) ($artwork->file_name ?? 'artwork'),
                'file_path' => $filePath,
                'hash' => $fileHash,
                'file_ext' => (string) ($artwork->file_ext ?? ''),
                'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
                'file_size' => $fileSize,
                'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
                'width' => $width,
                'height' => $height,
            ],
            'files' => [],
        ];

        return $this->createVersionFromSnapshot($artwork, $snapshot, $userId, $changeNote);
    }

    /**
     * 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 {
        $previousSnapshot = $this->captureArtworkSnapshot($artwork);
        $snapshot = is_array($version->snapshot_json)
            ? $this->normalizeSnapshot($version->snapshot_json)
            : $this->normalizeSnapshot([
                'artwork' => array_merge($previousSnapshot['artwork'] ?? [], [
                    'file_name' => (string) ($artwork->file_name ?? 'artwork'),
                    'file_path' => $version->file_path,
                    'hash' => $version->file_hash,
                    'file_ext' => (string) ($artwork->file_ext ?? ''),
                    'thumb_ext' => (string) ($artwork->thumb_ext ?? ''),
                    'file_size' => (int) $version->file_size,
                    'mime_type' => (string) ($artwork->mime_type ?? 'application/octet-stream'),
                    'width' => (int) $version->width,
                    'height' => (int) $version->height,
                ]),
                'files' => $previousSnapshot['files'] ?? [],
            ]);

        $this->applySnapshot($artwork, $snapshot);
        $artwork->refresh();

        return $this->createVersionFromSnapshot(
            $artwork,
            $snapshot,
            $userId,
            "Restored from version {$version->version_number}",
            $previousSnapshot,
        );
    }

    /**
     * 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;
    }

    /**
     * @param array<string, mixed> $previousSnapshot
     * @param array<string, mixed> $newSnapshot
     */
    public function shouldRequireReapprovalFromSnapshots(array $previousSnapshot, array $newSnapshot): bool
    {
        $previous = $this->snapshotArtworkMeta($previousSnapshot);
        $next = $this->snapshotArtworkMeta($newSnapshot);

        if (($previous['width'] ?? 0) <= 0 || ($previous['height'] ?? 0) <= 0) {
            return false;
        }

        $widthChange = abs($next['width'] - $previous['width']) / max($previous['width'], 1);
        $heightChange = abs($next['height'] - $previous['height']) / max($previous['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);
        }
    }

    /**
     * @param array<string, mixed>|null $snapshot
     */
    private function ensureBaselineVersion(Artwork $artwork, ?array $snapshot, int $userId): ?ArtworkVersion
    {
        if ($artwork->versions()->exists()) {
            return null;
        }

        $normalizedSnapshot = $this->normalizeSnapshot($snapshot ?? $this->captureArtworkSnapshot($artwork));
        $meta = $this->snapshotArtworkMeta($normalizedSnapshot);
        $baselineNumber = max(1, (int) ($artwork->version_count ?? 1));

        $version = ArtworkVersion::create([
            'artwork_id' => $artwork->id,
            'version_number' => $baselineNumber,
            'file_path' => $meta['file_path'],
            'file_hash' => $meta['hash'],
            'width' => $meta['width'],
            'height' => $meta['height'],
            'file_size' => $meta['file_size'],
            'change_note' => 'Baseline snapshot',
            'snapshot_json' => $normalizedSnapshot,
            'is_current' => true,
        ]);

        $artwork->update([
            'current_version_id' => $version->id,
            'version_count' => $baselineNumber,
            'version_updated_at' => now(),
        ]);

        ArtworkVersionEvent::create([
            'artwork_id' => $artwork->id,
            'user_id' => $userId,
            'action' => 'baseline_snapshot',
            'version_id' => $version->id,
        ]);

        return $version;
    }

    /**
     * @param array<string, mixed> $snapshot
     * @return array{file_name: string, file_path: string, hash: string, file_ext: string, thumb_ext: string, file_size: int, mime_type: string, width: int, height: int}
     */
    private function snapshotArtworkMeta(array $snapshot): array
    {
        $normalized = $this->normalizeSnapshot($snapshot);
        $artwork = $normalized['artwork'];

        return [
            'file_name' => (string) ($artwork['file_name'] ?? 'artwork'),
            'file_path' => (string) ($artwork['file_path'] ?? ''),
            'hash' => (string) ($artwork['hash'] ?? ''),
            'file_ext' => (string) ($artwork['file_ext'] ?? ''),
            'thumb_ext' => (string) ($artwork['thumb_ext'] ?? ''),
            'file_size' => (int) ($artwork['file_size'] ?? 0),
            'mime_type' => (string) ($artwork['mime_type'] ?? 'application/octet-stream'),
            'width' => max(0, (int) ($artwork['width'] ?? 0)),
            'height' => max(0, (int) ($artwork['height'] ?? 0)),
        ];
    }

    /**
     * @param array<string, mixed>|null $snapshot
     * @return array{artwork: array<string, mixed>, files: array<int, array<string, mixed>>}
     */
    private function normalizeSnapshot(?array $snapshot): array
    {
        $artwork = is_array($snapshot['artwork'] ?? null) ? $snapshot['artwork'] : [];
        $files = is_array($snapshot['files'] ?? null) ? $snapshot['files'] : [];

        return [
            'artwork' => [
                'file_name' => (string) ($artwork['file_name'] ?? 'artwork'),
                'file_path' => (string) ($artwork['file_path'] ?? ''),
                'hash' => (string) ($artwork['hash'] ?? ''),
                'file_ext' => (string) ($artwork['file_ext'] ?? ''),
                'thumb_ext' => (string) ($artwork['thumb_ext'] ?? ''),
                'file_size' => (int) ($artwork['file_size'] ?? 0),
                'mime_type' => (string) ($artwork['mime_type'] ?? 'application/octet-stream'),
                'width' => max(0, (int) ($artwork['width'] ?? 0)),
                'height' => max(0, (int) ($artwork['height'] ?? 0)),
            ],
            'files' => collect($files)
                ->filter(fn ($file): bool => is_array($file))
                ->map(fn (array $file): array => [
                    'variant' => (string) ($file['variant'] ?? ''),
                    'path' => (string) ($file['path'] ?? ''),
                    'mime' => (string) ($file['mime'] ?? 'application/octet-stream'),
                    'size' => (int) ($file['size'] ?? 0),
                ])
                ->filter(fn (array $file): bool => $file['variant'] !== '' && $file['path'] !== '')
                ->values()
                ->all(),
        ];
    }
}
