<?php
namespace App\Http\Controllers;
use App\Models\{Account,Country,CrmCase,FollowUp,NotificationAlert,OperationalDocument,User};use App\Services\CrmNotificationService;use Illuminate\Http\Request;use Illuminate\Support\Facades\DB;use Illuminate\Validation\Rule;
class OperationsFoundationController extends Controller{ private static array $lastAssignment=[]; private static array $failedAssignment=[];
 public static function nextRoundRobinAssignee(int $businessId,string $module): ?int {return self::nextAssignee($businessId,$module);}
 public static function nextAssignee(int $businessId,string $module,?int $countryId=null): ?int {$rules=DB::table('assignment_rules')->where('business_id',$businessId)->where('module',$module)->where('active',true)->orderByRaw("case when strategy = 'territory' then 0 else 1 end")->get();foreach($rules as $rule){$territories=collect(json_decode($rule->territory_country_ids??'[]',true))->map(fn($id)=>(int)$id);if($rule->strategy==='territory'&&(!$countryId||!$territories->contains($countryId)))continue;$users=collect(json_decode($rule->user_ids,true))->filter()->values();if($users->isEmpty())continue;$position=$users->search($rule->last_user_id);$next=$users->get($position===false?0:(($position+1)%$users->count()));DB::table('assignment_rules')->where('id',$rule->id)->update(['last_user_id'=>$next,'updated_at'=>now()]);self::$lastAssignment[$module]=['business_id'=>$businessId,'rule_id'=>$rule->id,'strategy'=>$rule->strategy,'eligible_user_ids'=>json_encode($users->all()),'selected_user_id'=>$next,'reason'=>$rule->strategy==='territory'?'Matched territory rule':'Selected by round-robin rule'];return (int)$next;}self::$failedAssignment[$module]=['business_id'=>$businessId,'module'=>$module,'reason'=>'No eligible '.$module.' assignee was found.'];User::where('business_id',$businessId)->where('is_admin',true)->get()->each(fn($user)=>app(CrmNotificationService::class)->send([$user],'assignment_failure','Assignment routing failed','No eligible '.$module.' assignee was found.',route('operations.assignment-rules.index'),'AssignmentRule',0,'failed'));return null;}
 public static function recordAssignment(object $record,string $module):void{if(!empty(self::$lastAssignment[$module])){DB::table('assignment_rule_executions')->insert(self::$lastAssignment[$module]+['assignable_type'=>get_class($record),'assignable_id'=>$record->id,'created_at'=>now(),'updated_at'=>now()]);unset(self::$lastAssignment[$module]);return;}if(!empty(self::$failedAssignment[$module])){DB::table('assignment_failure_queue')->insert(self::$failedAssignment[$module]+['assignable_type'=>get_class($record),'assignable_id'=>$record->id,'status'=>'open','created_at'=>now(),'updated_at'=>now()]);unset(self::$failedAssignment[$module]);}}
 public function assignmentRules(Request $r){abort_unless($r->user()->is_admin,403);$b=$r->user()->business_id;$users=User::where('business_id',$b)->orderBy('name')->get(['id','name','team_name']);$userNames=$users->pluck('name','id');$countries=Country::orderBy('name')->get(['id','name']);$countryNames=$countries->pluck('name','id');$rules=DB::table('assignment_rules')->where('business_id',$b)->get()->map(function($rule)use($userNames,$countryNames){$rule->name=$rule->name?:ucwords($rule->module).' '.ucwords(str_replace('_',' ',$rule->strategy)).' rule';$rule->member_names=collect(json_decode($rule->user_ids,true))->map(fn($id)=>$userNames[$id]??'Former user')->join(', ');$rule->territory_names=collect(json_decode($rule->territory_country_ids??'[]',true))->map(fn($id)=>$countryNames[$id]??'Unknown')->join(', ');return $rule;});$audits=DB::table('assignment_rule_executions as audit')->leftJoin('assignment_rules as rule','rule.id','=','audit.rule_id')->where('audit.business_id',$b)->select('audit.*','rule.name as rule_name')->latest('audit.id')->limit(100)->get()->map(function($audit)use($userNames){$audit->rule_name=$audit->rule_name?:ucwords(str_replace('_',' ',$audit->strategy ?: 'automatic')).' routing rule';$audit->eligible_names=collect(json_decode($audit->eligible_user_ids??'[]',true))->map(fn($id)=>$userNames[$id]??'Former user')->join(', ');$audit->selected_name=$userNames[$audit->selected_user_id]??'Unassigned';return $audit;});$failures=DB::table('assignment_failure_queue')->where('business_id',$b)->where('status','open')->latest('id')->get();return view('operations.assignment-rules',compact('rules','users','countries','audits','failures'));}
 public function customerSuccess(Request $r){$b=$r->user()->business_id;$accounts=Account::where('business_id',$b)->where('status','!=','archived')->orderBy('account_name')->get(['id','account_name']);$renewals=DB::table('renewal_opportunities')->where('business_id',$b)->where('status','open')->orderBy('renewal_date')->limit(50)->get();$health=DB::table('customer_health_snapshots')->where('business_id',$b)->latest()->limit(50)->get();$policies=DB::table('sla_policies')->where('business_id',$b)->get();$breaches=DB::table('sla_events')->where('business_id',$b)->where('status','breached')->latest('breached_at')->limit(50)->get();$users=User::where('business_id',$b)->orderBy('name')->get(['id','name']);return view('operations.customer-success',compact('accounts','renewals','health','policies','breaches','users'));}
 public function dashboard(Request $r){$b=$r->user()->business_id;$r->validate(['date_from'=>'nullable|date','date_to'=>'nullable|date|after_or_equal:date_from']);$from=$r->input('date_from',now()->subDays(30)->toDateString());$to=$r->input('date_to',now()->toDateString());$documents=OperationalDocument::where('business_id',$b)->whereBetween('created_at',[$from.' 00:00:00',$to.' 23:59:59']);$invoices=(clone $documents)->where('type','invoices');$data=['openRenewals'=>DB::table('renewal_opportunities')->where('business_id',$b)->where('status','open')->count(),'renewalsDue'=>DB::table('renewal_opportunities')->where('business_id',$b)->where('status','open')->whereDate('renewal_date','<=',now()->addDays(30))->count(),'atRiskAccounts'=>DB::table('customer_health_snapshots')->where('business_id',$b)->where('status','at_risk')->count(),'slaBreaches'=>DB::table('sla_events')->where('business_id',$b)->where('status','breached')->count(),'outstandingInvoices'=>(clone $invoices)->whereIn('status',['sent','partially_paid'])->count(),'outstandingValue'=>(clone $invoices)->whereIn('status',['sent','partially_paid'])->get()->sum(fn($x)=>($x->details['balance_due']??$x->details['total_amount']??0)),'paidInvoices'=>(clone $invoices)->where('status','paid')->count(),'activeProjects'=>DB::table('projects')->where('business_id',$b)->whereIn('status',['planned','active','on_hold'])->count(),'deliveriesInProgress'=>(clone $documents)->where('type','deliveries')->whereNotIn('status',['closed','cancelled'])->count(),'salesOrdersApproved'=>(clone $documents)->where('type','sales-orders')->whereIn('status',['approved','partially_delivered','completed'])->count()];$statusBreakdown=(clone $documents)->select('type','status',DB::raw('count(*) as total'))->groupBy('type','status')->orderBy('type')->get();$recent=(clone $documents)->latest()->limit(15)->get();return view('operations.dashboard',compact('data','from','to','statusBreakdown','recent'));}
 public function hierarchy(Request $r,Account $account){abort_unless($account->business_id===$r->user()->business_id,403);$data=$r->validate(['parent_account_id'=>['nullable',Rule::exists('accounts','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))]]);$parent=$data['parent_account_id']??null;abort_if($parent===$account->id,422);while($parent){abort_if($parent===$account->id,422,'Account hierarchy cannot contain a cycle.');$parent=Account::find($parent)?->parent_account_id;}$account->update($data);return back()->with('success','Account hierarchy updated.');}
 public function assignAccount(Request $r,Account $account){abort_unless($account->business_id===$r->user()->business_id,403);$d=$r->validate(['owner_id'=>['required',Rule::exists('users','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'cascade_open_cases'=>'nullable|boolean']);DB::transaction(function()use($account,$d){$account->update(['contact_owner_id'=>$d['owner_id']]);if($d['cascade_open_cases']??false)DB::table('cases')->where('account_id',$account->id)->whereNotIn('status',['resolved','closed'])->update(['assigned_to'=>$d['owner_id'],'updated_at'=>now()]);});return back()->with('success','Account ownership updated.');}
 public function merge(Request $r,Account $account){abort_unless($account->business_id===$r->user()->business_id,403);$data=$r->validate(['target_account_id'=>['required',Rule::exists('accounts','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'confirm_name'=>['required','string']]);$target=Account::where('business_id',$r->user()->business_id)->findOrFail($data['target_account_id']);abort_unless($target->status!=='archived',422,'An archived account cannot be a merge target.');abort_unless($data['confirm_name']===$account->account_name,422,'Enter the source account name to confirm this merge.');abort_if($target->id===$account->id,422);DB::transaction(function()use($account,$target){$sourceContacts=DB::table('account_contact')->where('account_id',$account->id)->pluck('contact_id');$targetContacts=DB::table('account_contact')->where('account_id',$target->id)->pluck('contact_id');DB::table('account_contact')->where('account_id',$account->id)->whereIn('contact_id',$targetContacts)->delete();DB::table('account_contact')->where('account_id',$account->id)->update(['account_id'=>$target->id]);$sourceDeals=DB::table('deal_account')->where('account_id',$account->id)->pluck('deal_id');$targetDeals=DB::table('deal_account')->where('account_id',$target->id)->pluck('deal_id');DB::table('deal_account')->where('account_id',$account->id)->whereIn('deal_id',$targetDeals)->delete();DB::table('deal_account')->where('account_id',$account->id)->update(['account_id'=>$target->id]);foreach(['contacts','deals','projects','cases','activities','operational_documents','renewal_opportunities','customer_health_snapshots','invoice_schedules'] as $table)DB::table($table)->where('account_id',$account->id)->update(['account_id'=>$target->id,'updated_at'=>now()]);DB::table('accounts')->where('parent_account_id',$account->id)->where('id','<>',$target->id)->update(['parent_account_id'=>$target->id,'updated_at'=>now()]);if((int)$target->parent_account_id===(int)$account->id)$target->update(['parent_account_id'=>$account->parent_account_id]);$account->update(['merged_into_id'=>$target->id,'status'=>'archived']);});return redirect()->route('accounts')->with('success','Account merged; operational, customer-success, and hierarchy links were moved to the target.');}
 public function health(Request $r){$d=$r->validate(['account_id'=>['required',Rule::exists('accounts','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'score'=>'required|integer|min:0|max:100','status'=>['required',Rule::in(['healthy','watch','at_risk'])],'notes'=>'nullable|required_if:status,at_risk|string|max:5000']);$id=DB::table('customer_health_snapshots')->insertGetId($d+['business_id'=>$r->user()->business_id,'recorded_by'=>$r->user()->id,'created_at'=>now(),'updated_at'=>now()]);if($d['status']==='at_risk')FollowUp::firstOrCreate(['business_id'=>$r->user()->business_id,'related_type'=>Account::class,'related_id'=>$d['account_id'],'title'=>'Customer health recovery'],['owner_id'=>$r->user()->id,'follow_up_at'=>now()->addDays(2),'remind_at'=>now()->addDay(),'priority'=>'high','status'=>'pending','notes'=>'At-risk health snapshot #'.$id.': '.$d['notes']]);return back()->with('success',$d['status']==='at_risk'?'Customer health recorded and recovery follow-up created.':'Customer health recorded.');}
 public function renewal(Request $r){$d=$r->validate(['account_id'=>['required',Rule::exists('accounts','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'renewal_date'=>'required|date','value'=>'required|numeric|min:0','owner_id'=>['nullable',Rule::exists('users','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'notes'=>'nullable|string|max:5000']);$d['owner_id']??=$r->user()->id;DB::table('renewal_opportunities')->insert($d+['business_id'=>$r->user()->business_id,'status'=>'open','created_at'=>now(),'updated_at'=>now()]);return back()->with('success','Renewal opportunity created and assigned.');}
 public function renewalStatus(Request $r,int $renewal){$d=$r->validate(['status'=>['required',Rule::in(['open','engaged','renewed','lost','cancelled'])],'notes'=>'nullable|required_if:status,lost|string|max:5000']);$record=DB::table('renewal_opportunities')->where('id',$renewal)->where('business_id',$r->user()->business_id)->first();abort_unless($record,404);DB::table('renewal_opportunities')->where('id',$renewal)->update(['status'=>$d['status'],'notes'=>$d['notes']??$record->notes,'updated_at'=>now()]);if(in_array($d['status'],['open','engaged'],true))FollowUp::firstOrCreate(['business_id'=>$r->user()->business_id,'related_type'=>'RenewalOpportunity','related_id'=>$renewal,'title'=>'Renewal follow-up #'.$renewal],['owner_id'=>$record->owner_id?:$r->user()->id,'follow_up_at'=>now()->addDays(7),'remind_at'=>now()->addDays(6),'priority'=>'high','status'=>'pending','notes'=>$d['notes']??null]);return back()->with('success','Renewal status updated.');}
 public function invoiceSchedule(Request $r){$d=$r->validate(['account_id'=>['required',Rule::exists('accounts','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'deal_id'=>'nullable|exists:deals,id','operational_document_id'=>'nullable|exists:operational_documents,id','frequency'=>['required',Rule::in(['one_time','monthly','quarterly','yearly'])],'next_invoice_date'=>'required|date','amount'=>'required|numeric|min:0.01','currency'=>'required|string|max:8','payment_terms'=>'required|string|max:255','description'=>'required|string|max:5000']);DB::table('invoice_schedules')->insert($d+['business_id'=>$r->user()->business_id,'owner_id'=>$r->user()->id,'active'=>true,'created_at'=>now(),'updated_at'=>now()]);return back()->with('success','Invoice schedule created.');}
 public function sla(Request $r){$d=$r->validate(['priority'=>['required',Rule::in(['low','medium','high','urgent'])],'resolution_hours'=>'required|integer|min:1|max:8760']);DB::table('sla_policies')->updateOrInsert(['business_id'=>$r->user()->business_id,'priority'=>$d['priority']],$d+['business_id'=>$r->user()->business_id,'active'=>true,'updated_at'=>now(),'created_at'=>now()]);return back()->with('success','SLA policy saved.');}
 public function refreshSla(Request $r){$b=$r->user()->business_id;$policies=DB::table('sla_policies')->where('business_id',$b)->where('active',true)->get()->keyBy('priority');foreach(CrmCase::where('business_id',$b)->get() as $case){$p=$policies[$case->priority]??null;if(!$p)continue;$due=$case->created_at->copy()->addHours($p->resolution_hours);$status=in_array($case->status,['resolved','closed'],true)?'resolved':($due->isPast()?'breached':'open');$existing=DB::table('sla_events')->where('case_id',$case->id)->where('sla_policy_id',$p->id)->first();DB::table('sla_events')->updateOrInsert(['case_id'=>$case->id,'sla_policy_id'=>$p->id],['business_id'=>$b,'due_at'=>$due,'status'=>$status,'breached_at'=>$status==='breached'?($existing?->breached_at?:now()):null,'updated_at'=>now(),'created_at'=>$existing?->created_at?:now()]);if($status==='breached'&&$existing?->status!=='breached'){NotificationAlert::firstOrCreate(['business_id'=>$b,'name'=>'SLA breach: '.$case->case_number]);$users=User::where('business_id',$b)->where(fn($q)=>$q->whereKey($case->assigned_to)->orWhere('is_admin',true))->get();app(CrmNotificationService::class)->send($users,'sla_breach','SLA breach',$case->case_number.' is past its '.$p->resolution_hours.' hour SLA.',route('cases.show',$case),'CrmCase',$case->id,'breached');}}return back()->with('success','SLA monitoring refreshed and breaches escalated.');}
 public function assignmentRule(Request $r){abort_unless($r->user()->is_admin,403);$d=$r->validate(['name'=>'nullable|string|max:255','module'=>['required',Rule::in(['leads','deals','cases'])],'strategy'=>['required',Rule::in(['manual','round_robin','territory'])],'user_ids'=>'required|array|min:1','user_ids.*'=>['integer',Rule::exists('users','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))],'territory_country_ids'=>'nullable|required_if:strategy,territory|array|min:1','territory_country_ids.*'=>'integer|exists:countries,id']);DB::table('assignment_rules')->updateOrInsert(['business_id'=>$r->user()->business_id,'module'=>$d['module']],['name'=>$d['name']?:ucwords($d['module']).' '.ucwords(str_replace('_',' ',$d['strategy'])).' rule','strategy'=>$d['strategy'],'user_ids'=>json_encode($d['user_ids']),'territory_country_ids'=>json_encode($d['territory_country_ids']??[]),'active'=>true,'updated_at'=>now(),'created_at'=>now()]);return back()->with('success','Assignment rule saved.');}
 public function resolveAssignmentFailure(Request $r,int $failure){abort_unless($r->user()->is_admin,403);$queue=DB::table('assignment_failure_queue')->where('id',$failure)->where('business_id',$r->user()->business_id)->where('status','open')->first();abort_unless($queue,404);$data=$r->validate(['user_id'=>['required','integer',Rule::exists('users','id')->where(fn($q)=>$q->where('business_id',$r->user()->business_id))]]);$columns=['leads'=>'contact_owner_id','deals'=>'owner_id','cases'=>'assigned_to'];$models=['leads'=>\App\Models\Lead::class,'deals'=>\App\Models\Deal::class,'cases'=>CrmCase::class];abort_unless(isset($columns[$queue->module])&&$queue->assignable_id,422,'This failure has no assignable record to resolve.');$record=$models[$queue->module]::whereKey($queue->assignable_id)->where('business_id',$r->user()->business_id)->firstOrFail();$record->update([$columns[$queue->module]=>$data['user_id']]);DB::table('assignment_failure_queue')->where('id',$queue->id)->update(['status'=>'resolved','resolved_by'=>$r->user()->id,'resolved_at'=>now(),'updated_at'=>now()]);return back()->with('success','Failed assignment resolved and record assigned.');}
}
