<?php

namespace App\Actions\Communications;

use App\Enum\Communication\CallProvider;
use App\Enum\Communication\CommunicationChannel;
use App\Enum\Communication\CommunicationDirection;
use App\Enum\Communication\CommunicationEventType;
use App\Enum\Communication\CommunicationStatus;
use App\Models\Communication\Communication;
use App\Models\Communication\SolicitorPhoneNumber;
use App\Models\Solicitor\Solicitor;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use RuntimeException;

/**
 * StartOutboundCallAction — the single creation path for every outbound browser call.
 *
 * Shared by the per-client dialler (CallManager), institution calls (CallInstitute)
 * and the global ad-hoc dialpad (CallOverlay). It only writes the Communication +
 * Call + event records and returns the loaded Communication; it deliberately does
 * NOT dispatch any browser/Livewire events — the calling component is responsible
 * for firing `dialler-call-started` and `twilio-start-browser-call`.
 *
 * Whatever lands in `calls.to_number` is what TwilioController::handleOutbound()
 * dials, so this action is the only place that needs to know how to build a call.
 */
class StartOutboundCallAction
{
    /**
     * @param  Model|null  $subject  the communicable the call is *about* — a Client for
     *                               both client and institution calls (so the call shows
     *                               in the client's activity feed), or null for a
     *                               client-less ad-hoc call.
     * @param  string  $toNumber     the number to dial (normalised to E.164 here).
     * @param  string  $targetLabel  human label for who is actually being dialled.
     * @param  string  $targetType   'client' | 'institution' | 'manual'.
     * @param  string  $callType     drives the from-line choice and the compliance gate.
     * @param  bool    $record       false suppresses recording (e.g. client-less ad-hoc).
     */
    public function handle(
        ?Model $subject,
        string $toNumber,
        string $targetLabel,
        string $targetType,
        string $callType,
        ?int   $claimId = null,
        ?int   $solicitorId = null,
        bool   $record = true,
        string $uiOrigin = 'call_manager',
    ): Communication {
        $to = self::normalizeNumber($toNumber);

        if (! $to) {
            throw new RuntimeException('A valid phone number is required to place the call.');
        }

        $solicitorId ??= $subject?->solicitor_id ?? app(Solicitor::class)?->id;

        if (! $solicitorId) {
            throw new RuntimeException('Unable to determine the solicitor for this call.');
        }

        $fromLine = $this->resolveFromLine($callType, $solicitorId);

        return DB::transaction(function () use (
            $subject, $to, $targetLabel, $targetType, $callType,
            $claimId, $solicitorId, $record, $uiOrigin, $fromLine
        ) {
            $communication = Communication::query()->create([
                'solicitor_id'      => $solicitorId,
                'claim_id'          => $claimId,
                'agent_id'          => auth()->id(),
                'communicable_type' => $subject?->getMorphClass(),
                'communicable_id'   => $subject?->getKey(),
                'channel'           => CommunicationChannel::Voice->value,
                'direction'         => CommunicationDirection::Outbound->value,
                'status'            => CommunicationStatus::Initiated->value,
                'started_at'        => now(),
                'preview'           => 'Outbound call to ' . $targetLabel,
                'meta'              => [
                    'provider'  => CallProvider::Twilio->value,
                    'ui_origin' => $uiOrigin,
                ],
            ]);

            $call = $communication->call()->create([
                'solicitor_id'              => $solicitorId,
                'assigned_agent_id'         => auth()->id(),
                'solicitor_phone_number_id' => $fromLine->id,
                'provider'                  => CallProvider::Twilio->value,
                'provider_call_sid'         => null,
                'from_number'               => $fromLine->normalized_phone_number,
                'to_number'                 => $to,
                'phone_number_type_slug'    => $fromLine->type?->slug,
                'call_type'                 => $callType,
                'target_label'              => $targetLabel,
                'target_type'               => $targetType,
                'is_recorded'               => $record && (bool) ($fromLine->type?->auto_record ?? true),
                'recording_notice_played'   => false,
                'meta'                      => ['browser_sdk' => true],
            ]);

            $communication->events()->create([
                'solicitor_id' => $solicitorId,
                'call_id'      => $call->id,
                'agent_id'     => auth()->id(),
                'event'        => CommunicationEventType::OutboundCallCreated->value,
                'source'       => 'agent',
                'description'  => 'Outbound browser call created.',
                'occurred_at'  => now(),
                'payload'      => [
                    'from_number'  => $fromLine->normalized_phone_number,
                    'to_number'    => $to,
                    'target_label' => $targetLabel,
                    'call_type'    => $callType,
                ],
            ]);

            return $communication->load('call');
        });
    }

    /**
     * Resolve the "from" solicitor line by the TYPE of call. Client calls go out on
     * the client line; institution / ad-hoc calls use the external line. Keying off
     * the call type (not the communicable's class) matters because institution calls
     * keep the Client as the communicable but must still dial from the external line.
     */
    protected function resolveFromLine(string $callType, int $solicitorId): SolicitorPhoneNumber
    {
        $preferredSlug = match ($callType) {
            'client', 'manual' => 'client_line',
            'institution'      => 'external_line',
            default            => 'external_line',
        };

        $number = SolicitorPhoneNumber::query()
            ->with('type')
            ->where('solicitor_id', $solicitorId)
            ->where('is_active', true)
            ->whereHas('type', fn ($q) => $q->where('slug', $preferredSlug))
            ->orderByDesc('is_primary')
            ->first()
            ?? SolicitorPhoneNumber::query()
                ->with('type')
                ->where('solicitor_id', $solicitorId)
                ->where('is_active', true)
                ->orderByDesc('is_primary')
                ->first();

        if (! $number) {
            throw new RuntimeException('No active solicitor phone number is configured.');
        }

        return $number;
    }

    /**
     * Normalise to E.164 for Twilio. Permissive — accepts UK national (0…),
     * 00-prefixed international and already-E.164 input — unlike the strict UK-only
     * PhoneNumber value object which rejects non-UK numbers (institutions can be
     * anywhere). Returns null when there is nothing dialable.
     */
    public static function normalizeNumber(?string $value): ?string
    {
        $value = trim(preg_replace('/[^\d+]/', '', (string) $value) ?? '');

        if ($value === '' || $value === '+') {
            return null;
        }

        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;
    }
}
