<?php

namespace App\Http\Controllers\Twilio;

use App\Enum\Communication\CommunicationStatus;
use App\Models\Communication\Call;
use App\Services\Communications\MissedCallTaskService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Twilio\TwiML\VoiceResponse;

/**
 * Handles the action callback Twilio POSTs when a <Dial> verb completes.
 *
 * If the dial succeeded (DialCallStatus = completed) we end the parent
 * call cleanly. For any other outcome — no-answer, busy, failed, or
 * canceled (which includes the SDK reject() path for agent decline) —
 * we mark the inbound Communication as Missed (so it appears in the
 * Missed Calls panel) and fall through to a voicemail prompt. If the
 * caller leaves a message the recording callback will upgrade the
 * status from Missed → Voicemail.
 */
class TwilioDialFallbackController {
    public function __construct(
        private readonly MissedCallTaskService $missedCallTasks,
    ) {
    }

    public function __invoke(Request $request): Response {
        $dialCallStatus = $request->input('DialCallStatus');
        $callSid        = $request->input('CallSid');

        Log::debug('TwilioDialFallbackController: dial action', [
            'CallSid'          => $callSid,
            'DialCallSid'      => $request->input('DialCallSid'),
            'DialCallStatus'   => $dialCallStatus,
            'DialCallDuration' => $request->input('DialCallDuration'),
        ]);

        $voice = new VoiceResponse();

        if ($dialCallStatus === 'completed') {
            // Agent answered and the conversation ended normally. Record the
            // answered/completed state before hanging up so the call is tagged
            // correctly (and the recording callback does not misfile it as a
            // voicemail) and it clears from the active-calls list.
            $this->markCommunicationAnswered($callSid, $request->input('DialCallDuration'));

            $voice->hangup();

            return response((string) $voice, 200)->header('Content-Type', 'text/xml');
        }

        // No agent answered. Mark as Missed before offering voicemail so the
        // dashboard's Missed Calls panel picks it up immediately. The
        // recording callback will upgrade to Voicemail if a message is left.
        $this->markCommunicationMissed($callSid);

        $call = Call::with(['communication', 'phoneNumber'])
            ->where('provider_call_sid', $callSid)
            ->first();

        if (! $call || ! $call->communication) {
            Log::debug('TwilioDialFallbackController: Call not found');
        }

        $communication = $call->communication;
        $this->missedCallTasks->diarise($communication);
        $solicitor = $communication->solicitor;

        $greetingUrl = $call->phoneNumber?->voicemailGreetingUrl();
        if ($greetingUrl) {
            $voice->play($greetingUrl);
        } else {
            $voice->say('Hi thank you for calling ' . $solicitor->name . '. I’m afraid all our staff are busy right now. Please leave a message with your name and contact details and we will call you back as soon as possible');
        }
        $voice->record([
            'maxLength'                     => 120,
            'playBeep'                      => true,
            'recordingStatusCallback'       => route('twilio.voice.recording'),
            'recordingStatusCallbackMethod' => 'POST',
            'recordingStatusCallbackEvent'  => 'completed',
        ]);

        return response((string) $voice, 200)->header('Content-Type', 'text/xml');
    }

    protected function markCommunicationMissed(?string $callSid): void
    {
        if (! $callSid) {
            return;
        }

        $call = Call::with('communication')
            ->where('provider_call_sid', $callSid)
            ->first();

        if (! $call || ! $call->communication) {
            return;
        }

        $communication = $call->communication;

        // Guard against overwriting any later/terminal status that might have
        // already been set (e.g. Voicemail by a racing recording callback).
        if (! in_array($communication->status, [
            CommunicationStatus::Initiated->value,
            CommunicationStatus::Ringing->value,
        ], true)) {
            return;
        }

        $communication->update([
            'status'   => CommunicationStatus::Missed->value,
            'ended_at' => $communication->ended_at ?? now(),
        ]);
    }

    /**
     * Mark the inbound call as answered and completed once the agent dial
     * finished successfully. The per-leg `answered` status callback usually
     * sets answered_at while the call is live; this is the terminal update
     * that also gives the call a final status and a backup answered_at in
     * case that callback was delayed or dropped.
     */
    protected function markCommunicationAnswered(?string $callSid, ?string $dialCallDuration): void
    {
        if (! $callSid) {
            return;
        }

        $call = Call::with('communication')
            ->where('provider_call_sid', $callSid)
            ->first();

        if (! $call || ! $call->communication) {
            return;
        }

        $communication = $call->communication;

        // Never clobber a terminal state that already arrived.
        if (in_array($communication->status, [
            CommunicationStatus::Completed->value,
            CommunicationStatus::Failed->value,
        ], true)) {
            return;
        }

        $communication->update([
            'status'      => CommunicationStatus::Voicemail->value,
            'answered_at' => $communication->answered_at ?? now(),
            'ended_at'    => $communication->ended_at ?? now(),
            'duration'    => is_numeric($dialCallDuration)
                ? (int) $dialCallDuration
                : $communication->duration,
        ]);
    }
}
