<?php

declare(strict_types=1);

namespace Acms\Services\Mailer;

use Acms\Services\Facades\Application;
use Acms\Services\Facades\Common;
use Acms\Services\Facades\LocalStorage;
use Acms\Services\Facades\Logger as AcmsLogger;
use Acms\Services\Facades\Template as TplFacade;
use Acms\Services\Template\TwigPolicy;
use Acms\Traits\Utilities\FieldTrait;
use Field;
use Field_Validation;
use Template;
use ACMS_Corrector;

/**
 * メールテンプレート（ファイル経路・文字列直接入力）の描画を担う。
 *
 * Twig（tpl_twig 有効時）→ 既存 ACMS テンプレートパイプライン、の2段で処理する。
 * 「描画」に責務を限定し、送信（Engine）とは分離する（Symfony の Mailer/Mime/BodyRenderer 分割と同型）。
 */
class TemplateRenderer
{
    use FieldTrait;

    /**
     * メールテンプレートの解決
     *
     * @param string $path
     * @param Field $field
     * @param string|null $charset
     *
     * @return string
     */
    public function getMailTxt($path, $field, $charset = null)
    {
        if (!$path) {
            return '';
        }
        try {
            $tpl = LocalStorage::get($path, THEMES_DIR);
            if ($tpl === false) {
                return '';
            }
            if ($charset = detectEncode($tpl)) {
                $tpl = mb_convert_encoding($tpl, 'UTF-8', $charset);
            }
            if ($tpl === false) {
                return '';
            }

            // 公開側 main.php と同じく Twig → ACMS の 2 段で処理する (tpl_twig 無効時は renderMailTwigIfEnabled が
            // Twig を素通りさせる)。`.twig` 候補の優先解決は呼び出し元の findTemplate が済ませているため
            // ここでは再解決しない。テーマ階層を走査する Twig サービス (template.mail.twig) を使うので
            // include/extends・Markdown・extendsTwig 独自フィルタが公開側テーマと同じ仕組みで使える。
            // render 名に解決済みファイル名を渡し、autoescape は Twig 標準の 'name' 戦略
            // (拡張子で .txt → エスケープなし / それ以外 → html) に委ねる。
            $rendered = $this->renderMailTwigIfEnabled('template.mail.twig', $tpl, $field, LocalStorage::mbBasename($path));
            return $this->renderAcmsMailPipeline($rendered, $field);
        } catch (\Throwable $e) {
            AcmsLogger::warning(
                'メールテンプレートを取得できませんでした',
                Common::exceptionArray($e, ['path' => $path])
            );
            return '';
        }
    }

    /**
     * 文字列直接入力 (フォームの件名・本文など運営者の管理画面入力) からメール本文を組み立てる。
     *
     * tpl_twig 有効時はサンドボックス専用の Twig サービス (template.mail.twig.sandboxed) で評価してから
     * ACMS パイプラインへ渡す。ファイル経路 ({@see self::getMailTxt()}) と同じく
     * 「{@see self::renderMailTwigIfEnabled()} → {@see self::renderAcmsMailPipeline()}」の 2 段で処理する。
     *
     * @param string $txt
     * @param Field $field
     * @return string
     */
    public function getMailTxtFromTxt($txt, $field)
    {
        try {
            // サンドボックス経路は autoescape=false で論理名が不要なため $name は渡さない (renderMailTwigIfEnabled 参照)。
            $rendered = $this->renderMailTwigIfEnabled('template.mail.twig.sandboxed', $txt, $field);
            return $this->renderAcmsMailPipeline($rendered, $field);
        } catch (\Throwable $e) {
            AcmsLogger::warning(
                'メールテンプレートを組み立てできませんでした',
                Common::exceptionArray($e, ['text' => $txt])
            );
            return '';
        }
    }

    /**
     * メールテキストを既存の ACMS テンプレートパイプラインで組み立てる。
     *
     * var ブロック展開 → template.acms → Field 変数差し込み → IF 展開、の順に通す。
     * Twig 評価は呼び出し側 ({@see getMailTxt} / {@see getMailTxtFromTxt}) の責務とし、
     * このメソッドでは一切行わない。これにより、ファイル経路 (getMailTxt) で Twig レンダ済みの
     * 出力を再び Twig 評価してしまう二重レンダリング (= ユーザー入力フィールド値に含まれる
     * `{{ }}` がサンドボックス非経由で評価される SSTI) を防ぐ。
     *
     * @param string $txt
     * @param Field $field
     * @return string
     */
    private function renderAcmsMailPipeline($txt, $field)
    {
        global $extend_section_stack;
        $extend_section_stack = [];
        $acmsTplEngine = Application::make('template.acms');
        assert($acmsTplEngine instanceof \Acms\Services\Template\Acms);
        $txt = buildVarBlocks($txt, true);

        $acmsTplEngine->loadFromString($txt, '/', config('theme'), BID);
        $post = Field_Validation::singleton('post');
        $acmsTplEngine->setPostData($post);
        $acmsTplEngine->setNoBuildIF(true);
        $tpl = $acmsTplEngine->render();
        $extend_section_stack = [];

        $Tpl = new Template($tpl, new ACMS_Corrector());
        $vars = TplFacade::buildField($field, $Tpl);
        $Tpl->add(null, $vars);
        return buildVarBlocks(buildIF($Tpl->get()));
    }

    /**
     * メールテンプレ文字列を指定の Twig サービスで評価する (Twig → ACMS の 2 段の 1 段目)。
     *
     * 評価トリガーは公開側テーマと同じく tpl_twig のみ ({@see TwigPolicy::isEnabled()})。無効時は Twig を
     * 評価せず $tpl をそのまま返し、後段の ACMS パイプライン ({@see self::renderAcmsMailPipeline()}) だけで
     * 処理する。内容推測やフラグでの分岐はしない。これにより getMailTxt (ファイル経路) /
     * getMailTxtFromTxt (文字列直接入力) の双方が「renderMailTwigIfEnabled → renderAcmsMailPipeline」という
     * 同一の流れになる。
     *
     * 信頼境界はサービスキーで切り替える: template.mail.twig はファイル経路 (制作者) のフル機能、
     * template.mail.twig.sandboxed はフォーム直接入力 (運営者) のサンドボックス評価で SSTI を防ぐ。
     * フィールド値 (フォーム送信値など) はコンテキストとして渡すだけで、Twig は 1 回しか実行しないため
     * 送信者入力が Twig として再評価されることはない。
     *
     * Twig 評価が失敗した場合は本文を空に倒さず、Twig 未評価の元テキストへフォールバックする。
     * 「評価できなかったものは評価しない (リテラルのまま後段 ACMS パイプラインへ流す)」とすることで、
     * サイレントな本文・件名の欠落を防ぎつつ、未評価のため SSTI も生じない。
     *
     * 捕捉は {@see \Throwable} とする。Twig はレンダリング中の例外を RuntimeException にラップするが、
     * テンプレートのコンパイル時や {@see self::buildMailTwigContext()} (Field 構造化) で
     * `\Error` / `\TypeError` が出る経路もあり、`\Exception` だけではメール送信フローを
     * fatal 終了させ得るため。
     *
     * @param 'template.mail.twig'|'template.mail.twig.sandboxed' $serviceKey コンテナ登録キー
     * @param string $tpl 評価するテンプレート文字列
     * @param Field $field Twig コンテキストの元になる Field
     * @param string|null $name レンダの論理名。autoescape が 'name' 戦略の経路 (template.mail.twig) では
     *   この拡張子で html / エスケープなしを判定するためファイル名を渡す。autoescape=false のサンドボックス
     *   経路 (template.mail.twig.sandboxed) では論理名が autoescape に影響しないため null とし、
     *   renderString の既定名に委ねる
     * @return string Twig 評価結果。tpl_twig 無効時・評価失敗時は Twig 未評価の $tpl
     */
    private function renderMailTwigIfEnabled(string $serviceKey, string $tpl, $field, ?string $name = null): string
    {
        if (!TwigPolicy::isEnabled()) {
            return $tpl;
        }
        try {
            $twig = $this->mailTwig($serviceKey);
            $context = $this->buildMailTwigContext($field);
            // $name が null (サンドボックス経路) のときは renderString の既定名に委ねる。
            return $name === null
                ? $twig->renderString($tpl, $context)
                : $twig->renderString($tpl, $context, $name);
        } catch (\Throwable $e) {
            AcmsLogger::warning(
                'メールテンプレートの Twig 評価に失敗したため、Twig 未評価のまま後段処理を継続します',
                Common::exceptionArray($e, ['service' => $serviceKey])
            );
            return $tpl;
        }
    }

    /**
     * Field を Twig コンテキスト辞書に組み立てる。
     *
     * 公開側 V2 モジュール / API と同じ {@see FieldTrait::buildFieldTrait()} で構造化するため、
     * Twig からは `{{ name.value }}` / `{{ name.array }}` や、メディアフィールドの
     * `{{ photo.value.path }}` など公開側と同じ形でアクセスできる。Field オブジェクトは渡さず、
     * すべて連想配列に展開されるためサンドボックス (メソッド・プロパティアクセス禁止) とも整合する。
     *
     * `takeover` (Field 全体の acmsSerialize) はメール本文に露出すると情報漏えいになるため除外する。
     *
     * @param Field|null $field 受け取り側のシグネチャ統一のため null を許容する。null のときは空辞書を返す。
     * @return array<string, mixed>
     */
    private function buildMailTwigContext($field): array
    {
        if (!$field instanceof Field) {
            return [];
        }
        $context = $this->buildFieldTrait($field) ?? [];
        // takeover (Field/子 Field 全体の acmsSerialize) はメール本文に露出すると情報漏えいになるため除外する。
        foreach (array_keys($context) as $key) {
            if ($key === 'takeover' || str_ends_with((string) $key, ':takeover')) {
                unset($context[$key]);
            }
        }
        return $context;
    }

    /**
     * メール用 Twig サービスをサービスコンテナから取得する。
     *
     * 同じ Twig クラスを設定違いで別キー登録している:
     * - `template.mail.twig`           … ファイル経路 (制作者)。フル機能・非サンドボックス
     * - `template.mail.twig.sandboxed` … フォーム直接入力 (運営者)。サンドボックス
     *
     * @param 'template.mail.twig'|'template.mail.twig.sandboxed' $key コンテナ登録キー
     */
    private function mailTwig(string $key): \Acms\Services\Template\Twig
    {
        $twig = Application::make($key);
        assert($twig instanceof \Acms\Services\Template\Twig);
        return $twig;
    }
}
