<?php
namespace App\Jobs;
use App\Models\{OperationsAlert,Payout,WebhookDelivery};
use Illuminate\Bus\Queueable;use Illuminate\Contracts\Queue\ShouldQueue;use Illuminate\Foundation\Bus\Dispatchable;use Illuminate\Queue\InteractsWithQueue;use Illuminate\Queue\SerializesModels;use Illuminate\Support\Facades\{DB,Http};
final class OperationsMonitorJob implements ShouldQueue {use Dispatchable,InteractsWithQueue,Queueable,SerializesModels;public function __construct(){ $this->onQueue('notifications'); }
 public function handle():void{ $this->failedJobs();$this->webhookFailures();$this->stuckPayouts();$this->lowTrxHotWallet(); }
 private function alert(string $severity,string $code,string $message,array $context=[],?string $subjectType=null,?string $subjectId=null):void{OperationsAlert::updateOrCreate(['code'=>$code,'subject_type'=>$subjectType,'subject_id'=>$subjectId,'acknowledged_at'=>null],['severity'=>$severity,'message'=>$message,'context'=>$context]);}
 private function failedJobs():void{$minutes=(int)config('gateway.observability.failed_jobs_alert_window_minutes',15);if(!DB::getSchemaBuilder()->hasTable('failed_jobs'))return;$count=DB::table('failed_jobs')->where('failed_at','>=',now()->subMinutes($minutes))->count();if($count>0)$this->alert('critical','FAILED_JOBS_RECENT',"{$count} queue jobs failed in the last {$minutes} minutes.",['failed_jobs'=>$count,'window_minutes'=>$minutes]);}
 private function webhookFailures():void{$minutes=(int)config('gateway.observability.webhook_failure_alert_window_minutes',15);$failed=WebhookDelivery::whereIn('status',['retrying','exhausted'])->where('updated_at','>=',now()->subMinutes($minutes))->count();$exhausted=WebhookDelivery::where('status','exhausted')->where('updated_at','>=',now()->subMinutes($minutes))->count();if($failed>0)$this->alert($exhausted>0?'critical':'warning','WEBHOOK_FAILURES_RECENT',"{$failed} webhook deliveries failed or are retrying in the last {$minutes} minutes.",['failed_or_retrying'=>$failed,'exhausted'=>$exhausted,'window_minutes'=>$minutes]);}
 private function stuckPayouts():void{$minutes=(int)config('gateway.observability.stuck_payout_alert_minutes',30);$rows=Payout::whereIn('status',['broadcasting','confirming'])->where('updated_at','<=',now()->subMinutes($minutes))->limit(20)->get();foreach($rows as $p){$this->alert('critical','STUCK_PAYOUT','Payout has been stuck in '.$p->status.' for more than '.$minutes.' minutes.',['payout_id'=>$p->public_id,'status'=>$p->status,'updated_at'=>$p->updated_at?->toIso8601String()],'payout',(string)$p->id);}}
 private function lowTrxHotWallet():void{$signer=rtrim((string)config('gateway.tron.signer_url'),'/');$token=(string)config('gateway.tron.signer_token');$node=rtrim((string)config('gateway.tron.node_url'),'/');if($signer===''||$token===''||$node==='')return;try{$hot=Http::timeout(5)->withToken($token)->get($signer.'/v1/hot-wallet');$address=(string)$hot->json('address','');if($address==='')return;$req=Http::baseUrl($node)->acceptJson()->timeout(8);$apiKey=(string)config('gateway.tron.api_key');if($apiKey!=='')$req=$req->withHeaders(['TRON-PRO-API-KEY'=>$apiKey]);$res=$req->post('/wallet/getaccount',['address'=>$address,'visible'=>true]);if(!$res->successful()){ $this->alert('warning','TRON_HOT_WALLET_BALANCE_UNKNOWN','Could not read TRON hot wallet balance.',['address'=>$address,'status'=>$res->status(),'body'=>substr($res->body(),0,500)]);return;}$balance=(string)($res->json('balance')??'0');$min=(string)config('gateway.observability.low_trx_hot_wallet_min_sun','100000000');if(bccomp($balance,$min,0)<0)$this->alert('critical','LOW_TRX_HOT_WALLET','TRON hot wallet TRX balance is below threshold.',['address'=>$address,'balance_sun'=>$balance,'minimum_sun'=>$min]);}catch(\Throwable $e){$this->alert('warning','TRON_HOT_WALLET_BALANCE_UNKNOWN','Could not check TRON hot wallet balance.',['error'=>$e->getMessage()]);}}
}