<?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 BRACE_TOKEN_REGEX = '/\{\{\s*([a-zA-Z0-9_.]+)(?:\s*\|\s*([^}]+?))?\s*\}\}/';

    /**
     * DSAR letter templates use %token% syntax (no pipe-fallback support —
     * existing content never used one). Kept separate from the brace regex so
     * existing DsarType content renders unchanged.
     */
    private const PERCENT_TOKEN_REGEX = '/%([a-zA-Z0-9_]+)%/';

    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
    {
        return $this->unknownTokensIn([
            $template->subject,
            $template->body_text_template,
            $template->body_html_template,
        ]);
    }

    /**
     * Generalized version of validate() for any set of raw strings (e.g. DSAR
     * letter content), not just a ContactTemplate's fields.
     *
     * @param array<int, ?string> $sources
     * @return array<int, string>
     */
    public function unknownTokensIn(array $sources, string $style = 'braces'): array
    {
        $unknown = [];
        foreach ($sources as $source) {
            foreach ($this->extractTokens($source, $style) 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, string $style = 'braces'): array
    {
        if ($source === null || $source === '') {
            return [];
        }
        preg_match_all($this->tokenRegex($style), $source, $m);
        return array_values(array_unique($m[1] ?? []));
    }

    /**
     * Render a raw string (not tied to a ContactTemplate) against the shared
     * registry. Used by DSAR letter content, which resolves %token% syntax
     * through the same context-aware registry as {{ }} Contact Templates.
     *
     * @return array{0: ?string, 1: array<string, string>, 2: array<int, string>} [rendered, used, unknown]
     */
    public function renderRaw(?string $source, PlaceholderContext $ctx, bool $strict = false, string $style = 'braces'): array
    {
        $used = [];
        $unknown = [];

        $rendered = $this->replace($source, $ctx, $used, $unknown, $style);

        if ($strict && !empty($unknown)) {
            throw new UnknownPlaceholderException(array_values(array_unique($unknown)), 0);
        }

        return [$rendered, $used, array_values(array_unique($unknown))];
    }

    private function tokenRegex(string $style): string
    {
        return $style === 'percent' ? self::PERCENT_TOKEN_REGEX : self::BRACE_TOKEN_REGEX;
    }

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

        return preg_replace_callback(
            $this->tokenRegex($style),
            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');
    }
}
