<?php

namespace Acms\Services\Fulltext;

use Acms\Services\Facades\Application;
use Acms\Services\Facades\Database as DB;
use ACMS_Hook;
use ACMS_RAM;
use SQL;

class Helper
{
    /**
     * エントリーのフルテキストを取得
     *
     * @param int $eid
     *
     * @return string
     */
    public function loadEntryFulltext(int $eid): string
    {
        $unitRepository = Application::make('unit-repository');
        assert($unitRepository instanceof \Acms\Services\Unit\Repository);

        $text = $unitRepository->getUnitSearchText($eid);
        $entry = [
            'id' => $eid,
            'title' => ACMS_RAM::entryTitle($eid),
            'code' => ACMS_RAM::entryCode($eid),
        ];

        $sql = SQL::newSelect('field');
        $sql->addSelect('field_key');
        $sql->addSelect('field_value');
        $sql->addWhereOpr('field_search', 'on');
        $sql->addWhereOpr('field_eid', $eid);
        $q = $sql->get(dsn());
        $statement = DB::query($q, 'exec');

        $field = [];
        if ($statement && ($row = DB::next($statement))) {
            do {
                if (!isset($field[$row['field_key']])) {
                    $field[$row['field_key']] = [];
                }
                $field[$row['field_key']][] = $row['field_value'];
            } while ($row = DB::next($statement));
        }

        if (HOOK_ENABLE) {
            $hook = ACMS_Hook::singleton();
            $hook->call('filterEntryFulltext', [&$entry, &$field, $eid]);
        }

        $metaText = implode(
            ' ',
            [
                implode(' ', array_values($entry)),
                implode(' ', array_map(function (array $values) {
                    return implode(' ', $values);
                }, $field)),
            ]
        );

        $fulltext = preg_replace('/\s+/u', ' ', strip_tags($text))
        . "\x0d\x0a\x0a\x0d" . preg_replace('/\s+/u', ' ', strip_tags($metaText))
            ;
        return $fulltext;
    }

    /**
     * ユーザーのフルテキストを取得
     *
     * @param int $uid
     *
     * @return string
     */
    public function loadUserFulltext(int $uid): string
    {
        $user = [
            'name' => ACMS_RAM::userName($uid),
            'code' => ACMS_RAM::userCode($uid),
            'mail' => ACMS_RAM::userMail($uid),
            'mail_mobile' => ACMS_RAM::userMailMobile($uid),
            'url' => ACMS_RAM::userUrl($uid),
        ];

        $SQL = SQL::newSelect('field');
        $SQL->addSelect('field_key');
        $SQL->addSelect('field_value');
        $SQL->addWhereOpr('field_search', 'on');
        $SQL->addWhereOpr('field_uid', $uid);
        $q = $SQL->get(dsn());
        $statement = DB::query($q, 'exec');

        $field = [];
        if ($statement && ($row = DB::next($statement))) {
            do {
                if (!isset($field[$row['field_key']])) {
                    $field[$row['field_key']] = [];
                }
                $field[$row['field_key']][] = $row['field_value'];
            } while ($row = DB::next($statement));
        }

        if (HOOK_ENABLE) {
            $hook = ACMS_Hook::singleton();
            $hook->call('filterUserFulltext', [&$user, &$field, $uid]);
        }

        $userText = implode(' ', array_values($user));
        $metaText = implode(' ', array_map(function (array $values) {
            return implode(' ', $values);
        }, $field));

        $fulltext = preg_replace('/\s+/u', ' ', strip_tags($userText))
        . "\x0d\x0a\x0a\x0d" . preg_replace('/\s+/u', ' ', strip_tags($metaText))
            ;
        return $fulltext;
    }

    /**
     * カテゴリのフルテキストを取得
     *
     * @param int $cid
     *
     * @return string
     */
    public function loadCategoryFulltext(int $cid): string
    {
        $category = [
            'name' => ACMS_RAM::categoryName($cid),
            'code' => ACMS_RAM::categoryCode($cid),
        ];

        $SQL = SQL::newSelect('field');
        $SQL->addSelect('field_key');
        $SQL->addSelect('field_value');
        $SQL->addWhereOpr('field_search', 'on');
        $SQL->addWhereOpr('field_cid', $cid);
        $q = $SQL->get(dsn());
        $statement = DB::query($q, 'exec');

        $field = [];
        if ($statement && ($row = DB::next($statement))) {
            do {
                if (!isset($field[$row['field_key']])) {
                    $field[$row['field_key']] = [];
                }
                $field[$row['field_key']][] = $row['field_value'];
            } while ($row = DB::next($statement));
        }

        if (HOOK_ENABLE) {
            $hook = ACMS_Hook::singleton();
            $hook->call('filterCategoryFulltext', [&$category, &$field, $cid]);
        }

        $categoryText = implode(' ', array_values($category));
        $metaText = implode(' ', array_map(function (array $values) {
            return implode(' ', $values);
        }, $field));

        $fulltext = preg_replace('/\s+/u', ' ', strip_tags($categoryText))
        . "\x0d\x0a\x0a\x0d" . preg_replace('/\s+/u', ' ', strip_tags($metaText));
        return $fulltext;
    }

    /**
     * ブログのフルテキストを取得
     *
     * @param int $bid
     *
     * @return string
     */
    public function loadBlogFulltext(int $bid): string
    {
        $blog = [
            'name' => ACMS_RAM::blogName($bid),
            'code' => ACMS_RAM::blogCode($bid),
            'domain' => ACMS_RAM::blogDomain($bid),
        ];

        $SQL = SQL::newSelect('field');
        $SQL->addSelect('field_key');
        $SQL->addSelect('field_value');
        $SQL->addWhereOpr('field_search', 'on');
        $SQL->addWhereOpr('field_bid', $bid);
        $q = $SQL->get(dsn());
        $statement = DB::query($q, 'exec');

        $field = [];
        if ($statement && ($row = DB::next($statement))) {
            do {
                if (!isset($field[$row['field_key']])) {
                    $field[$row['field_key']] = [];
                }
                $field[$row['field_key']][] = $row['field_value'];
            } while ($row = DB::next($statement));
        }

        if (HOOK_ENABLE) {
            $hook = ACMS_Hook::singleton();
            $hook->call('filterBlogFulltext', [&$blog, &$field, $bid]);
        }

        $blogText = implode(' ', array_values($blog));
        $metaText = implode(' ', array_map(function (array $values) {
            return implode(' ', $values);
        }, $field));

        $fulltext = preg_replace('/\s+/u', ' ', strip_tags($blogText))
        . "\x0d\x0a\x0a\x0d" . preg_replace('/\s+/u', ' ', strip_tags($metaText));
        return $fulltext;
    }

    /**
     * フルテキストの保存
     *
     * @param string $type フルテキストのタイプ
     * @param int $id
     * @param string|null $fulltext
     * @param int|null $targetBid 保存先ブログ ID。null / 省略時は現在のブログ（BID）
     *
     * @return void
     */
    public function saveFulltext(string $type, int $id, ?string $fulltext = null, ?int $targetBid = null): void
    {
        // Why not デフォルト値に BID を書く: {@see \Acms\Services\Field\FieldRepository::save()} と同じ理由
        // （ファサードの @method 表記が「?int $targetBid = null」で、null 明示時に
        // fulltext_blog_id = null の INSERT が黙って失敗するのを防ぐ）。
        $targetBid = $targetBid ?? BID;

        $SQL = SQL::newDelete('fulltext');
        $SQL->addWhereOpr('fulltext_' . $type, $id);
        DB::query($SQL->get(dsn()), 'exec');

        if ($fulltext !== null && $fulltext !== '') {
            $fulltext = $this->normalizeSearchText($fulltext);
            $SQL    = SQL::newInsert('fulltext');
            $SQL->addInsert('fulltext_value', $fulltext);
            if (config('ngram')) {
                $SQL->addInsert(
                    'fulltext_ngram',
                    preg_replace('/(　|\s)+/u', ' ', join(' ', ngram(strip_tags($fulltext), config('ngram'))))
                );
            }
            $SQL->addInsert('fulltext_' . $type, $id);
            $SQL->addInsert('fulltext_blog_id', $targetBid);
            DB::query($SQL->get(dsn()), 'exec');
        }
    }

    /**
     * 全文検索の対象テキストを正規化する
     *
     * config('search_convert_kana') に mb_convert_kana() のモード文字列（例: KV）が指定されている場合に、
     * そのモードで変換した文字列を返す。未設定または off なら入力をそのまま返す。
     *
     * 半角カナの濁点・半濁点は「本体文字 + 独立した濁点記号」の 2 文字（ｶ + ﾞ）で、全角は結合済みの
     * 1 文字（ガ）。照合順序 utf8mb4_unicode_ci はカナ単体の全角・半角差は吸収するが、この結合までは
     * 吸収せず文字数が合わないため一致しない。インデックス生成時と検索キーワード受付時の双方で本メソッドを
     * 通し、両者を同じ表記に寄せることで一致させる。
     *
     * mb_convert_kana() での変換後、拡張アプリ・ユーザー拡張が独自の正規化（長音記号の畳み込み、
     * 旧字体・異体字の統一など）を追加できるよう `extendsSearchNormalization` フックを発火する。
     *
     * Why not Normalizer::normalize(): NFKC は intl 拡張を要求するため、動作環境の前提を上げずに済む
     * mbstring で実現する。
     * Why not 不正なモードでの警告ログ: 本メソッドは検索リクエストごと・インデックス保存ごとに呼ばれる
     * ため、設定ミス時にログが際限なく膨らむ。加えて mb_convert_kana() は不正なモードで ValueError を投げ、
     * 検索ページ全体が落ちてしまうので、事前に弾いて変換なしにフォールバックする。
     *
     * @param string $text
     * @return string
     */
    public function normalizeSearchText(string $text): string
    {
        $mode = config('search_convert_kana');
        if ($mode !== '' && $mode !== 'off' && preg_match('/^[rRnNaAsSkKhHcCV]+$/', $mode)) {
            $text = mb_convert_kana($text, $mode, 'UTF-8');
        }
        return $this->fireExtendsSearchNormalization($text);
    }

    /**
     * 検索テキストの正規化拡張フックを発火する。
     *
     * フック発火（グローバルな ACMS_Hook へのアクセス）を本メソッドに閉じることで、
     * {@see normalizeSearchText()} 自体は引数を受け取って返すだけの純粋な形を保ち、単体テスト可能性を保つ。
     * Hook 無効時（HOOK_ENABLE が偽）は何もしない。
     *
     * @param string $text mb_convert_kana() 適用後のテキスト
     * @return string
     */
    private function fireExtendsSearchNormalization(string $text): string
    {
        if (!HOOK_ENABLE) {
            return $text;
        }
        ACMS_Hook::singleton()->call('extendsSearchNormalization', [&$text]);
        return $text;
    }
}
