<?php

namespace App\Services\Sms;

use App\Models\SmsLog;
use App\Models\SmsProfile;
use App\Models\SmsSender;
use App\Models\SmsTemplate;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;

class SmsService
{
    public function __construct(
        protected SmsClient $client,
    ) {}

    /**
     * Normalize whitespace to avoid accidental Unicode switching and provider quirks.
     * - Convert non-breaking space (U+00A0) and tabs to regular spaces
     * - Preserve newlines
     */
    protected function normalizeForSms(string $text): string
    {
        // Replace NBSP (\xC2\xA0 in UTF-8) and tabs with regular space
        $text = str_replace(["\u{00A0}", "\t"], ' ', $text);

        // Optionally collapse CRLF to LF (providers typically accept both); keep as-is otherwise
        return $text;
    }

    /**
     * Heuristic GSM-7 detection: treat any codepoint >= 0x80 as UCS-2 except € which is allowed via extension table.
     */
    protected function isGsm7(string $text): bool
    {
        $len = mb_strlen($text, 'UTF-8');
        for ($i = 0; $i < $len; $i++) {
            $ch = mb_substr($text, $i, 1, 'UTF-8');
            if ($ch === '€') {
                continue;
            }
            // Fast ASCII check: multibyte UTF-8 means non-ASCII (>= 0x80)
            if (strlen($ch) > 1) {
                return false;
            }
        }

        return true;
    }

    /**
     * Count GSM-7 units, where extension table chars cost 2 (ESC + char).
     */
    protected function gsm7Length(string $text): int
    {
        static $extended = null;
        if ($extended === null) {
            $extended = ['^', '{', '}', '\\', '[', '~', ']', '|'];
        }

        $len = 0;
        $strlen = mb_strlen($text, 'UTF-8');
        for ($i = 0; $i < $strlen; $i++) {
            $ch = mb_substr($text, $i, 1, 'UTF-8');
            if ($ch === '€' || in_array($ch, $extended, true)) {
                $len += 2;
            } else {
                $len += 1;
            }
        }

        return $len;
    }

    /**
     * Truncate text to provider hard limits: 640 GSM-7 units or 320 UCS-2 units.
     */
    public function enforceLengthLimit(string $text): string
    {
        $text = $this->normalizeForSms($text);
        $isGsm = $this->isGsm7($text);
        $limit = $isGsm ? 640 : 320;

        if ($isGsm) {
            // Fast-path: if within limit, return
            if ($this->gsm7Length($text) <= $limit) {
                return $text;
            }
            // Truncate respecting extension char cost
            $out = '';
            $acc = 0;
            $strlen = mb_strlen($text, 'UTF-8');
            for ($i = 0; $i < $strlen; $i++) {
                $ch = mb_substr($text, $i, 1, 'UTF-8');
                $cost = ($ch === '€' || in_array($ch, ['^', '{', '}', '\\', '[', '~', ']', '|'], true)) ? 2 : 1;
                if ($acc + $cost > $limit) {
                    break;
                }
                $out .= $ch;
                $acc += $cost;
            }

            return $out;
        }

        // UCS-2: count by UTF-16 code units; approximate via mb_substr slicing by codepoints
        // We use mb_substr which handles Unicode correctly for most cases; providers count 1 per code unit.
        // For BMP characters (most cases like Slovenian diacritics), 1 codepoint ~= 1 code unit.
        if (mb_strlen($text, 'UTF-8') <= $limit) {
            return $text;
        }

        return mb_substr($text, 0, $limit, 'UTF-8');
    }

    /**
     * Send a raw text message.
     */
    public function sendRaw(SmsProfile $profile, string $to, string $content, ?SmsSender $sender = null, ?string $countryCode = null, bool $deliveryReport = false, ?string $clientReference = null): SmsLog
    {
        return DB::transaction(function () use ($profile, $to, $content, $sender, $countryCode, $deliveryReport, $clientReference): SmsLog {
            // Enforce provider hard length limits before logging/sending
            $content = $this->enforceLengthLimit($content);
            $log = new SmsLog([
                'uuid' => (string) Str::uuid(),
                'profile_id' => $profile->id,
                'to_number' => $to,
                'sender' => $sender?->sname,
                'message' => $content,
                'status' => 'queued',
                'queued_at' => now(),
            ]);
            $log->save();

            $result = $this->client->send($profile, new SmsMessage(
                to: $to,
                content: $content,
                sender: $sender?->sname,
                senderPhone: $sender?->phone_number,
                countryCode: $countryCode,
                deliveryReport: $deliveryReport,
                clientReference: $clientReference,
            ));

            if ($result->status === 'sent') {
                $log->status = 'sent';
                $log->sent_at = now();
            } else {
                $log->status = 'failed';
                $log->failed_at = now();
            }
            $log->provider_message_id = $result->providerMessageId;
            $log->cost = $result->cost;
            $log->currency = $result->currency;
            $log->meta = $result->meta;
            $log->save();

            return $log;
        });
    }

    /**
     * Render an SMS from template and send.
     */
    public function sendFromTemplate(SmsTemplate $template, string $to, array $variables = [], ?SmsProfile $profile = null, ?SmsSender $sender = null, ?string $countryCode = null, bool $deliveryReport = false, ?string $clientReference = null): SmsLog
    {
        $profile = $profile ?: $template->defaultProfile;
        if (! $profile) {
            throw new \InvalidArgumentException('SMS profile is required to send a message.');
        }

        $sender = $sender ?: $template->defaultSender;
        $content = $this->renderContent($template->content, $variables);

        $log = $this->sendRaw($profile, $to, $content, $sender, $countryCode, $deliveryReport, $clientReference);
        $log->template_id = $template->id;
        $log->save();

        return $log;
    }

    public function renderContent(string $content, array $vars): string
    {
        // Support {token} and {nested.keys} using dot-notation lookup
        $resolver = function (array $arr, string $path) {
            if (array_key_exists($path, $arr)) {
                return $arr[$path];
            }
            $segments = explode('.', $path);
            $cur = $arr;
            foreach ($segments as $seg) {
                if (is_array($cur) && array_key_exists($seg, $cur)) {
                    $cur = $cur[$seg];
                } else {
                    return null;
                }
            }

            return $cur;
        };

        return preg_replace_callback('/\{([a-zA-Z0-9_\.]+)\}/', function ($m) use ($vars, $resolver) {
            $key = $m[1];
            $val = $resolver($vars, $key);

            return $val !== null ? (string) $val : $m[0];
        }, $content);
    }

    /**
     * Format a number to EU style: thousands separated by '.', decimals by ','.
     */
    public function formatAmountEu(mixed $value, int $decimals = 2): string
    {
        if ($value === null || $value === '') {
            return number_format(0, $decimals, ',', '.');
        }
        $str = (string) $value;
        // Normalize possible EU-style input like "1.234,56" to standard for float casting
        if (str_contains($str, ',')) {
            $str = str_replace(['.', ','], ['', '.'], $str);
        }
        $num = (float) $str;

        return number_format($num, $decimals, ',', '.');
    }

    /**
     * Get current credit balance from provider.
     */
    public function getCreditBalance(SmsProfile $profile): string
    {
        return $this->client->getCreditBalance($profile);
    }

    /**
     * Get price quote(s) from provider.
     * Returns array of strings as provided by the API.
     */
    public function getPriceQuotes(SmsProfile $profile): array
    {
        return $this->client->getPriceQuotes($profile);
    }
}
