// koseki-web — 固有ロジック（共通coreへ委譲される per-system handle）
import { join } from "node:path";

// ── 戸籍 証明手数料の算定（標準仕様書: 証明種別×通数。手数料額は手数料条例。法定額を既定）──
const KOSEKI_FEES = {
  "戸籍全部事項証明書": 450, "戸籍個人事項証明書": 450, // 戸籍謄抄本
  "除籍全部事項証明書": 750, "除籍個人事項証明書": 750, "改製原戸籍": 750,
  "戸籍記載事項証明書": 350, "受理証明書": 350, "身分証明書": 300, "独身証明書": 300,
};
const KOSEKI_POSTAL = 140;
function computeKosekiFee({ certType, copies = 1, postal = false }, fees = KOSEKI_FEES) {
  const unit = fees[certType];
  if (unit === undefined) return { error: "unknown certType", certType };
  const n = Math.max(1, Number(copies) || 1);
  const subtotal = unit * n;
  const postalFee = postal ? KOSEKI_POSTAL : 0;
  return { certType, unitFee: unit, copies: n, subtotal, postalFee, total: subtotal + postalFee };
}

export async function handle(ctx) {
  const { req, res, pathname, searchParams, actor, send, sendRaw, parseBody, readJson, writeJson, audit,
    maskRecord, validateRecord, requireRole, hasRole, dataRoot, restrictionsFile, crypto,
    dbSelect, dbSelectOne, dbInsert, dbUpdate, dbDelete } = ctx;
  const reply = (...a) => { send(...a); return true; };
  const familyRegistersFile = join(dataRoot, "family-registers.json");
  const familyMembersFile = join(dataRoot, "family-members.json");
  const kosekiEventsFile = join(dataRoot, "koseki-events.json");


    // ── 戸籍 (Family Registers) ───────────────────────────────────────────

    if (pathname === "/api/family-registers" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const registers = (await dbSelect("koseki.family_register")) ?? await readJson(familyRegistersFile, []);
      const status = searchParams.get("status");
      return reply(res, 200, status ? registers.filter((r) => r.status === status) : registers);
    }

    if (pathname === "/api/family-registers" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.householdHead || !body.address) { const e = new Error("householdHead and address are required"); e.statusCode = 400; throw e; }
      const registers = (await dbSelect("koseki.family_register")) ?? await readJson(familyRegistersFile, []);
      const item = { id: crypto.randomUUID(), kosekiCode: "KS-" + Date.now().toString(36).toUpperCase(), householdHead: String(body.householdHead), address: String(body.address), registrationDate: body.registrationDate ?? new Date().toISOString().slice(0, 10), status: "active", notes: body.notes ?? "", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), createdBy: actor.id };
      const dbInserted = await dbInsert("koseki.family_register", item); if (dbInserted === null) { registers.unshift(item); await writeJson(familyRegistersFile, registers); }
      await audit(req, "family-register.create", { id: item.id, kosekiCode: item.kosekiCode });
      return reply(res, 201, item);
    }

    const registerMatch = pathname.match(/^\/api\/family-registers\/([^/]+)$/);
    if (registerMatch) {
      requireRole(actor, "CLERK");
      const registers = (await dbSelect("koseki.family_register")) ?? await readJson(familyRegistersFile, []);
      const index = registers.findIndex((r) => r.id === registerMatch[1]);
      if (index === -1) return reply(res, 404, { error: "family register not found" });
      if (req.method === "GET") return reply(res, 200, registers[index]);
      if (req.method === "PUT") {
        const body = await parseBody(req);
        const item = { ...registers[index], householdHead: body.householdHead ?? registers[index].householdHead, address: body.address ?? registers[index].address, status: body.status ?? registers[index].status, notes: body.notes ?? registers[index].notes, updatedAt: new Date().toISOString(), updatedBy: actor.id };
        const upd = await dbUpdate("koseki.family_register", item.id, item);
        if (upd === null) { registers[index] = item; await writeJson(familyRegistersFile, registers); }
        await audit(req, "family-register.update", { id: item.id });
        return reply(res, 200, item);
      }
    }

    // ── 構成員 (Family Members) ───────────────────────────────────────────

    const membersMatch = pathname.match(/^\/api\/family-registers\/([^/]+)\/members$/);
    if (membersMatch) {
      requireRole(actor, "CLERK");
      const registerId = membersMatch[1];
      const registers = (await dbSelect("koseki.family_register")) ?? await readJson(familyRegistersFile, []);
      if (!registers.find((r) => r.id === registerId)) return reply(res, 404, { error: "family register not found" });
      const members = (await dbSelect("koseki.family_member")) ?? await readJson(familyMembersFile, []);

      if (req.method === "GET") return reply(res, 200, members.filter((m) => m.registerId === registerId));

      if (req.method === "POST") {
        const body = await parseBody(req);
        if (!body.fullName || !body.birthDate || !body.relationship) { const e = new Error("fullName, birthDate and relationship are required"); e.statusCode = 400; throw e; }
        const item = { id: crypto.randomUUID(), registerId, fullName: String(body.fullName), birthDate: String(body.birthDate), relationship: String(body.relationship), myNumberMasked: body.myNumberLast4 ? "*******" + String(body.myNumberLast4).slice(-4) : null, status: "active", createdAt: new Date().toISOString(), createdBy: actor.id };
        const dbInserted = await dbInsert("koseki.family_member", item); if (dbInserted === null) { members.unshift(item); await writeJson(familyMembersFile, members); }
        await audit(req, "family-member.add", { id: item.id, registerId, fullName: item.fullName });
        return reply(res, 201, item);
      }
    }

    // ── 届出 (Koseki Events) ──────────────────────────────────────────────

    if (pathname === "/api/koseki-events" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const events = (await dbSelect("koseki.koseki_event")) ?? await readJson(kosekiEventsFile, []);
      const registerId = searchParams.get("registerId");
      const eventType = searchParams.get("eventType");
      let result = events;
      if (registerId) result = result.filter((e) => e.registerId === registerId);
      if (eventType) result = result.filter((e) => e.eventType === eventType);
      return reply(res, 200, result);
    }

    if (pathname === "/api/koseki-events" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const validTypes = ["birth", "death", "marriage", "divorce", "adoption", "address_change", "name_change", "transfer"];
      if (!body.registerId || !body.eventType || !body.effectiveDate) { const e = new Error("registerId, eventType and effectiveDate are required"); e.statusCode = 400; throw e; }
      if (!validTypes.includes(body.eventType)) { const e = new Error(`eventType must be one of: ${validTypes.join(", ")}`); e.statusCode = 400; throw e; }
      const events = (await dbSelect("koseki.koseki_event")) ?? await readJson(kosekiEventsFile, []);
      const item = { id: crypto.randomUUID(), registerId: String(body.registerId), eventType: body.eventType, effectiveDate: String(body.effectiveDate), reportedBy: body.reportedBy ?? actor.id, memberId: body.memberId ?? null, notes: body.notes ?? "", status: "accepted", createdAt: new Date().toISOString(), createdBy: actor.id };
      const dbInserted = await dbInsert("koseki.koseki_event", item); if (dbInserted === null) { events.unshift(item); await writeJson(kosekiEventsFile, events); }
      await audit(req, `koseki.${body.eventType}`, { id: item.id, registerId: item.registerId, effectiveDate: item.effectiveDate });
      return reply(res, 201, item);
    }


    const regDelMatch = pathname.match(/^\/api\/family-registers\/([^\/]+)$/);
    if (regDelMatch && req.method === "DELETE") {
      requireRole(actor, "ADMIN");
      const delId = regDelMatch[1];
      const deleted = await dbDelete("koseki.family_register", delId);
      if (deleted === null) {
        const _items = await readJson(familyRegistersFile, []);
        const _idx = _items.findIndex((x) => x.id === delId);
        if (_idx === -1) return reply(res, 404, { error: "family register not found" });
        _items.splice(_idx, 1);
        await writeJson(familyRegistersFile, _items);
      }
      await audit(req, "family-register.delete", { id: delId });
      return reply(res, 204, {});
    }

    const memberDeleteMatch = pathname.match(/^\/api\/family-registers\/[^\/]+\/members\/([^\/]+)$/);
    if (memberDeleteMatch && req.method === "DELETE") {
      requireRole(actor, "ADMIN");
      const delId = memberDeleteMatch[1];
      const deleted = await dbDelete("koseki.family_member", delId);
      if (deleted === null) {
        const _items = await readJson(familyMembersFile, []);
        const _idx = _items.findIndex((x) => x.id === delId);
        if (_idx === -1) return reply(res, 404, { error: "family member not found" });
        _items.splice(_idx, 1);
        await writeJson(familyMembersFile, _items);
      }
      await audit(req, "family-member.delete", { id: delId });
      return reply(res, 204, {});
    }


    // ── 戸籍 証明手数料の算定（標準仕様書準拠）──
    if (pathname === "/api/koseki/fee" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeKosekiFee(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "koseki.fee-calc", { certType: result.certType, total: result.total });
      return reply(res, 200, result);
    }

    // ════ イメージ変換/訂正/紙出力・データリスト検査・コード化・仮戸籍・移記メニュー・受付/整理簿・
    //      公選法/人口動態提供・高齢者消除・振り仮名委託・受信取込・ログ（標準仕様書）════

    const kosekiImagesFile = join(dataRoot, "koseki-images.json");
    const kosekiCodesFile = join(dataRoot, "koseki-custom-codes.json");
    const kosekiUketsukeFile = join(dataRoot, "koseki-uketsuke.json");
    const kosekiLogsFile = join(dataRoot, "koseki-logs.json");

    // ── 除籍イメージ: 登録・変換（PDF/BMP）・紙出力・直接訂正（再製含む）──
    if (pathname === "/api/koseki/images" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["除籍", "再製原戸籍", "再製戸籍", "再製原除籍", "再製除籍"];
      if (!b.registerId || !kinds.includes(b.kind)) return reply(res, 400, { error: `registerId, kind(${kinds.join("/")}) required` });
      const images = await readJson(kosekiImagesFile, []);
      const item = { id: crypto.randomUUID(), registerId: String(b.registerId), kind: b.kind,
        format: b.format ?? "BMP", corrections: [], registeredAt: new Date().toISOString(), by: actor.id };
      images.unshift(item);
      await writeJson(kosekiImagesFile, images);
      await audit(req, "koseki-images.register", { kind: item.kind });
      return reply(res, 201, item);
    }
    {
      const m = pathname.match(/^\/api\/koseki\/images\/([^/]+)\/(convert|print|correct)$/);
      if (m && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const images = await readJson(kosekiImagesFile, []);
        const idx = images.findIndex((i) => i.id === m[1]);
        if (idx === -1) return reply(res, 404, { error: "image not found" });
        if (m[2] === "convert") {
          const formats = ["PDF", "BMP", "TIFF"];
          if (!formats.includes(b.to)) return reply(res, 400, { error: `to(${formats.join("/")}) required` });
          await audit(req, "koseki-images.convert", { id: m[1], to: b.to });
          return reply(res, 200, { id: m[1], from: images[idx].format, to: b.to, converted: true,
            note: "用途に合わせてPDF/BMP等へ変換" });
        }
        if (m[2] === "print") {
          await audit(req, "koseki-images.print", { id: m[1] });
          return reply(res, 200, { id: m[1], kind: images[idx].kind, printed: true,
            usage: "戸籍事務専用", note: "イメージの紙出力（戸籍事務専用として出力）" });
        }
        // correct: システムから直接訂正（再製原除籍・再製除籍も同経路）
        images[idx].corrections.push({ field: b.field ?? "", value: b.value ?? "", at: new Date().toISOString(), by: actor.id });
        await writeJson(kosekiImagesFile, images);
        await audit(req, "koseki-images.correct", { id: m[1] });
        return reply(res, 200, { id: m[1], kind: images[idx].kind, corrected: true, corrections: images[idx].corrections });
      }
    }

    // ── 戸籍基本データリストの検査（空欄許容・漢字空白1文字許容の項目別チェック）──
    if (pathname === "/api/koseki/datalist/validate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const records = Array.isArray(b.records) ? b.records : [];
      if (!records.length) return reply(res, 400, { error: "records[]{file,field,value} required" });
      const BLANK_OK = new Set(["戸籍特定.筆頭者振り仮名", "氏名.カナ氏名", "氏名.振り仮名", "受付事件本人.カナ氏名", "受付事件本人.振り仮名"]);
      const ZSPACE_OK = new Set(["戸籍特定.筆頭者", "戸籍特定.筆頭者振り仮名", "個人特定.父", "個人特定.母", "個人特定.養父", "個人特定.養母",
        "氏名.カナ氏名", "氏名.振り仮名", "氏名.漢字氏名", "不受理申出.相手方氏名", "受付.届後戸籍の筆頭者",
        "受付事件本人.カナ氏名", "受付事件本人.振り仮名"]);
      const issues = [];
      for (const r of records) {
        const key = `${r.file}.${r.field}`;
        const v = String(r.value ?? "");
        if (v === "" && !BLANK_OK.has(key)) issues.push({ key, issue: "空欄不可項目が空欄" });
        if (v.includes("　") && !ZSPACE_OK.has(key)) issues.push({ key, issue: "漢字空白1文字の許容外使用" });
      }
      await audit(req, "koseki-datalist.validate", { records: records.length, issues: issues.length });
      return reply(res, 200, { checked: records.length, ok: issues.length === 0, issues });
    }

    // ── ABRにない旧本籍・国名コードにない外国名のコード化 ──
    if (pathname === "/api/koseki/codes/custom" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["旧本籍", "外国名"];
      if (!kinds.includes(b.kind) || !b.value) return reply(res, 400, { error: `kind(${kinds.join("/")}), value required` });
      const codes = await readJson(kosekiCodesFile, []);
      const existing = codes.find((c) => c.kind === b.kind && c.value === String(b.value));
      if (existing) return reply(res, 200, { ...existing, reused: true });
      const prefix = b.kind === "旧本籍" ? "KHB" : "KGC";
      const item = { id: crypto.randomUUID(), kind: b.kind, value: String(b.value),
        code: `${prefix}-${String(codes.filter((c) => c.kind === b.kind).length + 1).padStart(5, "0")}`,
        at: new Date().toISOString(), by: actor.id };
      codes.unshift(item);
      await writeJson(kosekiCodesFile, codes);
      await audit(req, "koseki-codes.custom", { kind: b.kind, code: item.code });
      return reply(res, 201, item);
    }

    // ── 仮戸籍（中間ファイル）の作成 ──
    if (pathname === "/api/koseki/karikoseki" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.registerId) return reply(res, 400, { error: "registerId required" });
      await audit(req, "koseki-karikoseki.create", { registerId: b.registerId });
      return reply(res, 201, { kind: "中間ファイル（仮戸籍）", registerId: String(b.registerId),
        karikosekiId: crypto.randomUUID(), draft: true, note: "仮戸籍ファイルに中間ファイルを作成（確定処理で本戸籍へ反映）" });
    }

    // ── 移記の事件メニュー選択（戸籍事項・身分事項）──
    if (pathname === "/api/koseki/iki/menu" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, {
        戸籍事項: ["新戸籍編製", "転籍", "戸籍改製", "再製", "氏の変更"],
        身分事項: ["出生", "婚姻", "離婚", "養子縁組", "養子離縁", "認知", "死亡", "国籍取得", "帰化"],
      });
    }
    if (pathname === "/api/koseki/iki/select" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const cats = ["戸籍事項", "身分事項"];
      if (!cats.includes(b.category) || !b.item) return reply(res, 400, { error: `category(${cats.join("/")}), item required` });
      await audit(req, "koseki-iki.select", { category: b.category, item: b.item });
      return reply(res, 200, { category: b.category, selected: String(b.item), note: "移記事件メニューから記録事項を選択" });
    }

    // ── 受付ファイルの新規作成（件数指定×番号取得ファイルから最新受領番号）──
    if (pathname === "/api/koseki/uketsuke/create" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const count = Math.max(1, Number(b.count) || 1);
      const store = await readJson(kosekiUketsukeFile, { counter: 0, files: [] });
      const files = Array.from({ length: count }, () => ({ receiptNumber: ++store.counter,
        createdAt: new Date().toISOString(), by: actor.id }));
      store.files.unshift(...files);
      await writeJson(kosekiUketsukeFile, store);
      await audit(req, "koseki-uketsuke.create", { count });
      return reply(res, 201, { created: count, receiptNumbers: files.map((f) => f.receiptNumber),
        note: "番号取得ファイルから最新の受領番号を取得して連番作成" });
    }

    // ── 不受理処分整理簿の出力（1年を超えない任意範囲）──
    if (pathname === "/api/koseki/fujuri/seiribo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.from || !b.to) return reply(res, 400, { error: "from, to required" });
      const oneYearLater = b.from.replace(/^(\d{4})/, (y) => String(Number(y) + 1));
      if (b.to > oneYearLater) return reply(res, 400, { error: "出力範囲は1年を超えない任意指定としてください" });
      await audit(req, "koseki-fujuri.seiribo", { from: b.from, to: b.to });
      return reply(res, 200, { document: "不受理処分整理簿", period: { from: b.from, to: b.to } });
    }

    // ── 公選法通知ファイルの作成（附票データ取得に準ずる）──
    if (pathname === "/api/koseki/kosenho/notify" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const events = Array.isArray(b.events) ? b.events : [];
      if (!events.length) return reply(res, 400, { error: "events[]{personId,eventType(死亡/国籍喪失等)} required" });
      await audit(req, "koseki-kosenho.notify", { events: events.length });
      return reply(res, 200, { file: "公選法通知ファイル", target: "選挙人名簿システム",
        basedOn: "4.7.18 戸籍附票データ取得〜4.7.19に準拠", count: events.length, rows: events });
    }

    // ── 年単位の事件表 ──
    if (pathname === "/api/koseki/jikenhyo" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const year = Number(searchParams.get("year") ?? new Date().getFullYear());
      const events = await readJson(kosekiEventsFile, []);
      const inYear = events.filter((e) => String(e.eventDate ?? e.createdAt ?? "").startsWith(String(year)));
      const byType = {};
      for (const e of inYear) byType[e.eventType ?? "その他"] = (byType[e.eventType ?? "その他"] ?? 0) + 1;
      await audit(req, "koseki-jikenhyo", { year, count: inYear.length });
      return reply(res, 200, { document: "事件表（年単位）", year, total: inYear.length, byType });
    }

    // ── 高齢者消除（基準日抽出＝住所は附票から・戸籍記載許可申請書）──
    if (pathname === "/api/koseki/koreisha/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const persons = Array.isArray(b.persons) ? b.persons : [];
      if (!b.baseDate || !persons.length) return reply(res, 400, { error: "baseDate, persons[]{personId,birthDate,fuhyoAddress} required" });
      const threshold = Number(b.ageThreshold ?? 100);
      const targets = persons.filter((p) => {
        const limit = p.birthDate?.replace(/^(\d{4})/, (y) => String(Number(y) + threshold));
        return limit && limit <= b.baseDate;
      }).map((p) => ({ personId: p.personId, birthDate: p.birthDate,
        address: p.fuhyoAddress ?? "", addressSource: "戸籍附票システム" }));
      await audit(req, "koseki-koreisha.extract", { baseDate: b.baseDate, targets: targets.length });
      return reply(res, 200, { baseDate: b.baseDate, ageThreshold: threshold, count: targets.length, targets });
    }
    if (pathname === "/api/koseki/koreisha/permit-application" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.personId) return reply(res, 400, { error: "personId required" });
      await audit(req, "koseki-koreisha.permit", { personId: b.personId });
      return reply(res, 200, { document: "高齢者消除に対する戸籍記載許可申請書", personId: String(b.personId),
        addressee: "管轄法務局", issuedAt: new Date().toISOString().slice(0, 10) });
    }

    // ── 委託業者用の仮の振り仮名通知ファイル作成（指定日切り出し）──
    if (pathname === "/api/koseki/furigana/vendor-file" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!b.asOf || !rows.length) return reply(res, 400, { error: "asOf(市区町村指定日), rows[]{personId,furigana,notifiedAt} required" });
      const cut = rows.filter((r) => (r.notifiedAt ?? "") <= b.asOf);
      await audit(req, "koseki-furigana.vendor-file", { asOf: b.asOf, count: cut.length });
      return reply(res, 200, { file: "委託業者用 振り仮名通知ファイル", asOf: b.asOf, count: cut.length, rows: cut });
    }

    // ── 受信データからの戸籍情報システムデータ作成 ──
    if (pathname === "/api/koseki/receive/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const messages = Array.isArray(b.messages) ? b.messages : [];
      if (!messages.length) return reply(res, 400, { error: "messages[]{messageType,payload} required" });
      const created = messages.filter((m) => m.messageType && m.payload)
        .map((m) => ({ messageType: m.messageType, recordId: crypto.randomUUID(), status: "作成済" }));
      await audit(req, "koseki-receive.import", { messages: messages.length, created: created.length });
      return reply(res, 200, { received: messages.length, created: created.length, records: created });
    }

    // ── 人口動態調査事務システムへの情報提供（出生・婚姻・離婚・死亡＋死産）──
    if (pathname === "/api/koseki/provide/vital" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const kinds = ["出生", "婚姻", "離婚", "死亡", "死産"];
      const events = Array.isArray(b.events) ? b.events : [];
      if (!events.length) return reply(res, 400, { error: `events[]{eventType(${kinds.join("/")}),receiptNumber} required` });
      const bad = events.find((e) => !kinds.includes(e.eventType));
      if (bad) return reply(res, 400, { error: `eventType は ${kinds.join("/")} のいずれか` });
      await audit(req, "koseki-provide.vital", { events: events.length });
      return reply(res, 200, { target: "人口動態調査事務システム", count: events.length,
        rows: events.map((e) => ({ eventType: e.eventType, receiptNumber: e.receiptNumber ?? "" })),
        note: "死産届は受付ファイル作成時に提供" });
    }

    // ── ログ（イベント・通信）の記録・照会 ──
    if (pathname === "/api/koseki/logs" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["イベント", "通信"];
      if (!kinds.includes(b.kind) || !b.message) return reply(res, 400, { error: `kind(${kinds.join("/")}), message required` });
      const logs = await readJson(kosekiLogsFile, []);
      logs.unshift({ id: crypto.randomUUID(), kind: b.kind, message: String(b.message),
        at: new Date().toISOString(), by: actor.id });
      await writeJson(kosekiLogsFile, logs);
      return reply(res, 201, { recorded: true, kind: b.kind });
    }
    if (pathname === "/api/koseki/logs" && req.method === "GET") {
      requireRole(actor, "AUDITOR");
      const kind = searchParams.get("kind") ?? "";
      const logs = await readJson(kosekiLogsFile, []);
      const hits = kind ? logs.filter((l) => l.kind === kind) : logs;
      return reply(res, 200, { kinds: ["イベント", "通信"], count: hits.length, logs: hits.slice(0, 200) });
    }

  return false;
}
