<?php

namespace App\Components\Dialler;

use App\Actions\Communications\TransferCallAction;
use App\Enum\Communication\CommunicationEventType;
use App\Enum\Communication\CommunicationStatus;
use App\Models\Communication\Call;
use App\Models\Communication\Communication;
use App\Models\Dialler\AgentStatus as AgentStatusModel;
use App\Models\Solicitor\Solicitor;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Livewire\Attributes\Computed;
use Livewire\Attributes\On;
use Livewire\Component;

/**
 * CallOverlay — global softphone widget (mounted once in the layout header)
 *
 * Combines:
 *  • Agent presence status selector (always visible in the header)
 *  • Full call lifecycle overlay: incoming → ringing → connected → wrap-up
 *
 * Phases:
 *   idle      — overlay hidden; only the status dropdown is shown
 *   incoming  — inbound call awaiting accept / decline
 *   ringing   — outbound call connecting, or inbound call being answered
 *   connected — live call with mute + hangup + timer
 *   wrapup    — post-call disposition form
 *   error     — call failed; brief error message before returning to idle
 */
class CallOverlay extends Component {

    // ── Agent presence ────────────────────────────────────────────────────────
    public ?AgentStatusModel $agentStatus = null;
    public string $agentPresence = 'offline';   // available|busy|wrap_up|do_not_disturb|offline

    // ── Call overlay visibility & phase ──────────────────────────────────────
    public bool   $visible = false;
    public string $phase   = 'idle';   // idle|incoming|ringing|connected|wrapup|error

    // ── Call metadata ─────────────────────────────────────────────────────────
    public string  $direction   = 'outbound';
    public ?string $callerLabel = null;
    public ?string $callerUrl   = null;
    public ?string $callSid     = null;
    public ?string $callerFrom  = null;   // raw From value from Twilio SDK (used for fallback lookup)

    // ── DB record IDs ─────────────────────────────────────────────────────────
    public ?int $communicationId = null;
    public ?int $callId          = null;

    // ── In-call state ─────────────────────────────────────────────────────────
    public bool    $muted        = false;
    public ?string $errorMessage = null;

    // ── Transfer state ────────────────────────────────────────────────────────
    public bool    $showTransferPicker = false;
    public bool    $transferring       = false;
    public ?int    $transferredToId    = null;
    public ?string $transferredToName  = null;

    // ── Wrap-up form ──────────────────────────────────────────────────────────
    public string  $disposition      = '';
    public string  $notes            = '';
    public bool    $callbackRequired = false;
    public ?string $callbackAt       = null;

    // ─────────────────────────────────────────────────────────────────────────

    public function mount(): void
    {
        $solicitorId = app(Solicitor::class)?->id;

        $this->agentStatus = AgentStatusModel::firstOrCreate(
            ['agent_id' => Auth::id(), 'solicitor_id' => $solicitorId],
            ['status'   => 'offline'],
        );

        $this->agentPresence = $this->agentStatus->status ?? 'offline';

        // Stamp the heartbeat immediately on page load so the routing rule's
        // recency check passes without waiting for the first wire:poll cycle.
        if ($this->agentPresence !== 'offline') {
            $this->agentStatus->update(['status_changed_at' => now()]);
        }
    }

    // ── Agent presence ────────────────────────────────────────────────────────

    public function touchPresence(): void
    {
        if ($this->agentStatus && $this->agentPresence !== 'offline') {
            $this->agentStatus->update(['status_changed_at' => now()]);
        }
    }

    public function setPresence(string $status): void
    {
        $allowed = ['available', 'busy', 'wrap_up', 'do_not_disturb', 'offline'];

        if (! in_array($status, $allowed, true)) {
            return;
        }

        $this->agentStatus->update([
            'status'            => $status,
            'status_changed_at' => now(),
        ]);

        $this->agentPresence = $status;
    }

    // ── Outbound call initiated (from CallManager::startCall()) ──────────────

    #[On('dialler-call-started')]
    public function handleCallStarted(
        int    $communication_id,
        int    $call_id,
        string $label,
        string $direction = 'outbound'
    ): void {
        $this->resetCallState();

        $this->communicationId = $communication_id;
        $this->callId          = $call_id;
        $this->callerLabel     = $label;
        $this->direction       = $direction;
        $this->phase           = 'ringing';
        $this->visible         = true;
    }

    // ── Inbound call ─────────────────────────────────────────────────────────

    #[On('twilio-incoming-call')]
    public function handleIncomingCall($from = null, $call_sid = null): void
    {
        // Never interrupt an active call (wrapup can be interrupted for transfer-back calls)
        if (in_array($this->phase, ['ringing', 'connected'])) {
            return;
        }

        $this->resetCallState();

        $this->direction   = 'inbound';
        $this->callerLabel = is_string($from) ? $from : 'Unknown Caller';
        $this->callerFrom  = is_string($from) ? $from : null;
        $this->callSid     = is_string($call_sid) ? $call_sid : null;
        $this->phase       = 'incoming';
        $this->visible     = true;

        if ($this->callerFrom) {
            $this->resolveCallerByPhone($this->callerFrom);
        }

        // Webhook fires before the browser SDK event, so the DB record should
        // already exist — resolve it now so wrap-up can save immediately.
        if ($this->callSid) {
            $this->resolveInboundRecord();
        }
    }

    // ── SDK lifecycle events ──────────────────────────────────────────────────

    #[On('twilio-call-ringing')]
    public function handleCallRinging(): void
    {
        // Phase is already 'ringing' for outbound; no change needed.
        // For inbound, this fires after accept() — keep 'ringing' until accepted.
    }

    #[On('twilio-call-accepted')]
    public function handleCallAccepted(): void
    {
        // For OUTBOUND calls the SDK 'accept' event fires when the browser↔Twilio
        // WebRTC media leg is established (for ringback audio) — NOT when the
        // PSTN recipient picks up. We must NOT transition to 'connected' here;
        // pollOutboundConnected() drives that transition from answered_at.
        //
        // This guard is the source of truth even if twilio.js still emits the
        // event from a cached bundle: an outbound call can only enter the
        // connected phase via the poll.
        if ($this->direction === 'outbound' && $this->phase !== 'connected') {
            return;
        }

        // Retry inbound record lookup in case it wasn't ready on incoming event
        if ($this->direction === 'inbound' && ! $this->communicationId && $this->callSid) {
            $this->resolveInboundRecord();
        }

        $this->phase   = 'connected';
        $this->muted   = false;
        $this->visible = true;

        // Auto-set agent to busy while on a live call
        if ($this->agentPresence === 'available') {
            $this->setPresence('busy');
        }
    }

    // ── Outbound progress polling ─────────────────────────────────────────────
    //
    // The Twilio SDK 'accept' event for outbound calls fires when the
    // browser↔Twilio WebRTC leg is established (for ringback) — NOT when
    // the PSTN recipient picks up. We can only know that from the server-
    // side status callback, which writes answered_at on `in-progress`.
    //
    // While an outbound call is in 'ringing' phase the blade view runs
    // this poll. As soon as answered_at is set we transition to connected
    // and dispatch the same browser event the Alpine timer listens for.

    public function pollOutboundConnected(): void
    {
        if ($this->direction !== 'outbound') {
            return;
        }

        if ($this->phase !== 'ringing') {
            return;
        }

        if (! $this->communicationId) {
            return;
        }

        $answered = Communication::where('id', $this->communicationId)
            ->whereNotNull('answered_at')
            ->exists();

        if (! $answered) {
            return;
        }

        $this->phase   = 'connected';
        $this->muted   = false;
        $this->visible = true;

        if ($this->agentPresence === 'available') {
            $this->setPresence('busy');
        }

        // Start the Alpine duration timer (listens on @twilio-call-accepted.window).
        // handleCallAccepted() also fires on the same dispatch but is idempotent.
        $this->dispatch('twilio-call-accepted');
    }

    #[On('twilio-call-disconnected')]
    public function handleCallDisconnected(): void
    {
        $this->muted = false;

        // The Twilio SDK fires `accept` when the browser→Twilio WebRTC leg is
        // established for ringback audio — NOT when the PSTN recipient picks up.
        // The only reliable signal that the remote party actually answered is
        // answered_at, which is set by the status callback when in-progress fires.
        // If it is null the call was never truly answered — skip wrapup.
        if ($this->communicationId) {
            $answered = Communication::where('id', $this->communicationId)
                ->whereNotNull('answered_at')
                ->exists();

            if (! $answered) {
                $this->endCallWithoutAnswer(CommunicationStatus::Missed);
                return;
            }
        }

        $this->phase = $this->communicationId ? 'wrapup' : 'idle';

        // Move from busy → wrap_up so routing rules don't queue new calls
        // during the post-call wrap-up period.
        if ($this->agentPresence === 'busy') {
            $this->setPresence('wrap_up');
        }

        if ($this->phase === 'idle') {
            $this->dismiss();
        }
    }

    #[On('twilio-call-cancelled')]
    public function handleCallCancelled(): void
    {
        // Agent cancelled before the recipient answered — that's a missed call
        // from the contact's perspective, not a technical failure.
        $this->endCallWithoutAnswer(CommunicationStatus::Missed);
    }

    #[On('twilio-call-rejected')]
    public function handleCallRejected(): void
    {
        $this->dismiss();
    }

    #[On('twilio-call-error')]
    public function handleCallError($message = null): void
    {
        $this->errorMessage = is_string($message) ? $message : 'The call failed.';
        $this->muted        = false;

        $this->phase   = ($this->communicationId && $this->phase === 'connected')
            ? 'wrapup'
            : 'error';
        $this->visible = true;
    }

    // ── Agent call-control actions ────────────────────────────────────────────

    public function accept(): void
    {
        $this->phase = 'ringing';
        $this->dispatch('twilio-accept-browser-call');
    }

    public function reject(): void
    {
        $this->dispatch('twilio-reject-browser-call');
        $this->dismiss();
    }

    public function hangup(): void
    {
        $this->dispatch('twilio-hangup-browser-call');
    }

    public function toggleMute(): void
    {
        if ($this->phase !== 'connected') {
            return;
        }

        $this->muted = ! $this->muted;
        $this->dispatch($this->muted ? 'twilio-mute-browser-call' : 'twilio-unmute-browser-call');
    }

    // ── Wrap-up ───────────────────────────────────────────────────────────────

    public function saveWrapUp(\App\Services\NoteService $noteService): void
    {
        $this->validate([
            'disposition'      => [$this->transferredToId ? 'nullable' : 'required', 'string', 'max:100'],
            'notes'            => ['nullable', 'string'],
            'callbackRequired' => ['required', 'boolean'],
            'callbackAt'       => ['nullable', 'date', 'required_if:callbackRequired,true'],
        ], [
            'callbackAt.required_if' => 'Please choose a callback date and time.',
        ]);

        abort_unless($this->communicationId && $this->callId, 403);

        DB::transaction(function () use ($noteService) {
            $communication = Communication::query()
                ->with(['call', 'communicable'])
                ->findOrFail($this->communicationId);

            $call     = $communication->call;
            $call->update([
                'notes'       => filled($this->notes) ? $this->notes : null,
                'disposition' => filled($this->disposition) ? $this->disposition : null,
                'callback_at' => $this->callbackRequired && $this->callbackAt ? $this->callbackAt : null,
            ]);

            $dirLabel = $this->direction === 'inbound' ? 'Inbound call from' : 'Outbound call to';
            $preview  = $dirLabel . ' ' . ($this->callerLabel ?? 'unknown');

            if (filled($this->disposition)) {
                $preview .= ' · ' . str($this->disposition)->replace('_', ' ')->title();
            }

            $communication->update(['preview' => $preview]);

            $communication->events()->create([
                'solicitor_id' => $communication->solicitor_id,
                'call_id'      => $call->id,
                'agent_id'     => auth()->id(),
                'event'        => CommunicationEventType::CallDispositionSaved->value,
                'source'       => 'agent',
                'description'  => 'Call wrap-up saved.',
                'occurred_at'  => now(),
                'payload'      => [
                    'disposition'       => $this->disposition,
                    'callback_required' => $this->callbackRequired,
                    'callback_at'       => $this->callbackAt,
                ],
            ]);

            if (filled($this->notes)) {
                $communication->events()->create([
                    'solicitor_id' => $communication->solicitor_id,
                    'call_id'      => $call->id,
                    'agent_id'     => auth()->id(),
                    'event'        => CommunicationEventType::CallNotesSaved->value,
                    'source'       => 'agent',
                    'description'  => 'Call notes saved.',
                    'occurred_at'  => now(),
                    'payload'      => ['notes' => $this->notes],
                ]);

                if ($communication->communicable instanceof \App\Models\Client\Client) {
                    $noteService->addNote($communication->communicable, $this->notes);
                }
            }

            if ($this->callbackRequired) {
                $communication->events()->create([
                    'solicitor_id' => $communication->solicitor_id,
                    'call_id'      => $call->id,
                    'agent_id'     => auth()->id(),
                    'event'        => CommunicationEventType::CallbackRequested->value,
                    'source'       => 'agent',
                    'description'  => 'Callback requested.',
                    'occurred_at'  => now(),
                    'payload'      => ['callback_at' => $this->callbackAt],
                ]);
            }
        });

        $this->dispatch('toast', message: 'Call wrap-up saved.', type: 'success');
        $this->dismiss();
    }

    public function dismissWrapUp(): void
    {
        if ($this->communicationId && $this->callId) {
            $call = Call::with('communication')->find($this->callId);
            $call?->communication?->events()->create([
                'solicitor_id' => $call->communication->solicitor_id,
                'call_id'      => $call->id,
                'agent_id'     => auth()->id(),
                'event'        => CommunicationEventType::CallWrapUpSkipped->value,
                'source'       => 'agent',
                'description'  => 'Wrap-up dismissed without saving a disposition.',
                'occurred_at'  => now(),
                'payload'      => [],
            ]);
        }

        $this->dismiss();
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    protected function endCallWithoutAnswer(CommunicationStatus $status): void
    {
        if ($this->communicationId) {
            Communication::where('id', $this->communicationId)
                ->whereIn('status', [
                    CommunicationStatus::Initiated->value,
                    CommunicationStatus::Ringing->value,
                ])
                ->update([
                    'status'   => $status->value,
                    'ended_at' => now(),
                ]);
        }

        $this->dismiss();
    }

    public function dismiss(): void
    {
        // Restore availability after a call ends. Only auto-flip when the agent
        // was put into busy/wrap_up by the call lifecycle — leave do_not_disturb
        // and offline alone so agents who deliberately set those aren't interrupted.
        if (in_array($this->agentPresence, ['busy', 'wrap_up'], true)) {
            $this->setPresence('available');
        }

        $this->phase              = 'idle';
        $this->visible            = false;
        $this->direction          = 'outbound';
        $this->callerLabel        = null;
        $this->callerUrl          = null;
        $this->callerFrom         = null;
        $this->callSid            = null;
        $this->communicationId    = null;
        $this->callId             = null;
        $this->muted              = false;
        $this->errorMessage       = null;
        $this->disposition        = '';
        $this->notes              = '';
        $this->callbackRequired   = false;
        $this->callbackAt         = null;
        $this->showTransferPicker = false;
        $this->transferring       = false;
        $this->transferredToId    = null;
        $this->transferredToName  = null;
        $this->resetValidation();
    }

    protected function resetCallState(): void
    {
        $this->muted              = false;
        $this->errorMessage       = null;
        $this->communicationId    = null;
        $this->callId             = null;
        $this->callerUrl          = null;
        $this->callerFrom         = null;
        $this->callSid            = null;
        $this->disposition        = '';
        $this->notes              = '';
        $this->callbackRequired   = false;
        $this->callbackAt         = null;
        $this->showTransferPicker = false;
        $this->transferring       = false;
        $this->transferredToId    = null;
        $this->transferredToName  = null;
        $this->resetValidation();
    }

    protected function resolveInboundRecord(): void
    {
        // Primary: match by the call SID Twilio gives the browser SDK.
        // For <Dial><Client> inbound calls this is typically the parent (PSTN) SID
        // which matches provider_call_sid. If it doesn't match (e.g. SDK sends the
        // child leg SID), fall back to the caller's phone number.
        $call = null;

        if ($this->callSid) {
            $call = Call::with('communication.communicable')
                ->where('provider_call_sid', $this->callSid)
                ->first();
        }

        if (! $call && $this->callerFrom && ! str_starts_with($this->callerFrom, 'client:')) {
            $solicitorId = app(Solicitor::class)?->id;
            $call        = Call::with('communication.communicable')
                ->whereHas('communication', function ($q) use ($solicitorId) {
                    $q->where('direction', 'inbound')
                      ->where('solicitor_id', $solicitorId);
                })
                ->where('from_number', $this->callerFrom)
                ->latest('id')
                ->first();
        }

        if (! $call) {
            return;
        }

        $this->callId          = $call->id;
        $this->communicationId = $call->communication_id;

        $communicable = $call->communication?->communicable;

        if (! $communicable) {
            return;
        }

        if (method_exists($communicable, 'getCallableLabel')) {
            $this->callerLabel = $communicable->getCallableLabel();
        }

        if ($communicable instanceof \App\Models\Client\Client) {
            $this->callerUrl = route('clients.show', $communicable);
        }
    }

    // ── UI helpers ────────────────────────────────────────────────────────────

    public function presenceButtonClass(): string
    {
        return match ($this->agentPresence) {
            'available'      => 'btn-success',
            'busy'           => 'btn-danger',
            'wrap_up'        => 'btn-warning',
            'do_not_disturb' => 'btn-secondary',
            default          => 'btn-secondary',
        };
    }

    public function presenceDotClass(): string
    {
        return match ($this->agentPresence) {
            'available'      => 'bg-success',
            'busy'           => 'bg-danger',
            'wrap_up'        => 'bg-warning',
            'do_not_disturb' => 'bg-danger',
            default          => 'bg-secondary',
        };
    }

    // ── Computed ──────────────────────────────────────────────────────────────

    #[Computed]
    public function dispositionOptions(): array
    {
        return [
            'transferred'            => 'Transferred',
            'connected_successfully' => 'Connected Successfully',
            'left_voicemail'         => 'Left Voicemail',
            'no_answer'              => 'No Answer',
            'busy'                   => 'Busy',
            'wrong_number'           => 'Wrong Number',
            'callback_required'      => 'Callback Required',
            'follow_up_needed'       => 'Follow-Up Needed',
        ];
    }

    #[Computed]
    public function availableAgents(): array
    {
        $solicitorId = app(Solicitor::class)?->id;

        return AgentStatusModel::query()
            ->with('agent:id,name_first,name_last')
            ->where('solicitor_id', $solicitorId)
            ->where('agent_id', '!=', Auth::id())
            ->where('status', 'available')
            ->get()
            ->map(fn ($s) => [
                'id'   => $s->agent_id,
                'name' => trim($s->agent?->name_first . ' ' . $s->agent?->name_last),
            ])
            ->values()
            ->toArray();
    }

    // ── Transfer ──────────────────────────────────────────────────────────────

    public function openTransferPicker(): void
    {
        if ($this->phase !== 'connected') {
            return;
        }
        $this->showTransferPicker = true;
    }

    public function closeTransferPicker(): void
    {
        $this->showTransferPicker = false;
    }

    public function initiateTransfer(int $targetAgentId, TransferCallAction $action): void
    {
        if (! $this->callId || $this->transferring) {
            return;
        }

        $this->transferring = true;

        try {
            $agent = $action->handle($this->callId, $targetAgentId);

            $this->transferredToId    = $agent->id;
            $this->transferredToName  = $agent->name_first . ' ' . $agent->name_last;
            $this->showTransferPicker = false;
            $this->disposition        = 'transferred';
            $this->phase              = 'wrapup';
            $this->visible            = true;

        } catch (\Throwable $e) {
            logger()->debug($e->getMessage());
            $this->logTransferFailed($e->getMessage());
            $this->dispatch('toast', message: 'Transfer failed: ' . $e->getMessage(), type: 'error');
        } finally {
            $this->transferring = false;
        }
    }

    protected function logTransferFailed(string $error): void
    {
        if (! $this->callId) {
            return;
        }

        $call = Call::with('communication')->find($this->callId);
        $call?->communication?->events()->create([
            'solicitor_id' => $call->communication->solicitor_id,
            'call_id'      => $call->id,
            'agent_id'     => auth()->id(),
            'event'        => CommunicationEventType::CallTransferFailed->value,
            'source'       => 'agent',
            'description'  => 'Transfer failed: ' . $error,
            'occurred_at'  => now(),
            'payload'      => ['error' => $error],
        ]);
    }

    protected function resolveCallerByPhone(string $from): void
    {
        if (str_starts_with($from, 'client:')) {
            return;
        }

        $normalized = $this->normalizePhoneNumber($from);
        if ($normalized === '') {
            return;
        }

        $solicitorId = app(Solicitor::class)?->id;
        if (! $solicitorId) {
            return;
        }

        $client = \App\Models\Client\Client::where('solicitor_id', $solicitorId)
            ->where('phone_number', $normalized)
            ->first();

        if (! $client) {
            return;
        }

        $this->callerLabel = $client->getCallableLabel();
        $this->callerUrl   = route('clients.show', $client);
    }

    protected function normalizePhoneNumber(string $value): string
    {
        $value = trim(preg_replace('/[^\d+]/', '', $value) ?? '');

        if (str_starts_with($value, '00')) {
            $value = '+' . substr($value, 2);
        }

        if (! str_starts_with($value, '+')) {
            $countryCode = config('services.dialler.default_country_code', '44');
            $value       = str_starts_with($value, '0')
                ? '+' . $countryCode . substr($value, 1)
                : '+' . $value;
        }

        return $value;
    }

    public function render()
    {
        return view('components.dialler.call-overlay');
    }
}
