<?php

namespace App\Modules\Admin\SystemHealth\Services;

use App\Modules\Exports\Models\Export;
use App\Modules\Exports\Support\ExportStatus;
use App\Modules\Admin\Operations\Models\BackupRun;
use App\Modules\Admin\Operations\Services\BackupHealthService;
use App\Modules\Admin\Operations\Services\DeploymentReadinessService;
use App\Modules\Payroll\Models\PayrollPeriod;
use App\Modules\Payroll\Support\PayrollPeriodStatus;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

final class SystemHealthService
{
    public function __construct(
        private readonly BackupHealthService $backups,
        private readonly DeploymentReadinessService $readiness,
    ) {}

    /**
     * @return array{
     *   failed_exports: Collection<int, Export>,
     *   stuck_exports: Collection<int, Export>,
     *   stuck_payroll_periods: Collection<int, PayrollPeriod>,
     *   failed_snapshot_jobs: Collection<int, object>,
     *   failed_biometric_jobs: Collection<int, object>,
     *   failed_attendance_detection_jobs: Collection<int, object>,
     *   queue_backlog: array<string, int>,
     *   retry_spikes: array<string, int>,
     *   backup_health: array<string, mixed>,
     *   readiness: array<string, mixed>
     * }
     */
    public function snapshot(): array
    {
        $failedJobLimit = (int) config('operations.failed_jobs_limit', 50);
        $stuckExportMins = (int) config('operations.export_stuck_after_minutes', 30);
        $stalePayrollHrs = (int) config('operations.payroll_processing_stale_hours', 4);

        return [
            'failed_exports' => $this->failedExports(),
            'stuck_exports' => $this->stuckExports($stuckExportMins),
            'stuck_payroll_periods' => $this->stuckPayrollPeriods($stalePayrollHrs),
            'failed_snapshot_jobs' => $this->failedJobsLike('GeneratePayrollSnapshotsJob', $failedJobLimit),
            'failed_biometric_jobs' => $this->failedJobsLike('ProcessBiometricLogsJob', $failedJobLimit),
            'failed_attendance_detection_jobs' => $this->failedJobsLike(
                'RunAttendanceExceptionDetectionJob',
                $failedJobLimit,
            ),
            'queue_backlog' => $this->queueBacklogCounts(),
            'retry_spikes' => $this->retrySpikeCounts(),
            'backup_health' => $this->backupHealth(),
            'readiness' => $this->readiness->evaluate(),
        ];
    }

    /**
     * @return Collection<int, Export>
     */
    private function failedExports(): Collection
    {
        return Export::query()
            ->where('status', ExportStatus::FAILED)
            ->orderByDesc('id')
            ->limit(50)
            ->get();
    }

    /**
     * @return Collection<int, Export>
     */
    private function stuckExports(int $stuckAfterMinutes): Collection
    {
        $cutoff = now()->subMinutes(max(1, $stuckAfterMinutes));

        return Export::query()
            ->where('status', ExportStatus::PROCESSING)
            ->whereNotNull('started_at')
            ->where('started_at', '<', $cutoff)
            ->orderBy('started_at')
            ->limit(50)
            ->get();
    }

    /**
     * @return Collection<int, PayrollPeriod>
     */
    private function stuckPayrollPeriods(int $staleHours): Collection
    {
        $cutoff = now()->subHours(max(1, $staleHours));

        return PayrollPeriod::query()
            ->where('status', PayrollPeriodStatus::PROCESSING)
            ->where('updated_at', '<', $cutoff)
            ->orderBy('updated_at')
            ->limit(50)
            ->get();
    }

    /**
     * @return Collection<int, object>
     */
    private function failedJobsLike(string $needle, int $limit): Collection
    {
        if (! Schema::hasTable('failed_jobs')) {
            return collect();
        }

        return DB::table('failed_jobs')
            ->where('payload', 'like', '%'.$needle.'%')
            ->orderByDesc('id')
            ->limit($limit)
            ->get();
    }

    /**
     * @return array<string, int>
     */
    private function queueBacklogCounts(): array
    {
        if (! Schema::hasTable('jobs')) {
            return ['total' => 0];
        }
        $rows = DB::table('jobs')
            ->select('queue', DB::raw('count(*) as c'))
            ->groupBy('queue')
            ->orderByDesc('c')
            ->limit(10)
            ->get();
        $out = ['total' => (int) DB::table('jobs')->count()];
        foreach ($rows as $r) {
            $out[(string) $r->queue] = (int) $r->c;
        }

        return $out;
    }

    /**
     * @return array<string, int>
     */
    private function retrySpikeCounts(): array
    {
        if (! Schema::hasTable('jobs')) {
            return [];
        }
        $threshold = (int) config('operations.retry_spike_attempts_threshold', 3);
        $export = (int) DB::table('jobs')
            ->where('attempts', '>=', $threshold)
            ->where('payload', 'like', '%ExportProcessorJob%')
            ->count();
        $payroll = (int) DB::table('jobs')
            ->where('attempts', '>=', $threshold)
            ->where('payload', 'like', '%GeneratePayrollSnapshotsJob%')
            ->count();
        $attendance = (int) DB::table('jobs')
            ->where('attempts', '>=', $threshold)
            ->where('payload', 'like', '%ProcessBiometricLogsJob%')
            ->count();

        return [
            'threshold' => $threshold,
            'exports' => $export,
            'payroll' => $payroll,
            'attendance' => $attendance,
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function backupHealth(): array
    {
        $mins = (int) config('operations.backup_freshness_minutes', 1440);

        return [
            'db' => $this->backups->health(BackupRun::TYPE_DB, $mins),
            'files' => $this->backups->health(BackupRun::TYPE_FILES, $mins),
        ];
    }
}
