<?php

namespace App\Services\Dialler;

use App\Models\Dialler\AgentStatus;
use App\Models\Dialler\AgentStatusHistory;
use Carbon\CarbonInterface;
use Illuminate\Support\Facades\DB;

/**
 * Single choke-point for agent presence changes.
 *
 * Every status transition routes through transitionTo() so that the contiguous
 * interval history (agent_status_histories) and the live snapshot
 * (agent_statuses) stay in lock-step. Callers: CallOverlay (live agent),
 * SetAgentOfflineOnLogout, and ReapStaleAgentStatuses (console).
 */
class AgentStatusService
{
    /**
     * Move an agent to $status, closing the open interval and opening a new one.
     *
     * Idempotent: transitioning to the status the agent is already in records no
     * new interval and leaves status_changed_at untouched (so the dashboard
     * "changed X ago" label stays meaningful).
     *
     * $solicitorId is taken explicitly so the method is safe in console/queue
     * contexts where the BelongsToSolicitor global scope + auto-fill are off.
     *
     * @param  array{communication_id?:int|null, source?:string|null, meta?:array<string,mixed>}  $context
     * @param  CarbonInterface|null  $at  When the transition happened (defaults to now; the reaper backdates it to last_seen_at).
     */
    public function transitionTo(
        int $solicitorId,
        int $agentId,
        string $status,
        array $context = [],
        ?CarbonInterface $at = null
    ): AgentStatus {
        $at ??= now();

        return DB::transaction(function () use ($solicitorId, $agentId, $status, $context, $at) {
            $open = AgentStatusHistory::query()
                ->where('solicitor_id', $solicitorId)
                ->where('agent_id', $agentId)
                ->whereNull('ended_at')
                ->lockForUpdate()
                ->latest('started_at')
                ->first();

            $isChange = ! $open || $open->status !== $status;

            // Never let a backdated transition (e.g. the reaper closing a session
            // at last_seen_at) land before the current interval began.
            $effectiveAt = ($open && $at->lessThan($open->started_at)) ? $open->started_at : $at;

            if ($isChange) {
                if ($open) {
                    $open->update([
                        'ended_at'         => $effectiveAt,
                        'duration_seconds' => (int) $open->started_at->diffInSeconds($effectiveAt),
                    ]);
                }

                AgentStatusHistory::create([
                    'solicitor_id'     => $solicitorId,
                    'agent_id'         => $agentId,
                    'status'           => $status,
                    'started_at'       => $effectiveAt,
                    'communication_id' => $context['communication_id'] ?? null,
                    'source'           => $context['source'] ?? null,
                    'meta'             => $context['meta'] ?? null,
                ]);
            }

            // Keep the live snapshot in sync. Direct assignment (not mass-assign)
            // so current_communication_id is written even though it is not in the
            // model's $fillable.
            $snapshot = AgentStatus::firstOrNew([
                'solicitor_id' => $solicitorId,
                'agent_id'     => $agentId,
            ]);

            $snapshot->status = $status;

            if ($isChange || ! $snapshot->exists) {
                $snapshot->status_changed_at = $effectiveAt;
            }

            if (array_key_exists('communication_id', $context)) {
                $snapshot->current_communication_id = $context['communication_id'];
            }

            $snapshot->save();

            return $snapshot;
        });
    }
}
