<?php

namespace App\Actions\Communications;

use App\Enum\Communication\CommunicationDirection;
use App\Enum\Communication\CommunicationEventType;
use App\Models\Communication\Call;
use App\Models\User\User;
use App\Services\Communications\AgentPresenceService;
use Illuminate\Support\Facades\DB;
use Twilio\Rest\Client;

class TransferCallAction
{
    public function __construct(protected AgentPresenceService $presenceService) {}

    public function handle(int $callId, int $toAgentId): User
    {
        $call = Call::with('communication')->findOrFail($callId);

        abort_unless($call->communication, 422, 'Call has no associated communication.');
        abort_unless($call->provider_call_sid, 422, 'Call SID not yet available — please try again.');
        abort_if($toAgentId === auth()->id(), 422, 'Cannot transfer to yourself.');

        $toAgent     = User::findOrFail($toAgentId);
        $solicitorId = $call->communication->solicitor_id;

        if (! $this->presenceService->isAvailable($solicitorId, $toAgentId)) {
            throw new \RuntimeException('The selected agent is not currently available.');
        }

        // Record the intent before touching Twilio so we have an audit trail
        // even if the API call fails.
        DB::transaction(function () use ($call, $toAgent, $solicitorId) {
            $meta             = $call->meta ?? [];
            $meta['transfer'] = [
                'transferred_at' => now()->toIso8601String(),
                'from_agent_id'  => auth()->id(),
                'to_agent_id'    => $toAgent->id,
                'to_agent_name'  => $toAgent->name_first . ' ' . $toAgent->name_last,
            ];
            $call->update(['meta' => $meta]);

            $call->communication->events()->create([
                'solicitor_id' => $solicitorId,
                'call_id'      => $call->id,
                'agent_id'     => auth()->id(),
                'event'        => CommunicationEventType::CallTransferInitiated->value,
                'source'       => 'agent',
                'description'  => 'Transfer initiated to ' . $toAgent->name_first . ' ' . $toAgent->name_last . '.',
                'occurred_at'  => now(),
                'payload'      => ['from_agent_id' => auth()->id(), 'to_agent_id' => $toAgent->id],
            ]);
        });

        // Twilio API call is outside the transaction: a slow or failed request
        // will not hold row locks, and a rollback will not undo an already-sent
        // redirect instruction.
        $this->redirectCall($call->provider_call_sid, auth()->id(), $toAgent->id, $call);

        // Confirm completion only after Twilio has accepted the redirect.
        DB::transaction(function () use ($call, $toAgent, $solicitorId) {
            $call->communication->events()->create([
                'solicitor_id' => $solicitorId,
                'call_id'      => $call->id,
                'agent_id'     => auth()->id(),
                'event'        => CommunicationEventType::CallTransferred->value,
                'source'       => 'agent',
                'description'  => 'Call transferred to ' . $toAgent->name_first . ' ' . $toAgent->name_last . '.',
                'occurred_at'  => now(),
                'payload'      => ['from_agent_id' => auth()->id(), 'to_agent_id' => $toAgent->id],
            ]);

            $call->communication->update(['agent_id' => $toAgent->id]);
        });

        return $toAgent;
    }

    protected function redirectCall(string $callSid, int $fromAgentId, int $toAgentId, Call $call): void
    {
        $accountSid = $call->phoneNumber?->twilio_subaccount_sid ?? config('services.twilio.sid');
        $apiKey     = config('services.twilio.api_key');
        $apiSecret  = config('services.twilio.api_secret');
        $client     = new Client($apiKey, $apiSecret, $accountSid);

        $url = route('twilio.voice.transfer') . '?' . http_build_query([
            'to_agent_id'   => $toAgentId,
            'from_agent_id' => $fromAgentId,
        ]);

        // For outbound calls, provider_call_sid is Agent A's browser (the parent leg).
        // The client's phone is a child leg created by <Dial><Number>. Redirecting
        // the parent would tear down the bridge and disconnect the client. Instead we
        // redirect the child leg directly so the client stays on the line while the
        // agent swap happens. For inbound calls the stored SID already IS the client
        // (parent), so no lookup is needed.
        $sidToRedirect = $callSid;

        if ($call->communication?->direction === CommunicationDirection::Outbound->value) {
            // Use the PSTN child SID stored by HandleCallStatusCallbackAction when
            // the first status callback fires on the dialled phone leg.
            $childSid = $call->meta['child_call_sid'] ?? null;

            if ($childSid) {
                $sidToRedirect = $childSid;
            } else {
                // Fallback: look up live child calls via Twilio API.
                $childCalls = $client->calls->read([
                    'parentCallSid' => $callSid,
                    'status'        => 'in-progress',
                ], 1);

                if (! empty($childCalls)) {
                    $sidToRedirect = $childCalls[0]->sid;
                }
            }
        }

        $client->calls($sidToRedirect)->update([
            'url'    => $url,
            'method' => 'POST',
        ]);
    }
}
