<?php

namespace App\Services\Communications\Placeholders;

use App\Enum\Contact\MessageChannel;
use App\Exceptions\Communications\UnknownPlaceholderException;
use App\Models\Contact\ContactTemplate;
use App\ValueObjects\Communications\PlaceholderContext;
use App\ValueObjects\Communications\RenderedMessage;

class TemplateRenderer
{
    private const TOKEN_REGEX = '/\{\{\s*([a-zA-Z0-9_.]+)(?:\s*\|\s*([^}]+?))?\s*\}\}/';

    public function __construct(
        protected PlaceholderRegistry $registry,
    ) {}

    /**
     * Return a list of tokens present in the template that the registry
     * does not know about.  Called at template save time.
     *
     * @return array<int, string>
     */
    public function validate(ContactTemplate $template): array
    {
        $unknown = [];
        foreach ($this->extractTokens($template->subject) as $token) {
            if (!$this->registry->has($token)) {
                $unknown[] = $token;
            }
        }
        foreach ($this->extractTokens($template->body_text_template) as $token) {
            if (!$this->registry->has($token)) {
                $unknown[] = $token;
            }
        }
        foreach ($this->extractTokens($template->body_html_template) as $token) {
            if (!$this->registry->has($token)) {
                $unknown[] = $token;
            }
        }
        return array_values(array_unique($unknown));
    }

    public function render(
        ContactTemplate $template,
        PlaceholderContext $ctx,
        bool $strict = false,
    ): RenderedMessage {
        $used = [];
        $unknown = [];

        $subject = $this->replace($template->subject, $ctx, $used, $unknown);
        $bodyText = $this->replace($template->body_text_template, $ctx, $used, $unknown);
        $bodyHtml = $template->isEmail()
            ? $this->replace($template->body_html_template, $ctx, $used, $unknown)
            : null;

        if ($template->includeSignature() && $ctx->sender) {
            if ($bodyHtml && $ctx->sender->signatureHtml()) {
                $bodyHtml .= config('contact.signature.html_separator', '<br/><br/>')
                    . $ctx->sender->signatureHtml();
            }
            if ($bodyText && $ctx->sender->signatureText()) {
                $bodyText .= config('contact.signature.text_separator', "\n\n")
                    . $ctx->sender->signatureText();
            }
        }

        if ($strict && !empty($unknown)) {
            throw new UnknownPlaceholderException(
                array_values(array_unique($unknown)),
                (int) ($template->id ?? 0),
            );
        }

        return new RenderedMessage(
            subject: $subject,
            bodyText: $bodyText,
            bodyHtml: $bodyHtml,
            placeholdersUsed: $used,
            unknownPlaceholders: array_values(array_unique($unknown)),
        );
    }

    /**
     * @return array<int, string>
     */
    public function extractTokens(?string $source): array
    {
        if ($source === null || $source === '') {
            return [];
        }
        preg_match_all(self::TOKEN_REGEX, $source, $m);
        return array_values(array_unique($m[1] ?? []));
    }

    private function replace(?string $source, PlaceholderContext $ctx, array &$used, array &$unknown): ?string
    {
        if ($source === null || $source === '') {
            return $source;
        }

        return preg_replace_callback(
            self::TOKEN_REGEX,
            function (array $match) use ($ctx, &$used, &$unknown) {
                $token = $match[1];
                $callerFallback = isset($match[2]) ? trim($match[2]) : null;

                [$value, $found] = $this->registry->resolve($token, $ctx, $callerFallback);

                if (!$found) {
                    $unknown[] = $token;
                    return $callerFallback ?? '';
                }

                $used[$token] = $value;
                return $value;
            },
            $source,
        ) ?? $source;
    }

    public function previewSegments(RenderedMessage $msg, MessageChannel $channel): array
    {
        if ($channel !== MessageChannel::SMS) {
            return ['length' => 0, 'segments' => 0, 'encoding' => 'none'];
        }

        $body = $msg->bodyText ?? '';
        $length = mb_strlen($body);
        $isUnicode = preg_match('/[^\x20-\x7E\r\n]/u', $body) === 1;

        $cfg = config('contact.sms');
        if ($isUnicode) {
            $single = $cfg['ucs2_single_segment'];
            $multi = $cfg['ucs2_multi_segment'];
            $encoding = 'UCS-2';
        } else {
            $single = $cfg['gsm7_single_segment'];
            $multi = $cfg['gsm7_multi_segment'];
            $encoding = 'GSM-7';
        }

        $segments = $length <= $single ? 1 : (int) ceil($length / $multi);

        return compact('length', 'segments', 'encoding');
    }
}
