<?php

if ( ! defined( 'ABSPATH' ) ) {

    exit;

}



/**

 * 投稿完了通知 (メール送信)。

 *

 * 投稿が完了したとき (= AIAP_Poster::phase_finalize の末尾で `do_action( 'aiap_post_created' )` 発火時)

 * に、指定したメールアドレスへ投稿情報を送信する。

 *

 * SMTP の経路は以下のいずれか:

 *  1. アクティブな主要 SMTP プラグイン (WP Mail SMTP / Easy WP SMTP / Post SMTP /

 *     FluentSMTP / SMTP Mailer) を検出した場合: そのプラグインが phpmailer_init で

 *     PHPMailer を上書きしているため、本クラスは何もしない (= プラグインに委譲)。

 *  2. 上記が見つからない場合: 本クラス自身が phpmailer_init で PHPMailer を SMTP

 *     設定で上書きする (= ユーザーが設定画面で入力した SMTP 情報を使う)。

 *

 * パスワードは AIAP_Crypto で暗号化して options テーブルに保存する (claude.md L25 機密情報保護)。

 */

class AIAP_Notifier {



    /** options テーブルのキー */

    const OPTION_KEY = 'aiap_notify_settings';



    /**

     * 検出対象の主要 SMTP プラグイン。

     *

     * キー: プラグインのスラッグ (basename フォルダ/ファイル.php)

     * 値:   表示名 + 設定画面の admin_url 相対パス

     *

     * is_plugin_active() で活性化されているかを判定する。最初にヒットしたものを返す

     * (= 複数活性化されている場合は配列順で最初の 1 つを採用、L17 横展開で衝突懸念は薄い)。

     */

    private static function get_smtp_plugin_map() {

        return array(

            'wp-mail-smtp/wp_mail_smtp.php'   => array( 'name' => 'WP Mail SMTP',  'url' => 'admin.php?page=wp-mail-smtp' ),

            'easy-wp-smtp/easy-wp-smtp.php'   => array( 'name' => 'Easy WP SMTP',  'url' => 'admin.php?page=swpsmtp_settings' ),

            'post-smtp/postman-smtp.php'      => array( 'name' => 'Post SMTP',     'url' => 'admin.php?page=postman' ),

            'fluent-smtp/fluent-smtp.php'     => array( 'name' => 'FluentSMTP',    'url' => 'admin.php?page=fluent-mail' ),

            'smtp-mailer/main.php'            => array( 'name' => 'SMTP Mailer',   'url' => 'options-general.php?page=smtp-mailer' ),

        );

    }



    /**

     * フック登録。プラグイン初期化時 (ai-auto-poster.php の require_once 直後) に呼ぶ。

     *

     * wp_mail_from / wp_mail_from_name フィルタは「差出人アドレスと名前」を決定する WP 標準の経路。

     * WordPress は wp_mail() 内部で必ずこれらのフィルタを通すため、phpmailer_init で setFrom() を

     * 呼んでも、フィルタ側の値で上書きされる。よって差出人を確実に反映させるには両方のフックを併用する。

     */

    public static function register_hooks() {

        add_action( 'aiap_post_created', array( __CLASS__, 'send_post_notification' ), 10, 5 );

        add_action( 'phpmailer_init',    array( __CLASS__, 'configure_phpmailer' ), 99 );

        add_filter( 'wp_mail_from',      array( __CLASS__, 'filter_mail_from' ), 10, 1 );

        add_filter( 'wp_mail_from_name', array( __CLASS__, 'filter_mail_from_name' ), 10, 1 );

    }



    /**

     * 差出人メールアドレスの上書き (= wp_mail_from フィルタ)。

     *

     * SMTP プラグインが検出された場合はそちらに委譲 (= フィルタを通過させる)。

     * ユーザーが smtp_from_email を設定していない場合も WP デフォルトに任せる。

     *

     * @param string $email WordPress が決定したデフォルト差出人 (= wordpress@{host})

     * @return string 上書き後 / 元の値

     */

    public static function filter_mail_from( $email ) {

        if ( self::detect_smtp_plugin() !== null ) {

            return $email; // SMTP プラグインに委譲

        }

        $s = self::get_settings();

        if ( ! empty( $s['smtp_from_email'] ) && is_email( $s['smtp_from_email'] ) ) {

            return (string) $s['smtp_from_email'];

        }

        return $email;

    }



    /**

     * 差出人名の上書き (= wp_mail_from_name フィルタ)。

     */

    public static function filter_mail_from_name( $name ) {

        if ( self::detect_smtp_plugin() !== null ) {

            return $name;

        }

        $s = self::get_settings();

        if ( ! empty( $s['smtp_from_name'] ) ) {

            return (string) $s['smtp_from_name'];

        }

        return $name;

    }



    /**

     * 設定のデフォルト値。

     */

    public static function get_settings_defaults() {

        return array(

            'enabled'         => 0,        // 通知有効化トグル

            'email'           => '',       // 通知先メールアドレス

            'smtp_host'       => '',       // SMTP ホスト

            'smtp_port'       => 587,      // ポート (=既定 587)

            'smtp_encryption' => 'tls',    // none / tls / ssl

            'smtp_auth'       => 0,        // 認証使用フラグ

            'smtp_username'   => '',       // ユーザー名

            'smtp_password'   => '',       // パスワード (AIAP_Crypto::encrypt() で暗号化された値)

            'smtp_from_email' => '',       // 送信元アドレス

            'smtp_from_name'  => '',       // 送信元名

        );

    }



    /**

     * 一時的な設定上書き (= 送信テスト時にフォーム値を一瞬だけ反映するための機構)。

     *

     * AIAP_Admin_Ajax::ajax_notify_test がフォームの $_POST を normalize_input で整形し、

     * set_runtime_override で本クラスに渡す。get_settings は DB ではなくこの override を返す。

     * テスト送信が終わったら finally で clear_runtime_override を呼んで必ずクリアする。

     *

     * → ユーザーは「保存」を押さずに、画面に入力した内容でテストできる (UX 改善)。

     * → 通常の投稿完了通知 (= aiap_post_created 経由) は override 未設定なので、従来通り DB 値を使う。

     *

     * @var array|null

     */

    private static $runtime_override = null;



    /**

     * CID (Content-ID) でメールに埋め込む画像の一時保管領域。

     *

     * メールクライアントによって WebP は非対応 (Apple Mail / Outlook / Yahoo Mail 等) なので、

     * WebP 画像は JPEG に変換した上でメールに「埋め込み添付」(= CID) する。CID 埋め込みなら

     * 外部 URL 取得がないので「画像を読み込む」ボタンを押さずに自動表示される。

     *

     * 仕組み:

     *   1. send_post_notification 内で build_email_body を呼ぶ際に、画像バイナリと CID をここに登録

     *   2. wp_mail() が内部で phpmailer_init を発火 → configure_phpmailer が AddStringEmbeddedImage で添付

     *   3. send_post_notification の finally で必ずクリア (= 次回送信時に残らない)

     *

     * 各要素の構造: array( 'cid' => string, 'binary' => string, 'filename' => string, 'mime' => string )

     *

     * @var array

     */

    private static $cid_attachments = array();



    /**

     * 一時的な設定上書きをセット (= 送信テスト用)。

     *

     * @param array $settings get_settings_defaults と同じ構造の連想配列

     */

    public static function set_runtime_override( $settings ) {

        self::$runtime_override = is_array( $settings ) ? $settings : null;

    }



    /**

     * 一時的な設定上書きをクリア。

     */

    public static function clear_runtime_override() {

        self::$runtime_override = null;

    }



    /**

     * 設定を取得。

     *

     * - runtime override がセットされていればそれを最優先 (= 送信テストでフォーム値を使う場合)

     * - 通常は DB の保存値とデフォルト値をマージ

     */

    public static function get_settings() {

        if ( is_array( self::$runtime_override ) ) {

            return array_merge( self::get_settings_defaults(), self::$runtime_override );

        }

        $stored = get_option( self::OPTION_KEY, array() );

        if ( ! is_array( $stored ) ) {

            $stored = array();

        }

        return array_merge( self::get_settings_defaults(), $stored );

    }



    /**

     * フォーム入力を検証・サニタイズして、設定形式の連想配列に変換する。

     *

     * save_settings (= DB 保存用) と ajax_notify_test (= runtime override 用) の両方から

     * 呼ばれる共通処理 (claude.md L47 共通化)。パスワードの取り扱いと暗号化判定は呼び出し側に任せる。

     *

     * 戻り値の 'smtp_password_input' はフォームに入力された生の文字列 (= 未暗号化、空可)。

     * save_settings はこれを「空なら DB 既存値維持 / 入力ありなら暗号化」と処理し、

     * ajax_notify_test は「空なら DB 既存値を復号 / 入力ありはそのまま」と処理する。

     *

     * @param array $input フォーム送信値 ($_POST 等)

     * @return array|WP_Error 整形済み配列 / 検証エラー

     */

    public static function normalize_input( $input ) {

        $defaults = self::get_settings_defaults();

        $new = array();



        $new['enabled'] = ! empty( $input['enabled'] ) ? 1 : 0;



        $email = isset( $input['email'] ) ? sanitize_email( wp_unslash( (string) $input['email'] ) ) : '';

        if ( $email !== '' && ! is_email( $email ) ) {

            return new WP_Error( 'invalid_email', __( 'The notification address looks incorrect. Please check it.', 'ai-auto-poster' ) );

        }

        $new['email'] = $email;



        $new['smtp_host'] = isset( $input['smtp_host'] ) ? sanitize_text_field( wp_unslash( (string) $input['smtp_host'] ) ) : '';



        $port = isset( $input['smtp_port'] ) ? (int) $input['smtp_port'] : $defaults['smtp_port'];

        $new['smtp_port'] = max( 1, min( 65535, $port ) );



        $enc = isset( $input['smtp_encryption'] ) ? (string) $input['smtp_encryption'] : 'tls';

        if ( ! in_array( $enc, array( 'none', 'tls', 'ssl' ), true ) ) {

            $enc = 'tls';

        }

        $new['smtp_encryption'] = $enc;



        $new['smtp_auth']     = ! empty( $input['smtp_auth'] ) ? 1 : 0;

        $new['smtp_username'] = isset( $input['smtp_username'] ) ? sanitize_text_field( wp_unslash( (string) $input['smtp_username'] ) ) : '';



        // パスワードはフォームに入力された値を平文のまま返す (= 暗号化判定は呼び出し側)

        $new['smtp_password_input'] = isset( $input['smtp_password'] ) ? (string) wp_unslash( $input['smtp_password'] ) : '';



        $from_email = isset( $input['smtp_from_email'] ) ? sanitize_email( wp_unslash( (string) $input['smtp_from_email'] ) ) : '';

        if ( $from_email !== '' && ! is_email( $from_email ) ) {

            return new WP_Error( 'invalid_sender_email', __( 'The sender address looks incorrect. Please check it.', 'ai-auto-poster' ) );

        }

        $new['smtp_from_email'] = $from_email;

        $new['smtp_from_name']  = isset( $input['smtp_from_name'] ) ? sanitize_text_field( wp_unslash( (string) $input['smtp_from_name'] ) ) : '';



        return $new;

    }



    /**

     * 設定を保存。normalize_input でサニタイズしてからパスワードを暗号化し、DB に保存する。

     *

     * @param array $input  ユーザー入力 (= フォーム送信値)

     * @return array {

     *     @type bool   $saved 保存成否

     *     @type string $error エラー内容 (失敗時のみ)

     * }

     */

    public static function save_settings( $input ) {

        $normalized = self::normalize_input( $input );

        if ( is_wp_error( $normalized ) ) {

            return array( 'saved' => false, 'error' => $normalized->get_error_message() );

        }



        $current = self::get_settings();



        // パスワード: 空入力なら既存値を維持、入力ありなら暗号化して保存

        $raw_pw = $normalized['smtp_password_input'];

        unset( $normalized['smtp_password_input'] );

        if ( $raw_pw === '' ) {

            $normalized['smtp_password'] = isset( $current['smtp_password'] ) ? (string) $current['smtp_password'] : '';

        } else {

            if ( class_exists( 'AIAP_Crypto' ) && AIAP_Crypto::is_available() ) {

                $encrypted = AIAP_Crypto::encrypt( $raw_pw );

                if ( $encrypted === false ) {

                    return array( 'saved' => false, 'error' => __( 'Could not save the password securely. Please contact your administrator.', 'ai-auto-poster' ) );

                }

                $normalized['smtp_password'] = $encrypted;

            } else {

                // 暗号化不能なら平文を保存しない (claude.md L25 機密情報保護)

                return array( 'saved' => false, 'error' => __( 'Could not save the password securely. Please contact your administrator.', 'ai-auto-poster' ) );

            }

        }



        update_option( self::OPTION_KEY, $normalized, false );

        return array( 'saved' => true );

    }



    /**

     * 通知が有効化されているか (= トグル ON かつ通知先メールアドレスが設定されている)。

     */

    public static function is_enabled() {

        $s = self::get_settings();

        return ! empty( $s['enabled'] ) && $s['email'] !== '' && is_email( $s['email'] );

    }



    /**

     * アクティブな主要 SMTP プラグインを検出。なければ null。

     *

     * @return array|null {

     *     @type string $slug プラグインのスラッグ

     *     @type string $name 表示名

     *     @type string $url  設定画面の絶対 URL

     * }

     */

    public static function detect_smtp_plugin() {

        if ( ! function_exists( 'is_plugin_active' ) ) {

            // 管理画面外から呼ばれる可能性に備えて plugin.php をロード

            if ( file_exists( ABSPATH . 'wp-admin/includes/plugin.php' ) ) {

                require_once ABSPATH . 'wp-admin/includes/plugin.php';

            } else {

                return null;

            }

        }

        foreach ( self::get_smtp_plugin_map() as $slug => $info ) {

            if ( is_plugin_active( $slug ) ) {

                return array(

                    'slug' => $slug,

                    'name' => $info['name'],

                    'url'  => admin_url( $info['url'] ),

                );

            }

        }

        return null;

    }



    /**

     * 投稿完了時のメール送信 (= aiap_post_created アクションの handler)。

     *

     * @param int    $post_id    投稿 ID

     * @param array  $result     AI 生成結果 (title 等)

     * @param array  $settings   AI 設定 (provider 等)

     * @param string $source     'src_manual' / 'src_auto' / 'src_cron'

     * @param string $task_label タスクラベル (ログ用)

     */

    public static function send_post_notification( $post_id, $result, $settings, $source, $task_label ) {

        // 通知無効 (ユーザー設定 OFF) はサイレント return: 想定された正常動作のためログ不要

        if ( ! self::is_enabled() ) {

            return;

        }



        $s  = self::get_settings();

        $to = (string) $s['email'];

        if ( ! is_email( $to ) ) {

            return;

        }



        $post_id = (int) $post_id;

        if ( $post_id <= 0 ) {

            return;

        }



        // 旧実装は AIAP_Generated_Posts_Table::build_item_for_post() を呼んでいたが、

        // 本クラスは `extends WP_List_Table` で cron 経路 (= 自動投稿時の通知) では

        // WP_List_Table の依存により require_once が失敗し、メール通知が**サイレント

        // 不送信**になる不具合があった (= 自動投稿でメール通知ログが出ない原因)。

        // 本体ロジックを AIAP_Poster::build_item_for_post に移したため、WP_List_Table

        // 依存を完全に断つ。AIAP_Poster は cron / 管理画面 / AJAX のいずれでもロード済。

        $item = AIAP_Poster::build_item_for_post( $post_id );

        if ( ! is_array( $item ) ) {

            return;

        }



        $site  = wp_strip_all_tags( get_bloginfo( 'name' ) );

        $title = wp_strip_all_tags( (string) $item['title'] );



        /* translators: 1: site name, 2: post title */

        $subject = sprintf( __( '[%1$s] Post created: %2$s', 'ai-auto-poster' ), $site, $title );



        $body    = self::build_email_body( $item );

        $headers = array( 'Content-Type: text/html; charset=UTF-8' );



        // build_email_body 内で self::$cid_attachments に画像バイナリが登録されている可能性あり。

        // wp_mail() が内部で phpmailer_init を発火 → configure_phpmailer で AddStringEmbeddedImage が

        // 呼ばれる。送信成否にかかわらず必ず finally でクリアする (= 次回送信時に残らないように)。

        try {

            $sent = wp_mail( $to, $subject, $body, $headers );

        } finally {

            self::$cid_attachments = array();

        }



        if ( class_exists( 'AIAP_Logger' ) ) {

            if ( $sent ) {

                AIAP_Logger::add( 'info', 'log_notify_sent', array( $to, $post_id ) );

            } else {

                AIAP_Logger::add( 'warning', 'log_notify_failed', array( $to, $post_id ) );

            }

        }

    }



    /**

     * メール本文 (HTML) を組み立てる。

     *

     * 投稿履歴 UI と表示が一致するよう、整形ロジックは AIAP_Poster::format_* の

     * 共通ヘルパを通す (= AIAP_Notifier は AIAP_Generated_Posts_Table への依存ゼロ。

     * cron 経路で WP_List_Table 未ロードによりサイレント失敗していた問題を構造的に解消)。

     *

     * @param array $item AIAP_Poster::build_item_for_post() の戻り値

     */

    private static function build_email_body( $item ) {

        // 投稿履歴 UI と単一情報源の共通ヘルパで整形 (= 表示文言の差異を構造的に防ぐ)

        $title    = isset( $item['title'] ) && $item['title'] !== '' ? (string) $item['title'] : __( '(no title)', 'ai-auto-poster' );

        $edit_url = isset( $item['edit_url'] ) ? (string) $item['edit_url'] : '';

        $title_html = $edit_url !== ''

            ? sprintf( '<a href="%s">%s</a>', esc_url( $edit_url ), esc_html( $title ) )

            : esc_html( $title );



        $ts         = isset( $item['date'] ) ? (int) $item['date'] : 0;

        $created_at = $ts > 0 ? esc_html( wp_date( 'Y-m-d H:i:s', $ts ) ) : '—';



        $writer    = esc_html( AIAP_Poster::format_provider_label( isset( $item['provider'] ) ? $item['provider'] : '' ) );

        $task_name = esc_html( AIAP_Poster::format_task_name( $item ) );

        $status    = esc_html( AIAP_Poster::format_status_label( isset( $item['status'] ) ? $item['status'] : '' ) );



        // 画像 (= AI 生成画像のサムネイル)。CID 埋め込みでメールに同梱する。

        //

        // 多くのメールクライアント (Apple Mail / Outlook / Yahoo Mail / Thunderbird) は WebP 形式を

        // デコードできないので、WebP 画像は JPEG に変換してから CID 埋め込みする。JPEG/PNG/GIF は

        // 変換せずそのまま埋め込む。CID 埋め込みなので「画像を読み込む」ボタン不要で自動表示される。

        //

        // 画像の選定 (= どのサムネイル) は $item['thumb_id'] 経由で投稿履歴と同じ (= L45/L47 共通化)。

        $thumb_id   = isset( $item['thumb_id'] ) ? (int) $item['thumb_id'] : 0;

        $image_html = '';

        if ( $thumb_id > 0 ) {

            $image_html = self::build_image_cid_html( $thumb_id );

        }

        if ( $image_html === '' ) {

            // 画像なし時のプレースホルダ (= 投稿履歴の column_image と同じ「画像なし」状態)

            $image_html = '<span style="display:inline-block;width:60px;height:60px;background:#e5e7eb;border:1px solid #d1d5db;border-radius:4px;"></span>';

        }



        // 操作リンクはメールクライアントで dashicons フォントが効かないため、シンプルなテキストリンクに置き換える。

        // ただし URL 自体は同じ ($item['view_url'] / $item['edit_url']) を使うので「投稿履歴と同じ遷移先」になる。

        $view_url = isset( $item['view_url'] ) ? (string) $item['view_url'] : '';

        $edit_url = isset( $item['edit_url'] ) ? (string) $item['edit_url'] : '';

        $actions_parts = array();

        if ( $view_url !== '' ) {

            $actions_parts[] = '<a href="' . esc_url( $view_url ) . '" style="color:#2271b1;text-decoration:underline;">' . esc_html__( 'View post', 'ai-auto-poster' ) . '</a>';

        }

        if ( $edit_url !== '' ) {

            $actions_parts[] = '<a href="' . esc_url( $edit_url ) . '" style="color:#2271b1;text-decoration:underline;">' . esc_html__( 'Edit post', 'ai-auto-poster' ) . '</a>';

        }

        $actions_html = implode( ' &nbsp;|&nbsp; ', $actions_parts );



        // 投稿履歴の get_columns() と同じ並びで縦に並べる (= AI Generated Title / Created At / Provider /

        // Task / Post Status / AI Generated Image / Actions)。ラベルも投稿履歴と同じ msgid を使う。

        //

        // L21 整合: column_* メソッドは内部で esc_html を済ませて HTML 文字列を返すため、ここで再エスケープしない

        // (= 二重エスケープを避ける。例: タスク名「タスク #1」が「タスク #1」のような表示にならない)。

        $rows = array(

            array( __( 'AI Generated Title',  'ai-auto-poster' ), $title_html ),

            array( __( 'Created At',          'ai-auto-poster' ), $created_at ),

            array( __( 'Provider',            'ai-auto-poster' ), $writer ),

            array( __( 'Task',                'ai-auto-poster' ), $task_name ),

            array( __( 'Post Status',         'ai-auto-poster' ), $status ),

            array( __( 'AI Generated Image',  'ai-auto-poster' ), $image_html ),

            array( __( 'Actions',             'ai-auto-poster' ), $actions_html ),

        );



        // スマホ対応のため <table> をやめ、各項目を <div> の上下配置 (ラベル上、値下) で縦並びにする。

        // メールクライアントは @media クエリ非サポートが多いので、レスポンシブ対応は構造で行う (= 縦並びなら

        // 画面幅に応じて自然に折り返される)。間隔は密に詰まらないよう margin-bottom と margin-top を確保。

        $rows_html = '';

        foreach ( $rows as $r ) {

            $rows_html .= '<div style="margin-bottom:24px;">'

                . '<div style="color:#555;font-weight:600;font-size:12px;line-height:1.4;">' . esc_html( $r[0] ) . '</div>'

                . '<div style="font-size:14px;line-height:1.5;margin-top:8px;color:#222;">' . $r[1] . '</div>'

                . '</div>';

        }



        $body  = '<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>';

        $body .= '<body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#222;line-height:1.6;margin:0;padding:16px;background:#f5f5f5;">';

        $body .= '<div style="max-width:600px;margin:0 auto;background:#fff;border:1px solid #ddd;border-radius:6px;padding:20px;">';

        $body .= '<h2 style="margin:0 0 20px;font-size:18px;color:#1d2327;border-bottom:1px solid #eee;padding-bottom:12px;">' . esc_html__( 'A new post has been created', 'ai-auto-poster' ) . '</h2>';

        $body .= $rows_html;

        $body .= '<hr style="border:0;border-top:1px solid #ddd;margin:24px 0 12px;">';

        $body .= '<p style="font-size:12px;color:#888;margin:0;">' . esc_html__( 'Sent by AI Auto Poster', 'ai-auto-poster' ) . '</p>';

        $body .= '</div></body></html>';



        return $body;

    }



    /**

     * サムネイル画像を CID 埋め込み用に処理し、本文に挿入する img タグを返す。

     *

     * 多くのメールクライアント (Apple Mail / Outlook / Yahoo Mail / Thunderbird) は WebP 形式を

     * デコードできないので、WebP は JPEG に変換してから埋め込む。JPEG/PNG/GIF はそのまま。

     *

     * @param int $thumb_id 添付ファイル ID

     * @return string <img src="cid:..."> タグ、変換失敗時は空文字

     */

    private static function build_image_cid_html( $thumb_id ) {

        $file_path = get_attached_file( $thumb_id );

        if ( ! $file_path || ! file_exists( $file_path ) ) {

            return '';

        }

        if ( ! function_exists( 'wp_get_image_editor' ) ) {

            return '';

        }

        $editor = wp_get_image_editor( $file_path );

        if ( is_wp_error( $editor ) ) {

            return '';

        }



        // 拡張子で出力形式を決定。

        // - WebP/AVIF: 多くのメールクライアントが非対応なので JPEG に変換 (透過は黒背景になる)

        // - PNG: 透過情報を保つため PNG のまま (= JPEG 化すると透過部分が黒く塗りつぶされる)

        // - JPEG/GIF: そのままの形式で再エンコード

        $ext = strtolower( pathinfo( $file_path, PATHINFO_EXTENSION ) );

        if ( in_array( $ext, array( 'webp', 'avif' ), true ) ) {

            $out_mime = 'image/jpeg';

            $out_ext  = 'jpg';

        } elseif ( $ext === 'png' ) {

            $out_mime = 'image/png';

            $out_ext  = 'png';

        } elseif ( $ext === 'gif' ) {

            $out_mime = 'image/gif';

            $out_ext  = 'gif';

        } elseif ( in_array( $ext, array( 'jpg', 'jpeg' ), true ) ) {

            $out_mime = 'image/jpeg';

            $out_ext  = 'jpg';

        } else {

            // 未知の拡張子は安全のため埋め込まない

            return '';

        }



        // アスペクト比保持で最大 120x120 にリサイズ (= crop=false、正方形クロップしない)。

        // メール容量も節約 (= フルサイズ画像が数 MB の場合の対策)。

        $editor->resize( 120, 120, false );



        // tempnam で一時ファイルを作って読み出す (= WP_Image_Editor::save はファイルパス必須)。

        // sys_get_temp_dir() は PHP 標準、cron context でも利用可能。

        $tmp = tempnam( sys_get_temp_dir(), 'aiap-thumb-' );

        if ( $tmp === false ) {

            return '';

        }

        $tmp_out = $tmp . '.' . $out_ext;

        // Bug fix: 旧実装は @rename の戻り値を見ずに先へ進めていたため、rename が失敗した場合に

        // 元の tempnam ファイル ($tmp) が残り、別パス ($tmp_out) も後段の save 失敗時のみ削除と

        // 不整合だった。失敗時は $tmp を確実に削除し、成功時は古い $tmp パスが既に消えているため

        // $tmp_out のみ削除すればよい。

        if ( ! @rename( $tmp, $tmp_out ) ) {

            @unlink( $tmp );

            return '';

        }



        $saved = $editor->save( $tmp_out, $out_mime );

        if ( is_wp_error( $saved ) || ! file_exists( $tmp_out ) ) {

            @unlink( $tmp_out );

            return '';

        }



        $binary = @file_get_contents( $tmp_out );

        @unlink( $tmp_out );



        if ( $binary === false || $binary === '' ) {

            return '';

        }



        // リサイズ後の実寸を取得 (= width/height 属性でメールクライアントに正しく伝える)

        $img_width  = isset( $saved['width'] )  ? (int) $saved['width']  : 120;

        $img_height = isset( $saved['height'] ) ? (int) $saved['height'] : 120;



        // CID は post_id ベースで一意に。同一メール内で重複しない単純な形式。

        $cid = 'aiap_thumb_' . $thumb_id;



        self::$cid_attachments[] = array(

            'cid'      => $cid,

            'binary'   => $binary,

            'filename' => 'thumbnail.' . $out_ext,

            'mime'     => $out_mime,

        );



        return sprintf(

            '<img src="cid:%s" width="%d" height="%d" alt="" style="display:block;border:1px solid #ddd;border-radius:4px;max-width:120px;height:auto;">',

            esc_attr( $cid ),

            $img_width,

            $img_height

        );

    }



    /**

     * テストメールを送信 (= 設定画面の「送信テスト」ボタンから呼ばれる)。

     *

     * 設定済みの通知先メールアドレスに対し、保存済みの SMTP 設定経由でテストメールを送る。

     * 未保存値ではテストできない (= 保存→テストの順、シンプル化、L49 過剰設計回避)。

     *

     * wp_mail が false を返した場合、wp_mail_failed フック経由で PHPMailer エラー詳細を捕捉して返す。

     *

     * @return string|WP_Error 送信成功時は送信先アドレス、失敗時は WP_Error

     */

    public static function send_test_email() {

        $s  = self::get_settings();

        $to = (string) $s['email'];



        if ( $to === '' || ! is_email( $to ) ) {

            return new WP_Error( 'invalid_email', __( 'Notification email address is not configured. Save the address first.', 'ai-auto-poster' ) );

        }



        $site = wp_strip_all_tags( get_bloginfo( 'name' ) );

        /* translators: %s: site name */

        $subject = sprintf( __( '[%s] AI Auto Poster test email', 'ai-auto-poster' ), $site );

        $body    = self::build_test_email_body();

        $headers = array( 'Content-Type: text/html; charset=UTF-8' );



        // wp_mail_failed フックで PHPMailer のエラー詳細を捕捉する

        // (= wp_mail() の戻り値は bool だけで原因が分からないため)

        $captured_error = '';

        $capture = function ( $wp_error ) use ( &$captured_error ) {

            if ( is_wp_error( $wp_error ) ) {

                $captured_error = $wp_error->get_error_message();

            }

        };

        add_action( 'wp_mail_failed', $capture );



        $sent = wp_mail( $to, $subject, $body, $headers );



        remove_action( 'wp_mail_failed', $capture );



        if ( ! $sent ) {

            $msg = $captured_error !== '' ? $captured_error : __( 'Email send failed (no details available).', 'ai-auto-poster' );

            return new WP_Error( 'send_failed', $msg );

        }



        return $to;

    }



    /**

     * テストメール本文 (HTML) を組み立てる。

     *

     * build_email_body と HTML スケルトンが似ているが、責務が異なる (= 投稿情報 vs テストメッセージ)

     * ため敢えて関数を分離している (claude.md L49 過剰設計回避)。

     */

    private static function build_test_email_body() {

        $site     = wp_strip_all_tags( get_bloginfo( 'name' ) );

        $time     = current_time( 'mysql' );

        $detected = self::detect_smtp_plugin();

        $smtp_via = $detected !== null

            ? $detected['name']

            : ( ! empty( self::get_settings()['smtp_host'] )

                ? __( 'Custom SMTP (this plugin)', 'ai-auto-poster' )

                : __( 'WordPress default (PHP mail)', 'ai-auto-poster' ) );



        $h = function ( $s ) { return esc_html( (string) $s ); };



        $rows = array(

            array( __( 'Site',      'ai-auto-poster' ), $h( $site ) ),

            array( __( 'Sent at',   'ai-auto-poster' ), $h( $time ) ),

            array( __( 'SMTP via',  'ai-auto-poster' ), $h( $smtp_via ) ),

        );

        $rows_html = '';

        foreach ( $rows as $r ) {

            $rows_html .= '<tr><th align="left" style="padding:6px 12px 6px 0;color:#555;font-weight:600;white-space:nowrap;">' . $r[0] . '</th><td style="padding:6px 0;">' . $r[1] . '</td></tr>';

        }



        $body  = '<!DOCTYPE html><html><head><meta charset="UTF-8"></head><body style="font-family:-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;color:#222;line-height:1.6;margin:0;padding:24px;">';

        $body .= '<div style="max-width:600px;margin:0 auto;">';

        $body .= '<h2 style="margin:0 0 16px;font-size:18px;color:#1d2327;">' . esc_html__( 'AI Auto Poster — Test email', 'ai-auto-poster' ) . '</h2>';

        $body .= '<p style="margin:0 0 16px;font-size:14px;">' . esc_html__( 'If you can read this, the notification path is working.', 'ai-auto-poster' ) . '</p>';

        $body .= '<table style="border-collapse:collapse;font-size:14px;width:100%;">' . $rows_html . '</table>';

        $body .= '<hr style="border:0;border-top:1px solid #ddd;margin:32px 0 12px;">';

        $body .= '<p style="font-size:12px;color:#888;margin:0;">' . esc_html__( 'Sent by AI Auto Poster', 'ai-auto-poster' ) . '</p>';

        $body .= '</div></body></html>';



        return $body;

    }



    /**

     * PHPMailer を SMTP 設定で上書き (= phpmailer_init アクションの handler)。

     *

     * 動作条件:

     *   1. 主要 SMTP プラグインが検出されない (= プラグインに委譲しない)

     *   2. ユーザーが SMTP ホストを設定している (= 未設定なら WP デフォルトに任せる)

     *

     * これにより、SMTP プラグインと本機能が二重に PHPMailer を上書きすることを防ぐ。

     *

     * @param PHPMailer $phpmailer (WP コアの phpmailer_init アクションが渡す)

     */

    public static function configure_phpmailer( $phpmailer ) {

        // CID 画像添付 (= 投稿完了通知メールの WebP→JPEG 変換済みサムネイル等)。

        //

        // この処理は SMTP 設定の有無に関係なく行う必要があるため、SMTP プラグイン検出による

        // 早期 return より前に置く。AddStringEmbeddedImage は PHPMailer 標準メソッドなので

        // SMTP プラグイン使用時 (= PHPMailer インスタンスはそのまま) でも問題なく動作する。

        //

        // $cid_attachments は send_post_notification で wp_mail を呼ぶ前に build_email_body 経由で

        // 設定され、wp_mail 後の finally で必ずクリアされる。空配列なら何もしない (= 通常の wp_mail 呼出には影響しない)。

        if ( ! empty( self::$cid_attachments ) ) {

            foreach ( self::$cid_attachments as $att ) {

                if ( ! is_array( $att ) || empty( $att['binary'] ) || empty( $att['cid'] ) ) {

                    continue;

                }

                $phpmailer->AddStringEmbeddedImage(

                    $att['binary'],

                    $att['cid'],

                    isset( $att['filename'] ) ? $att['filename'] : 'image.jpg',

                    'base64',

                    isset( $att['mime'] ) ? $att['mime'] : 'image/jpeg'

                );

            }

        }



        if ( self::detect_smtp_plugin() !== null ) {

            return; // 主要 SMTP プラグインがあるならそちらに委譲

        }



        $s = self::get_settings();

        if ( empty( $s['smtp_host'] ) ) {

            return; // SMTP 未設定なら WP デフォルトに任せる

        }



        $phpmailer->isSMTP();

        $phpmailer->Host = (string) $s['smtp_host'];

        $phpmailer->Port = (int) $s['smtp_port'];



        switch ( $s['smtp_encryption'] ) {

            case 'tls':

                $phpmailer->SMTPSecure = 'tls';

                $phpmailer->SMTPAutoTLS = true;

                break;

            case 'ssl':

                $phpmailer->SMTPSecure = 'ssl';

                $phpmailer->SMTPAutoTLS = true;

                break;

            case 'none':

            default:

                $phpmailer->SMTPSecure = '';

                $phpmailer->SMTPAutoTLS = false;

                break;

        }



        if ( ! empty( $s['smtp_auth'] ) ) {

            $phpmailer->SMTPAuth = true;

            $phpmailer->Username = (string) $s['smtp_username'];

            // パスワードの値は 2 経路ある:

            //   1. DB から get_settings: AIAP_Crypto で暗号化済み (CIPHER_PREFIX 付き)

            //   2. runtime override (= 送信テスト時): 平文がそのまま入っている

            // is_encrypted で判定し、暗号化済みなら復号、平文ならそのまま使う。

            $pw = (string) $s['smtp_password'];

            if ( $pw !== '' ) {

                if ( class_exists( 'AIAP_Crypto' ) && AIAP_Crypto::is_encrypted( $pw ) ) {

                    $decrypted = AIAP_Crypto::decrypt( $pw );

                    if ( is_string( $decrypted ) && $decrypted !== '' ) {

                        $phpmailer->Password = $decrypted;

                    }

                } else {

                    // 平文 (= runtime override 経路、または暗号化失敗で平文保存された旧データ)

                    $phpmailer->Password = $pw;

                }

            }

        } else {

            $phpmailer->SMTPAuth = false;

        }



        if ( ! empty( $s['smtp_from_email'] ) ) {

            $from_name = ! empty( $s['smtp_from_name'] ) ? (string) $s['smtp_from_name'] : (string) wp_strip_all_tags( get_bloginfo( 'name' ) );

            $phpmailer->setFrom( (string) $s['smtp_from_email'], $from_name, false );

        }

    }

}

