/**
 * 検索の照合に使う文字列の正規化（指示書AG D-1〜D-5）。
 *
 * ★5系統（清掃詳細ログ / 施設マスタ / スケジュール / D-S制御盤 / レポート）が
 *   すべてこの1本を呼ぶ★。CLAUDE.md §4.6 が契約フラグに課したのと同じ規律
 *   （「純粋関数1箇所に閉じ込める」）を、検索の照合規則にも当てる。
 *   以前は照合規則が3種類に分裂しており、同じシリアルを同じ社内画面で探しても
 *   入力欄によって当たったり当たらなかったりしていた。
 *
 * ------------------------------------------------------------------------
 * ★なぜ強さの違う2種類があるのか★ = フィールドによって記号の意味が違うから（D-2）
 * ------------------------------------------------------------------------
 * | 強さ                 | 使うフィールド                                      |
 * |----------------------|-----------------------------------------------------|
 * | `foldName`（強）     | facility_name / prefecture                          |
 * | `foldPlain`（弱）    | address / serial_number / maker / model / model_name |
 *
 * ★施設名だけ強いのは、店名の記号が表記ゆれで、桁位置の意味を持たないから★
 *   実データが `ドン・キホーテ戸塚` のとき、`ドンキホーテ` や `ドンキ` と打つのは
 *   自然な操作だが、記号を残したままでは 0件になる。
 *
 * ------------------------------------------------------------------------
 * ★★シリアルと住所に `foldName` を使ってはいけない★★（D-2。実データで誤ヒットする）
 * ------------------------------------------------------------------------
 * 住所 — ★ハイフンは番地の区切り★:
 *
 *     東京都○○区1-2-3  →（記号除去）→ 東京都○○区123
 *     東京都○○区2-3-4  →（記号除去）→ 東京都○○区234
 *
 *   住所欄に `23` と入れるとこの2件が両方ヒットしてしまう（現状はどちらもヒットしない）。
 *   言い換えると `1-23番地` と `12-3番地` と `1-2-3番地` が区別できなくなる。
 *
 * シリアル — ★桁の境界が消える★:
 *
 *     PUDU-CC1-0001  →（記号除去）→ PUDUCC10001
 *
 *   シリアル欄に `10001`（連番10001号機のつもり）と入れると、`CC1` の `1` と `0001` が
 *   繋がった `PUDU-CC1-0001` にヒットしてしまう（現状は0件）。
 *   ★そして利得がほぼ無い★ — 実データの主形式 `8111R5628060011` には記号が1文字も無く、
 *   記号除去は no-op。`DS-001` 形式のために上のリスクを負うことになる。
 *   さらに `lib/config/index.ts` が「シリアルの文字種は絞らない（形式が未確定）」と
 *   明示しており、「記号は意味を持たない」という前提を置くこと自体が
 *   CLAUDE.md §7「仕様が不明な箇所を推測で実装しない」に反する。
 *
 * メーカー・機種は `ROBOT_MODELS`（`lib/config/index.ts`）に記号が1文字も無いので、
 * 記号除去の効果がそもそもゼロ。直すべきは `cc1` が当たらないことのほうである。
 *
 * ------------------------------------------------------------------------
 * ★入れていないもの（意図的に残した穴。D-4。ユーザーが選ばなかった。2026-09-13）★
 * ------------------------------------------------------------------------
 * - ★カナ・全半角の統一★（Unicode 正規化 / NFKC を含む）
 *   → `ﾄﾞﾝ` や `どん` では `ドン・キホーテ` を引けないまま。
 * - ★スペース区切りの AND 検索★
 *   → `ドン・キホーテ 戸塚` はスペースが除かれて1語として扱われる。
 * - ★英字とカナの読み替え（`MEGA` ⇄ `メガ`）★
 *   → どの語を対にするかは仕様判断（CLAUDE.md §7）。
 *   ★実店舗名が `MEGAドン・キホーテ` なので「メガドンキホーテ」では引けない。これは仕様★。
 *   不具合として報告されたら、この行と指示書AG D-4 を示すこと。
 *
 * ------------------------------------------------------------------------
 * ★正規化は必ず「両側」に掛ける（D-5）★
 * ------------------------------------------------------------------------
 * 入力側だけ正規化してもデータ側の中点が残るので、結局ヒットしない。
 * 呼び出し側が片側だけ正規化する間違いをできないよう、比較まで含めた
 * `includesFoldName` / `includesFoldPlain` を使うこと（`foldName` を直接呼ばない）。
 *
 * ★DBアクセスなし・例外を投げない純粋関数★（CLAUDE.md §9.1。表示整形なので `lib/format/`）。
 */

/**
 * ★`foldName` が除く記号（D-3）★。これ以外は除かない。
 *
 * | 種類       | 文字                                                  | 何のため                                     |
 * |------------|-------------------------------------------------------|----------------------------------------------|
 * | 中点       | `・`(U+30FB) `･`(U+FF65) `·`(U+00B7) `•`(U+2022)      | `ドンキホーテ` → `ドン・キホーテ`（本命）    |
 * | 長音       | `ー`(U+30FC)                                          | `サーバ` で `サーバー` を引けるのと同じ      |
 * | 空白       | 半角スペース(U+0020) / `　`(U+3000) / タブ(U+0009)    | `ドンキホーテ港南` → `MEGAドン・キホーテ 港南` |
 * | ハイフン類 | `-`(U+002D) `‐`〜`―`(U+2010-2015) `－`(U+FF0D) `_`(U+005F) | 店名の区切り                            |
 *
 * ★正規表現の文字クラスではなくコードポイントの集合で持つ理由★
 *   1. 除く文字に半角スペース・全角スペース・タブが含まれ、リテラルで書くと
 *      **読んで確認できない**（空白とタブが目で見分けられない）。
 *   2. 文字クラスの中では `-` が範囲指定に化ける。`[…\t-‐…]` のように並べると
 *      タブから U+2010 までの**約8000文字**が静かに除去対象になり、
 *      エラーもテスト失敗も出ないまま検索結果だけが狂う。
 */
const NAME_SYMBOL_CODE_POINTS: readonly number[] = [
  // 中点4種
  0x30fb, // ・ KATAKANA MIDDLE DOT
  0xff65, // ･ HALFWIDTH KATAKANA MIDDLE DOT
  0x00b7, // · MIDDLE DOT
  0x2022, // • BULLET
  // 長音
  0x30fc, // ー KATAKANA-HIRAGANA PROLONGED SOUND MARK
  // 空白3種
  0x0020, // 半角スペース
  0x3000, // 　 IDEOGRAPHIC SPACE
  0x0009, // タブ
  // ハイフン類
  0x002d, // - HYPHEN-MINUS
  0x2010, // ‐ HYPHEN
  0x2011, // ‑ NON-BREAKING HYPHEN
  0x2012, // ‒ FIGURE DASH
  0x2013, // – EN DASH
  0x2014, // — EM DASH
  0x2015, // ― HORIZONTAL BAR
  0xff0d, // － FULLWIDTH HYPHEN-MINUS
  0x005f, // _ LOW LINE
];

const NAME_SYMBOLS: ReadonlySet<string> = new Set(
  NAME_SYMBOL_CODE_POINTS.map((code) => String.fromCodePoint(code))
);

/**
 * 大文字小文字だけを揃える（★記号は残す★）。
 * 住所・シリアル・メーカー・機種で使う（記号を除いてはいけない理由はファイル冒頭）。
 */
export function foldPlain(value: string): string {
  return value.toLowerCase();
}

/**
 * 大文字小文字を揃え、さらに記号（中点・長音・空白・ハイフン類）を除く。
 * 施設名・都道府県で使う。除く記号の集合は `NAME_SYMBOL_CODE_POINTS` の表を見ること。
 *
 * ★記号だけの入力（例 `・`）は空文字になる★ — これは正しい（除く記号しか無いのだから）。
 *   ★この関数は「空文字になった」という事実を返すだけで、当たり外れは決めない★。
 *   ★記号だけの入力を「当たらない」に倒す判断は `includesFoldName` が持つ★
 *   （2026-09-14。理由はそちらの docstring）。★ここを変えないこと★ —
 *   `foldName("・") === ""` を固定した既存テストは正しい。
 */
export function foldName(value: string): string {
  let folded = "";
  for (const char of foldPlain(value)) {
    if (!NAME_SYMBOLS.has(char)) folded += char;
  }
  return folded;
}

/**
 * haystack と needle の**両方**を `foldName` に通してから部分一致を見る。
 * 施設名・都道府県の照合はこれを使う（D-2 / D-5）。
 *
 * ------------------------------------------------------------------------
 * ★記号だけの入力は「当たらない」（0件）★（★ユーザー判断 2026-09-14。案A★）
 * ------------------------------------------------------------------------
 * | 入力（needle）      | 判定                         |
 * |---------------------|------------------------------|
 * | `""`（未指定）      | ★true（素通し）★            |
 * | `・` `-` `　` など  | ★false（記号だけ）★         |
 * | `ドン・`            | 従来どおり（記号を除いて照合）|
 *
 * ★★なぜ「未指定（空文字）」と「記号だけ」を区別するのか★★
 *   どちらも `foldName` を通せば `""` になるが、★利用者の意図が正反対★である。
 *   - ★`""` は「絞り込まない」という指示★ … 一覧を全部見たい。素通しが正しい。
 *   - ★`・` は「この語で絞り込む」という指示★ … 利用者は何かを入力している。
 *     ★これを素通しにすると「検索したのに全件が出る」★ — 0件と違って
 *     ★画面上は成功に見えるので、打ち間違いに気づけない★。
 *     ★キーボードの切替ミスで `-` だけ残った状態で「絞り込めている」と誤認するのが
 *     いちばん危ない★（翌朝確認の運用で、見落としが「全件表示」に紛れる）。
 *   ★`foldName(needle) === ""` だけで判定してはいけない★ — それでは `""` も巻き込んで
 *   未指定が 0件になり、★施設名を入力していないのに一覧が空になる★
 *   （`facilityIdsMatchingName` の `null` / `[]` の区別と同じ罠）。
 *
 * ★★なぜ `includesFoldPlain` を同じにしないのか★★（★揃えないのが正しい★）
 *   `foldPlain` は★記号を残す★ので、`-` は `-` のまま照合される。
 *   ★`-` はシリアル `PUDU-CC1-0001` に実際に当たる★ = ★意味のある検索★であり、
 *   弾いてはいけない。施設マスタ・スケジュール・D-S制御盤の検索欄は
 *   ★「施設名 OR シリアル」★なので、`-` と打つと
 *   ★施設名では 0件・シリアルでは当たる → ハイフン入りのシリアルの行だけが出る★。
 *   これが狙った結果である（★「全件」でも「0件」でもない★）。
 *   ★`includesFoldPlain` にも同じ早期 return を足さないこと★ — 足すと
 *   ★シリアルをハイフンで探せなくなる★。
 */
export function includesFoldName(haystack: string, needle: string): boolean {
  // 1. 未指定 — 絞り込まない（★ここは従来どおり★）
  if (needle === "") return true;
  const foldedNeedle = foldName(needle);
  // 2. 記号だけ — 入力はあるのに条件が消えるので、素通しではなく「当たらない」に倒す
  if (foldedNeedle === "") return false;
  // 3. それ以外 — 両側を `foldName` に通して部分一致（D-5）
  return foldName(haystack).includes(foldedNeedle);
}

/**
 * haystack と needle の**両方**を `foldPlain` に通してから部分一致を見る。
 * 住所・シリアル・メーカー・機種の照合はこれを使う（D-2 / D-5）。
 */
export function includesFoldPlain(haystack: string, needle: string): boolean {
  return foldPlain(haystack).includes(foldPlain(needle));
}
