<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Recalculate heat_score for artworks based on hourly metric snapshots.
 *
 * Runs every 10–15 minutes via scheduler.
 *
 * Formula:
 *   raw_heat = ((views_delta*1 + downloads_delta*3 + favourites_delta*6
 *                + comments_delta*8 + shares_delta*12) / window_hours)
 *
 *   age_factor = 1 / (1 + hours_since_upload / 24)
 *
 *   heat_score = raw_heat * age_factor
 *
 * Usage:  php artisan nova:recalculate-heat
 *         php artisan nova:recalculate-heat --days=60 --chunk=1000 --lookback-hours=24 --dry-run
 */
class RecalculateHeatCommand extends Command
{
    protected $signature = 'nova:recalculate-heat
                            {--days=60 : Only process artworks created within this many days}
                            {--chunk=1000 : Chunk size for DB queries}
                            {--lookback-hours=24 : Smooth heat deltas over this many trailing hours}
                            {--dry-run : Compute scores without writing to DB}';

    protected $description = 'Recalculate heat/momentum scores for the Rising engine';

    /** Delta weights per the spec */
    private const WEIGHTS = [
        'views'      => 1,
        'downloads'  => 3,
        'favourites' => 6,
        'comments'   => 8,
        'shares'     => 12,
    ];

    public function handle(): int
    {
        $days       = (int) $this->option('days');
        $chunk      = (int) $this->option('chunk');
        $lookbackHours = max(1, (int) $this->option('lookback-hours'));
        $dryRun     = (bool) $this->option('dry-run');
        $now        = now();
        $currentHour = $now->copy()->startOfHour();
        $prevHour    = $currentHour->copy()->subHour();
        $lookbackStart = $currentHour->copy()->subHours($lookbackHours);

        $this->info("[nova:recalculate-heat] current_hour={$currentHour->toDateTimeString()} prev_hour={$prevHour->toDateTimeString()} lookback_start={$lookbackStart->toDateTimeString()} lookback_hours={$lookbackHours} days={$days}" . ($dryRun ? ' (dry-run)' : ''));

        $updatedCount = 0;
        $skippedCount = 0;

        // Process in chunks using artwork IDs that have at least one snapshot in the smoothing window
        $artworkIds = DB::table('artwork_metric_snapshots_hourly')
            ->whereBetween('bucket_hour', [$lookbackStart, $currentHour])
            ->distinct()
            ->pluck('artwork_id');

        if ($artworkIds->isEmpty()) {
            $this->warn('No snapshots found inside the requested lookback window. Run nova:metrics-snapshot-hourly first.');
            return self::SUCCESS;
        }

        // Load all snapshots for the lookback window in bulk
        $snapshots = DB::table('artwork_metric_snapshots_hourly')
            ->whereBetween('bucket_hour', [$lookbackStart, $currentHour])
            ->whereIn('artwork_id', $artworkIds)
            ->orderBy('bucket_hour')
            ->get()
            ->groupBy('artwork_id');

        // Load artwork published_at dates for age factor (use published_at, fall back to created_at)
        $artworkDates = DB::table('artworks')
            ->whereIn('id', $artworkIds)
            ->whereNull('deleted_at')
            ->where('is_approved', true)
            ->select('id', 'published_at', 'created_at')
            ->get()
            ->mapWithKeys(fn ($row) => [
                $row->id => \Carbon\Carbon::parse($row->published_at ?? $row->created_at),
            ]);

        // Process in chunks
        foreach ($artworkIds->chunk($chunk) as $chunkIds) {
            $upsertRows = [];

            foreach ($chunkIds as $artworkId) {
                $createdAt = $artworkDates->get($artworkId);
                if (!$createdAt) {
                    $skippedCount++;
                    continue;
                }

                $artworkSnapshots = $snapshots->get($artworkId);
                if (!$artworkSnapshots || $artworkSnapshots->isEmpty()) {
                    $skippedCount++;
                    continue;
                }

                $currentSnapshot = $artworkSnapshots->firstWhere('bucket_hour', $currentHour->toDateTimeString());
                if (! $currentSnapshot) {
                    $currentSnapshot = $artworkSnapshots->last();
                }

                $prevSnapshot = $artworkSnapshots->firstWhere('bucket_hour', $prevHour->toDateTimeString());
                $baselineSnapshot = $artworkSnapshots
                    ->filter(fn ($snapshot) => (string) $snapshot->bucket_hour < (string) ($currentSnapshot->bucket_hour ?? ''))
                    ->first();

                if (! $currentSnapshot) {
                    $skippedCount++;
                    continue;
                }

                // One-hour counters remain explicit fields for dashboards and debugging.
                $viewsDelta1h      = max(0, (int) ($currentSnapshot?->views_count ?? 0)      - (int) ($prevSnapshot?->views_count ?? 0));
                $downloadsDelta1h  = max(0, (int) ($currentSnapshot?->downloads_count ?? 0)  - (int) ($prevSnapshot?->downloads_count ?? 0));
                $favouritesDelta1h = max(0, (int) ($currentSnapshot?->favourites_count ?? 0) - (int) ($prevSnapshot?->favourites_count ?? 0));
                $commentsDelta1h   = max(0, (int) ($currentSnapshot?->comments_count ?? 0)   - (int) ($prevSnapshot?->comments_count ?? 0));
                $sharesDelta1h     = max(0, (int) ($currentSnapshot?->shares_count ?? 0)     - (int) ($prevSnapshot?->shares_count ?? 0));

                // Smooth the heat signal over a trailing window so low-traffic periods do not flatten Rising.
                // A single snapshot without an earlier baseline should not count as new momentum.
                if ($baselineSnapshot) {
                    $viewsDelta      = max(0, (int) ($currentSnapshot?->views_count ?? 0)      - (int) ($baselineSnapshot->views_count ?? 0));
                    $downloadsDelta  = max(0, (int) ($currentSnapshot?->downloads_count ?? 0)  - (int) ($baselineSnapshot->downloads_count ?? 0));
                    $favouritesDelta = max(0, (int) ($currentSnapshot?->favourites_count ?? 0) - (int) ($baselineSnapshot->favourites_count ?? 0));
                    $commentsDelta   = max(0, (int) ($currentSnapshot?->comments_count ?? 0)   - (int) ($baselineSnapshot->comments_count ?? 0));
                    $sharesDelta     = max(0, (int) ($currentSnapshot?->shares_count ?? 0)     - (int) ($baselineSnapshot->shares_count ?? 0));

                    $windowHours = max(
                        1.0,
                        abs($currentHour->copy()->parse($currentSnapshot->bucket_hour)->floatDiffInHours($currentHour->copy()->parse($baselineSnapshot->bucket_hour)))
                    );
                } else {
                    $viewsDelta = 0;
                    $downloadsDelta = 0;
                    $favouritesDelta = 0;
                    $commentsDelta = 0;
                    $sharesDelta = 0;
                    $windowHours = 1.0;
                }

                // Raw heat
                $rawHeat = (
                    ($viewsDelta * self::WEIGHTS['views'])
                    + ($downloadsDelta * self::WEIGHTS['downloads'])
                    + ($favouritesDelta * self::WEIGHTS['favourites'])
                    + ($commentsDelta * self::WEIGHTS['comments'])
                    + ($sharesDelta * self::WEIGHTS['shares'])
                ) / $windowHours;

                // Age factor: favors newer works
                $hoursSinceUpload = abs($now->floatDiffInHours($createdAt));
                $ageFactor = 1.0 / (1.0 + ($hoursSinceUpload / 24.0));

                // Final heat score
                $heatScore = max(0, $rawHeat * $ageFactor);

                $upsertRows[] = [
                    'artwork_id'           => $artworkId,
                    'heat_score'           => round($heatScore, 4),
                    'heat_score_updated_at' => $now,
                    'views_1h'             => $viewsDelta1h,
                    'downloads_1h'         => $downloadsDelta1h,
                    'favourites_1h'        => $favouritesDelta1h,
                    'comments_1h'          => $commentsDelta1h,
                    'shares_1h'            => $sharesDelta1h,
                ];

                $updatedCount++;
            }

            if (!$dryRun && !empty($upsertRows)) {
                DB::table('artwork_stats')->upsert(
                    $upsertRows,
                    ['artwork_id'],
                    ['heat_score', 'heat_score_updated_at', 'views_1h', 'downloads_1h', 'favourites_1h', 'comments_1h', 'shares_1h']
                );
            }
        }

        $this->info("Heat scores updated: {$updatedCount} | Skipped: {$skippedCount}");

        Log::info('[nova:recalculate-heat] completed', [
            'updated'  => $updatedCount,
            'skipped'  => $skippedCount,
            'dry_run'  => $dryRun,
        ]);

        return self::SUCCESS;
    }
}
