/**
 * GET /schedules で使うクエリ処理（F-09 スケジュール管理・読み取り専用）。
 * 仕様の正は `d-watch-api.yaml` の /schedules。
 *
 * `db.schedules()` は全件を返すだけなので、絞り込み・並べ替え・ページングはここで組み立てる
 * （`lib/ds-devices/query.ts` と同じ分担。`lib/db/` はデータ取得だけを担う）。
 *
 * ------------------------------------------------------------------------
 * ★実DB（`prismaSource`）実装時の注意★
 * ------------------------------------------------------------------------
 * 現在の `db.schedules()` は fixtures を返すだけで、Prisma 実装は未着手。
 * 下記はいずれも「型エラーもテスト失敗も出ないまま静かに壊れる」箇所なので、
 * `prismaSource.schedules()` を書く人は必ず読むこと。
 *
 * 1. ★論理が反転している★
 *    DB は `CleanSchedule.inactive`（`prisma/schema.prisma`。`Int @default(0)` で **0 = 有効**）、
 *    API/TS は `active: boolean`。名前が似ているので
 *    `active: Boolean(row.inactive)` と書きたくなるが、**それでは全件反転する**
 *    （有効なスケジュールが「無効」として並ぶ）。正しくは `active: row.inactive === 0`。
 *
 * 2. ★カラム名が違う★
 *    DB は `weeks`（`VarChar(7)`）、API/TS は `weekdays`。単純な spread では移せない。
 *
 * 3. ★時刻は `HHMM` の4桁文字列★
 *    `start_time` は 0000〜2359 だが、`end_time` の「時」は **24〜29 を取りうる**
 *    （`"2830"` = 翌日 4:30。日跨ぎ清掃を表現するため）。`HH:MM` として素朴にパースすると壊れる。
 *    API は D-060 に従い `"翌04:30"` の**整形済み文字列**で返す。
 *    なお整形は `db.schedules()`（データ層）が済ませてから `Schedule` を返す約束で、
 *    このクエリ層は一切関与しない（ここは事実の書き残しのみ）。
 *
 * 4. ★`weekdays_label` は DB に存在しない★
 *    表示用ラベル（「毎日」等）は**サーバ側で `weeks` から生成する**必要がある。
 *    DB から素直に select しただけでは埋まらない。
 */
import { parseMasterIdFilters } from "@/lib/api/master-id";
import type { QueryParseResult } from "@/lib/api/query-result";
import { PAGINATION } from "@/lib/config";
import { includesFoldName, includesFoldPlain } from "@/lib/format/search-text";
import type { Schedule } from "@/lib/types";

export interface ScheduleQuery {
  /** 施設名 or シリアルの部分一致（この中だけが OR） */
  q: string;
  /** 施設名の部分一致 */
  facilityName: string;
  /** シリアルの部分一致 */
  serial: string;
  /**
   * ホールディングス（`groups.id`）で絞り込み。★`null` = 絞らない★（指示書AK 段階2）。
   * ★`customerId` と同時に指定されたら AND★（`d-watch-api.yaml` の `GroupId`）。
   */
  groupId: number | null;
  /** 顧客（`customers.id`）で絞り込み。★`null` = 絞らない★ */
  customerId: number | null;
  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;
}

/**
 * クエリパラメータをパースする。
 *
 * page / per_page は不正値でも失敗させず、既定値・上限へ丸める
 * （`parseDsDeviceQuery` / `parseCleanLogQuery` と同じ丸め方。
 *  ページャの URL を手で編集した程度で 422 を返さないため）。
 *
 * ★group_id / customer_id を読む（指示書AK 段階2）★
 *   検証は `parseMasterIdFilters()`（`lib/api/master-id.ts`）の1本に任せる。
 *   ★`Number.parseInt()` で書き直さないこと★（`1abc` が `1` として通る。D-123）。
 *   ★このファイルには D-053 の「無視する」コメントが元から無かった★ —
 *   YAML の /schedules にもこの2つの宣言が無かったので、今回 YAML にも足した
 *   （`CLAUDE.md` §6-1「API契約を変えたくなったら、先に YAML を直す」）。
 *
 * ★戻り値を `QueryParseResult` にしてあった理由がここで効く★
 *   「後から絞り込み条件が増えたときに **ルート側を書き換えずに** 422 を返せる」
 *   ように用意されていた器に、★実際に最初の 422（group_id / customer_id）が入った★。
 */
export function parseScheduleQuery(
  searchParams: URLSearchParams
): QueryParseResult<ScheduleQuery> {
  const masters = parseMasterIdFilters(searchParams);
  if (!masters.ok) return masters;

  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() ?? "",
      groupId: masters.query.groupId,
      customerId: masters.query.customerId,
      page,
      perPage,
    },
  };
}

/**
 * 施設名・シリアルの部分一致（YAML の q の定義通り）。大文字小文字を区別しない。
 * ★q の内部だけが OR★（facility_name / serial とは AND。YAML の q の description）。
 *
 * ★照合は `lib/format/search-text.ts` の1本に寄せる（指示書AG D-1）★
 *   施設名は記号（中点・長音・空白・ハイフン類）も無視する = `includesFoldName`。
 *   ★シリアルは大文字小文字のみ★ = `includesFoldPlain`
 *   （記号を除くと桁の境界が消えて誤ヒットする。D-2。実例は search-text.ts 冒頭）。
 */
export function filterSchedulesByQ(items: Schedule[], q: string): Schedule[] {
  if (!q) return items;
  return items.filter(
    (item) =>
      includesFoldName(item.facility_name, q) ||
      includesFoldPlain(item.serial_number, q)
  );
}

/**
 * q / facility_name / serial をすべて AND で適用する。
 *
 * ★`active` では絞り込まない★
 *   一覧は有効・無効の**両方**を返す。画面（F-09）は「有効」列で見せ分ける仕様で、
 *   API 側で無効行を落とすと「登録したはずのスケジュールが消えた」ように見える。
 *   YAML の /schedules にも active のクエリパラメータは無い。
 *
 * ★serial は横断検索しない★
 *   `Schedule` は 1 レコード 1 シリアル（P1 は 1清掃エリア = 1ロボット。CLAUDE.md §8）なので、
 *   施設内の他ロボットまで見に行く必要はない。
 *
 * ★個別パラメータは q と完全に同じ照合規則にする★（指示書AG D-1 / 罠10）
 *   この2つはどの画面からも送られていない（API直叩き専用）が、API 契約上は存在する。
 *   q だけ直して放置すると「同じAPIのパラメータ間で挙動が違う」状態が新規に生まれる。
 */
export function filterSchedules(
  items: Schedule[],
  query: ScheduleQuery
): Schedule[] {
  const byQ = filterSchedulesByQ(items, query.q);
  return byQ.filter((item) => {
    if (
      query.facilityName &&
      !includesFoldName(item.facility_name, query.facilityName)
    ) {
      return false;
    }
    if (
      query.serial &&
      !includesFoldPlain(item.serial_number, query.serial)
    ) {
      return false;
    }
    return true;
  });
}

/**
 * id の昇順。
 *
 * F-09 は「登録済みスケジュールの台帳」であり、優先度で並べ替える画面ではない。
 * 曜日や時刻で並べると、同じ施設の行が離れて見比べにくくなるうえ、
 * 「翌04:30」のような整形済み文字列を持つ end_time では意図した順序にならない。
 * 登録順（= id 昇順）が一番安定して予測しやすいので、それを正とする。
 */
export function sortSchedules(items: Schedule[]): Schedule[] {
  return [...items].sort((a, b) => a.id - b.id);
}

/**
 * ページを切り出す。
 *
 * ★ルート側で `slice` を直書きせず関数に出している理由★
 *   兄弟API（ds-devices 等）はルート内で slice しているが、その形だと
 *   ページング境界（最終ページの端数・範囲外ページ）を検証するテストが
 *   「ルートと同じ計算をテスト側にも書き写す」ことになり、
 *   両方を同じように間違えたときに素通りする。ここに出しておけば
 *   テストが**本番と同じ関数**を叩ける。
 *
 * `page` が範囲外なら空配列を返す。総件数（`meta.total`）は絞り込み後の全件数であって
 * このページの件数ではないので、呼び出し側は `items.length` を total に使わないこと。
 */
export function paginateSchedules(
  items: Schedule[],
  page: number,
  perPage: number
): Schedule[] {
  const start = (page - 1) * perPage;
  return items.slice(start, start + perPage);
}
