/**
 * GET /facilities（F-08 施設ロボットマスタ）で使うクエリ処理。
 * 仕様の正は `d-watch-api.yaml` の /facilities。
 *
 * ★フィルタ・並べ替え・ページングをここ（純粋関数）に置く理由★
 *   `db.facilities()`（`lib/db/index.ts`）は全件を返すだけで、絞り込みを持たない。
 *   DBはTeamWorks管理でDFAはスキーマを変更しない（CLAUDE.md §2）ため、
 *   絞り込みはアプリ側の責務になる。ルート側に直接書くとテストから呼べなくなるので、
 *   DBアクセスを含まない純粋関数として切り出す（`lib/ds-devices/query.ts` と同じ形）。
 */
import { parseMasterIdFilters } from "@/lib/api/master-id";
import { fail, type QueryParseResult } from "@/lib/api/query-result";
import { PAGINATION } from "@/lib/config";
import { includesFoldName, includesFoldPlain } from "@/lib/format/search-text";
import type {
  Facility,
  FacilitySortKey,
  FacilitySortOrder,
  ListResponse,
} from "@/lib/types";

/**
 * `sort` の受け付ける値（★`d-watch-api.yaml` の enum と同じ並び★）。
 *
 * ★★文字列をここ1箇所に置く★★ — 422 のメッセージもこの配列から作る
 *   （`CLEAN_AREA_SCOPES`（`lib/clean-area/query.ts`）と同じ形）。
 *   ★`if (v === "monitoring" || ...)` と書き下さないこと★（増えたときに片方だけ直る）。
 */
export const FACILITY_SORT_KEYS: readonly FacilitySortKey[] = [
  "id",
  "monitoring",
  "d_security",
];

/** `order` の受け付ける値（★YAML の enum と同じ並び★） */
export const FACILITY_SORT_ORDERS: readonly FacilitySortOrder[] = ["desc", "asc"];

/** ★既定は id 昇順★（`d-watch-api.yaml` の `default: id`。`order` はこのとき無視される） */
export const FACILITY_SORT_KEY_DEFAULT: FacilitySortKey = "id";

/** ★既定は desc（契約あり・画面の○が先）★（YAML の `default: desc`） */
export const FACILITY_SORT_ORDER_DEFAULT: FacilitySortOrder = "desc";

export interface FacilityQuery {
  /** 施設名 or 都道府県 or 住所 or シリアル or メーカー or 機種（OR・部分一致） */
  q: string;
  facilityName: string;
  serial: string;
  address: string;
  /**
   * ホールディングス（`groups.id`）で絞り込み。★`null` = 絞らない★（指示書AK 段階2）。
   * ★`customerId` と同時に指定されたら AND★（`d-watch-api.yaml` の `GroupId`）。
   */
  groupId: number | null;
  /** 顧客（`customers.id`）で絞り込み。★`null` = 絞らない★ */
  customerId: number | null;
  /**
   * 並べ替えの列（ユーザー指示 2026-10-30）。★省略・空文字は `"id"`★。
   * ★`"id"` のときは `order` を見ない★（常に id 昇順。`sortFacilities()` の JSDoc）。
   */
  sort: FacilitySortKey;
  /** 並べ替えの向き。★省略・空文字は `"desc"`（○が先）★ */
  order: FacilitySortOrder;
  page: number;
  perPage: number;
}

function toInt(value: string | null, fallback: number): number {
  if (!value) return fallback;
  const n = Number.parseInt(value, 10);
  return Number.isNaN(n) ? fallback : n;
}

/**
 * クエリパラメータをパースする。
 *
 * ★★戻り値が `QueryParseResult` になった（指示書AK 段階2）★★
 *   ★以前は素の `FacilityQuery` を返していた★ — q / facility_name / serial / address は
 *   自由文字列で不正値という概念が無く、page / per_page は丸めるだけだったため、
 *   このエンドポイントには 422 になりうる入力が1つも無かった。
 *   ★`group_id` / `customer_id` が最初の「不正値がありうるパラメータ」になった★ので、
 *   旧 JSDoc の指示どおり `QueryParseResult<FacilityQuery>` に変え、
 *   ★YAML の /facilities にも 422 と2つのパラメータを追記した★（`CLAUDE.md` §6-1）。
 *
 * 文字列は前後の空白を除去する（`parseDsDeviceQuery` の q と同じ。
 * 検索欄からのコピペで前後に空白が入ると、除去しない実装では0件になってしまう）。
 *
 * ★検証は `parseMasterIdFilters()`（`lib/api/master-id.ts`）の1本に任せる★
 *   ★`Number.parseInt()` で書き直さないこと★（`1abc` が `1` として通る。D-123）。
 *
 * ★`sort` / `order`（ユーザー指示 2026-10-30）★
 *   - ★空文字は省略と同じ★（`?sort=` は「未指定」であって不正値ではない。`parseMasterIdParam()` と同じ考え方）。
 *     前後の空白も落とす（`parseCleanAreaQuery()` の `group_by` と同じ）
 *   - ★未知の値は 422★ — 黙って既定順に戻すと、画面の見出しは「並べ替え中」の表示なのに
 *     行は id 順、という食い違いが例外もテスト失敗も出さずに起きる
 *   - ★大文字小文字は同一視しない★（`Monitoring` / `DESC` は 422。YAML の enum どおりの完全一致）
 *   - ★`sort=id` でも `order` の不正値は 422★ — 無視するのは値の効き目だけで、検証は省かない
 *   - ★エラー文言に入力値をエコーしない★（`parseMasterIdParam()` と同じ理由）
 *   - 検証の順序は group_id → customer_id → sort → order（最初に見つかった1つだけを返す）
 */
export function parseFacilityQuery(
  searchParams: URLSearchParams
): QueryParseResult<FacilityQuery> {
  const masters = parseMasterIdFilters(searchParams);
  if (!masters.ok) return masters;

  const sortRaw = searchParams.get("sort")?.trim() || null;
  if (sortRaw !== null && !FACILITY_SORT_KEYS.includes(sortRaw as FacilitySortKey)) {
    return fail(422, "validation_failed", "sortの値が不正です", {
      sort: [`${FACILITY_SORT_KEYS.join(" / ")} のいずれかを指定してください`],
    });
  }
  const orderRaw = searchParams.get("order")?.trim() || null;
  if (
    orderRaw !== null &&
    !FACILITY_SORT_ORDERS.includes(orderRaw as FacilitySortOrder)
  ) {
    return fail(422, "validation_failed", "orderの値が不正です", {
      order: [`${FACILITY_SORT_ORDERS.join(" / ")} のいずれかを指定してください`],
    });
  }

  const page = Math.max(1, toInt(searchParams.get("page"), 1));
  const perPage = Math.min(
    PAGINATION.maxPerPage,
    Math.max(1, toInt(searchParams.get("per_page"), PAGINATION.defaultPerPage))
  );

  return {
    ok: true,
    query: {
      q: searchParams.get("q")?.trim() ?? "",
      facilityName: searchParams.get("facility_name")?.trim() ?? "",
      serial: searchParams.get("serial")?.trim() ?? "",
      address: searchParams.get("address")?.trim() ?? "",
      groupId: masters.query.groupId,
      customerId: masters.query.customerId,
      sort: (sortRaw as FacilitySortKey | null) ?? FACILITY_SORT_KEY_DEFAULT,
      order: (orderRaw as FacilitySortOrder | null) ?? FACILITY_SORT_ORDER_DEFAULT,
      page,
      perPage,
    },
  };
}

/**
 * 施設名 / 都道府県 / 住所 / シリアル / メーカー / 機種 の **OR** 部分一致（YAMLのqの定義通り）。
 * 大文字小文字は区別しない。
 *
 * ★照合はフィールドごとに強さを変える（指示書AG D-2）★
 *   - 施設名・都道府県 … `includesFoldName`（記号も無視する）。
 *     `ドン・キホーテ` の中点は表記ゆれで、桁位置の意味を持たない。
 *     都道府県の値（`東京都`/`神奈川県`）には記号が無いので実質 no-op だが、
 *     「名前を探す欄」として施設名と同じ規則にしておく。
 *   - 住所・シリアル・メーカー・機種 … `includesFoldPlain`（大文字小文字のみ）。
 *     ★住所のハイフンは番地の区切り、シリアルのハイフンは桁の境界★なので、
 *     除くと `23` が `1-2-3` に、`10001` が `PUDU-CC1-0001` に誤ヒットする
 *     （実例は `lib/format/search-text.ts` の冒頭）。
 *
 * ★`robots` が空配列でも落ちないこと★（未設置の施設は robots: [] で登録される。
 * `lib/fixtures/facilities.ts` の id:12）。`some()` は空配列に対して false を返すので、
 * 「ロボット情報では一致しない施設」として自然に扱われる。
 */
export function filterFacilitiesByQ(items: Facility[], q: string): Facility[] {
  if (!q) return items;
  return items.filter(
    (item) =>
      includesFoldName(item.facility_name, q) ||
      includesFoldName(item.prefecture, q) ||
      includesFoldPlain(item.address, q) ||
      item.robots.some(
        (robot) =>
          includesFoldPlain(robot.serial_number, q) ||
          includesFoldPlain(robot.maker, q) ||
          includesFoldPlain(robot.model, q)
      )
  );
}

/**
 * 個別パラメータ（facility_name / serial / address）による絞り込み。
 *
 * ★個別パラメータ同士は AND、q との関係も AND（OR になるのは q の内部だけ）★
 *   — YAML の /facilities の q の description の通り。
 *   空文字のパラメータは「指定なし」として素通しする。
 *
 * ★serial は `robots[]` を横断し、1台でも一致すればその施設をヒットとする★
 *   施設は複数台を持ちうる（P1は1清掃エリア=1ロボットだが、施設単位では複数台ありうる。
 *   `lib/fixtures/facilities.ts` の id:7 は3台）。先頭の1台だけを見る実装にすると、
 *   2台目以降のシリアルで検索した運用担当が「登録されていない」と誤解する。
 *
 * ★個別パラメータも q と完全に同じ照合規則にする★
 *   YAML が明示しているのは q のみだが、シリアル（例: 8111R5628060011）は英大文字を含み、
 *   区別する実装だと「8111r」と打った時だけ0件になる。同じ画面の検索が
 *   入力欄によって当たったり当たらなかったりするほうが事故になるため揃える。
 *   ★記号の扱いも同様に揃える★ — facility_name は `includesFoldName`、
 *   address / serial は `includesFoldPlain`（q の側と1文字も違わない。指示書AG D-2）。
 *
 * ★この個別パラメータはどの画面からも送られていない（API直叩き専用）★
 *   それでも API 契約上は存在するので、q だけ直して放置すると
 *   「同じAPIのパラメータ間で挙動が違う」状態が新規に生まれる（指示書AG 罠10）。
 */
export function filterFacilitiesByFields(
  items: Facility[],
  query: Pick<FacilityQuery, "facilityName" | "serial" | "address">
): Facility[] {
  const { facilityName, serial, address } = query;

  return items.filter((item) => {
    if (facilityName && !includesFoldName(item.facility_name, facilityName)) {
      return false;
    }
    if (address && !includesFoldPlain(item.address, address)) return false;
    if (
      serial &&
      !item.robots.some((robot) =>
        includesFoldPlain(robot.serial_number, serial)
      )
    ) {
      return false;
    }
    return true;
  });
}

/**
 * 並べ替え。★既定（`sort="id"`）は id の昇順★。
 *
 * ★既定の id 昇順は安定化のためだけの規則で、運用上の優先度を表すものではない★
 *   （`sortDsDevices` の serial_number 昇順と同じ考え方）。
 *   このマスタは読み取り専用の台帳で、「異常を上に出す」といった優先度が存在しない。
 *   一方で並べ替えを一切しないと、`db.facilities()` の返す順（fixtures の記述順・
 *   DB接続後は実行計画次第）に画面が左右され、リロードのたびに行が入れ替わって見える。
 *   id は不変で重複しないため、これだけで順序が一意に決まる。
 *
 * ★`monitoring` / `d_security` での並べ替え（ユーザー指示 2026-10-30「ロボットマスタを監視または
 *   D-SECでソートできるようにしてください」）★
 *   ★ページ送りがサーバー側なので、並べ替えも API で行う★ — 画面で1ページ分だけ並べ替えると、
 *   ○の施設が2ページ目以降に残っているのに「○が先」に見えてしまう。
 *   ★ルートでは絞り込みのあと・ページングの前に呼ぶこと★（`app/api/facilities/route.ts`）。
 *   - `desc` ＝ true（契約あり・画面の○）が先 / `asc` ＝ false（ー）が先
 *   - ★同じ値どうしは必ず id 昇順★（リロードで行が入れ替わらないように）。
 *     ★`asc` を「`desc` の結果の逆順」で作らないこと★ — 同値の中まで id 降順になる。
 *     反転させるのはフラグの比較だけ
 *   - ★`sort="id"` のときは `order` を見ない★（YAML の契約。id 降順という並びは提供しない）
 *
 * 引数を省略すると id 昇順（既定）。元の配列は書き換えない。
 */
export function sortFacilities(
  items: Facility[],
  sort: FacilitySortKey = FACILITY_SORT_KEY_DEFAULT,
  order: FacilitySortOrder = FACILITY_SORT_ORDER_DEFAULT
): Facility[] {
  const byId = (a: Facility, b: Facility) => a.id - b.id;
  if (sort === "id") return [...items].sort(byId);

  const flag = (f: Facility): boolean =>
    sort === "monitoring" ? f.monitoring_enabled : f.d_security_enabled;
  // boolean を 1 / 0 にして引き算する。`Number(flag(b)) - Number(flag(a))` は
  // true(1) を先に出す（desc）。asc はフラグの比較だけを反転させ、同値の id 昇順は残す。
  const direction = order === "desc" ? 1 : -1;
  return [...items].sort(
    (a, b) =>
      direction * (Number(flag(b)) - Number(flag(a))) || byId(a, b)
  );
}

/**
 * ページングを適用してレスポンス本体を組み立てる。
 *
 * ★`meta.total` は「フィルタ後・ページング前」の件数★
 *   画面のページャはこの値で総ページ数を出す。ページング後の件数（items.length）を
 *   入れると、2ページ目以降が存在しないことになってページ送りができなくなる。
 *   範囲外のページ（例: 3ページしかないのに page=99）でも total は全体の件数のまま返し、
 *   items だけが空になる。
 *
 * 組み立てまでをここに置くのは、この「total の意味」をルートではなくテストで
 * 固定できるようにするため（ルートは DB アクセスを含むため純粋関数として呼べない）。
 */
export function paginateFacilities(
  items: Facility[],
  page: number,
  perPage: number
): ListResponse<Facility> {
  const total = items.length;
  const start = (page - 1) * perPage;
  return {
    items: items.slice(start, start + perPage),
    meta: { total, page, per_page: perPage },
  };
}
