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

// ── 生活保護 最低生活費の算定（標準仕様書: 生活扶助(第1類×逓減率＋第2類)＋住宅扶助＋各種加算）──
//   基準額・逓減率は政令（級地別）。既定は例示の1級地-1（令和6年相当）の月額。
const SEIKATSU_STD = {
  // 第1類（個人別・年齢別 月額）
  class1: [
    { max: 2, amount: 44630 }, { max: 5, amount: 44630 }, { max: 11, amount: 46460 },
    { max: 19, amount: 49270 }, { max: 64, amount: 47420 }, { max: 74, amount: 45330 }, { max: 200, amount: 40860 },
  ],
  // 逓減率（世帯人員別）
  taper: { 1: 1.0, 2: 0.8548, 3: 0.7151, 4: 0.6010, 5: 0.5683 },
  // 第2類（世帯人員別 月額）
  class2: { 1: 28890, 2: 42420, 3: 47060, 4: 49080, 5: 49110 },
  housingCeiling: 53700, // 住宅扶助 上限（例示・単身）
};
function class1Amount(age, std) {
  const a = Number(age) || 0;
  return (std.class1.find((b) => a <= b.max) ?? std.class1[std.class1.length - 1]).amount;
}
function computeMinimumCostOfLiving({ members = [], housingCost = 0, additions = 0 }, std = SEIKATSU_STD) {
  const ms = Array.isArray(members) ? members : [];
  const n = ms.length;
  if (n === 0) return { minimumCost: 0, livingAssistance: 0, reason: "世帯員なし" };
  const class1Sum = ms.reduce((s, m) => s + class1Amount(m.age, std), 0);
  const taper = std.taper[Math.min(5, n)] ?? std.taper[5];
  const class1 = Math.floor(class1Sum * taper);
  const class2 = std.class2[Math.min(5, n)] ?? std.class2[5];
  const livingAssistance = class1 + class2; // 生活扶助基準額
  const housing = Math.min(Number(housingCost) || 0, std.housingCeiling); // 住宅扶助
  const add = Math.max(0, Number(additions) || 0); // 各種加算（障害・母子・児童養育等）
  const minimumCost = livingAssistance + housing + add;
  return { minimumCost, livingAssistance, class1, class2, taper, housing, additions: add, householdSize: n };
}

// ── 収入認定額の算定（標準仕様書 0211390）。勤労収入は基礎控除・必要経費(世帯員単位)を控除、その他収入は全額認定 ──
function computeShunyuNintei({ 勤労収入 = 0, その他収入 = 0, 基礎控除 = 0, 必要経費 = [] }) {
  const work = Math.max(0, Number(勤労収入) || 0);
  const other = Math.max(0, Number(その他収入) || 0);
  const deduction = Math.max(0, Number(基礎控除) || 0);
  const expenses = (Array.isArray(必要経費) ? 必要経費 : []).reduce((s, e) =>
    s + (typeof e === "object" ? (Number(e.amount) || 0) : (Number(e) || 0)), 0);
  const 勤労収入認定 = Math.max(0, work - deduction - expenses);
  return { 勤労収入: work, その他収入: other, 基礎控除: deduction, 必要経費: expenses,
    勤労収入認定, 収入認定額: 勤労収入認定 + other };
}
// ── 保護費の算定（標準仕様書 0211376/0211378/0211379）。最低生活費(基準生活費＋各扶助＋加算)−収入認定額。日割対応 ──
function computeHogohi(input = {}) {
  const adds = input.加算 || {};
  const 加算合計 = ["障害", "母子", "児童養育", "介護", "冬季加算", "期末一時扶助", "その他"]
    .reduce((s, k) => s + (Number(adds[k]) || 0), 0);
  const education = Math.max(0, Number(input.教育扶助) || 0);
  const mc = computeMinimumCostOfLiving({
    members: input.members || [], housingCost: input.housingCost || 0,
    additions: 加算合計 + education,
  });
  const nintei = computeShunyuNintei(input.収入 || {});
  let 保護費 = Math.max(0, mc.minimumCost - nintei.収入認定額);
  let 日割後保護費 = 保護費;
  if (input.日割 && Number(input.日割.days) > 0 && Number(input.日割.monthDays) > 0) {
    日割後保護費 = Math.floor(保護費 * Number(input.日割.days) / Number(input.日割.monthDays));
  }
  return {
    最低生活費: mc.minimumCost,
    内訳: { 生活扶助: mc.livingAssistance, 第1類: mc.class1, 第2類: mc.class2, 住宅扶助: mc.housing,
      教育扶助: education, 加算: 加算合計, 加算内訳: adds },
    収入認定額: nintei.収入認定額, 収入認定内訳: nintei,
    保護費, 日割後保護費,
    算定根拠: `最低生活費${mc.minimumCost}−収入認定${nintei.収入認定額}=保護費${保護費}` +
      (日割後保護費 !== 保護費 ? `（日割 ${input.日割.days}/${input.日割.monthDays}日＝${日割後保護費}）` : ""),
  };
}
// ── 同月内の複数変更の追加支給・過支給相殺（標準仕様書 0211395）──
function computeSeihoSousai({ items = [] }) {
  const list = Array.isArray(items) ? items : [];
  const 追加支給 = list.filter((x) => x.kind === "追加支給").reduce((s, x) => s + (Number(x.amount) || 0), 0);
  const 過支給 = list.filter((x) => x.kind === "過支給").reduce((s, x) => s + (Number(x.amount) || 0), 0);
  const net = 追加支給 - 過支給;
  return { 追加支給, 過支給, 相殺後: net,
    区分: net > 0 ? "追加支給" : net < 0 ? "過支給(返還)" : "相殺ゼロ",
    note: "同月内の複数変更を相殺し、追加支給/過支給返還額を確定" };
}

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 protectionCasesFile = join(dataRoot, "protection-cases.json");
  const householdMembersFile = join(dataRoot, "household-members.json");
  const paymentRecordsFile = join(dataRoot, "payment-records.json");
  const seihoTicketsFile = join(dataRoot, "seiho-tickets.json");
  const seihoCaseDocsFile = join(dataRoot, "seiho-case-docs.json");
  const seihoRepaymentsFile = join(dataRoot, "seiho-repayments.json");
  const seihoReceiptChecksFile = join(dataRoot, "seiho-receipt-checks.json");


    // ── 生活保護 保護ケース (Protection Cases) ───────────────────────────

    const PROTECTION_TYPES = ["livelihood", "housing", "education", "medical", "nursing_care", "maternity", "employment_support", "funeral"];

    if (pathname === "/api/protection-cases" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const cases = (await dbSelect("seikatsu_hogo.protection_case")) ?? await readJson(protectionCasesFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      const { status } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      const filtered = status ? cases.filter((c) => c.status === status) : cases;
      return reply(res, 200, filtered.map((c) => maskRecord(actor, restrictions, c)));
    }

    if (pathname === "/api/protection-cases" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.applicantName || !body.address) return reply(res, 400, { error: "applicantName, address required" });
      const cases = (await dbSelect("seikatsu_hogo.protection_case")) ?? await readJson(protectionCasesFile, []);
      const caseNumber = "SG-" + Date.now().toString(36).toUpperCase();
      const protectionCase = { id: crypto.randomUUID(), caseNumber, applicantName: body.applicantName, address: body.address, applicationDate: body.applicationDate ?? new Date().toISOString().slice(0, 10), protectionTypes: body.protectionTypes ?? ["livelihood"], status: "applied", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("seikatsu_hogo.protection_case", protectionCase); if (dbInserted === null) { cases.push(protectionCase); await writeJson(protectionCasesFile, cases); }
      await audit(req, "protection-case.apply", { id: protectionCase.id, caseNumber });
      return reply(res, 201, protectionCase);
    }

    const caseMatch = pathname.match(/^\/api\/protection-cases\/([^/]+)$/);
    if (caseMatch) {
      requireRole(actor, "CLERK");
      const protectionCase = (await dbSelectOne("seikatsu_hogo.protection_case", caseMatch[1]))
        ?? (await readJson(protectionCasesFile, [])).find((c) => c.id === caseMatch[1]);
      if (!protectionCase) return reply(res, 404, { error: "case not found" });
      if (req.method === "GET") return reply(res, 200, protectionCase);
      if (req.method === "PUT") {
        const body = await parseBody(req);
        Object.assign(protectionCase, body, { id: protectionCase.id, caseNumber: protectionCase.caseNumber, updatedAt: new Date().toISOString() });
        const upd = await dbUpdate("seikatsu_hogo.protection_case", protectionCase.id, protectionCase); if (upd === null) { const wl = await readJson(protectionCasesFile, []); const wi = wl.findIndex((x) => x.id === protectionCase.id); if (wi !== -1) { wl[wi] = protectionCase; } await writeJson(protectionCasesFile, wl); }
        await audit(req, "protection-case.update", { id: protectionCase.id });
        return reply(res, 200, protectionCase);
      }
    }

    const caseDecideMatch = pathname.match(/^\/api\/protection-cases\/([^/]+)\/decide$/);
    if (caseDecideMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const protectionCase = (await dbSelectOne("seikatsu_hogo.protection_case", caseDecideMatch[1]))
        ?? (await readJson(protectionCasesFile, [])).find((c) => c.id === caseDecideMatch[1]);
      if (!protectionCase) return reply(res, 404, { error: "case not found" });
      if (!["applied", "investigating"].includes(protectionCase.status)) return reply(res, 409, { error: "case is not in applicable status for decision" });
      const body = await parseBody(req);
      if (!["approved", "rejected"].includes(body.decision)) return reply(res, 400, { error: "decision must be approved or rejected" });
      protectionCase.status = body.decision;
      protectionCase.decisionDate = new Date().toISOString().slice(0, 10);
      protectionCase.decisionReason = body.reason ?? "";
      protectionCase.decidedBy = actor.id;
      const upd = await dbUpdate("seikatsu_hogo.protection_case", protectionCase.id, protectionCase); if (upd === null) { const wl = await readJson(protectionCasesFile, []); const wi = wl.findIndex((x) => x.id === protectionCase.id); if (wi !== -1) { wl[wi] = protectionCase; } await writeJson(protectionCasesFile, wl); }
      await audit(req, "protection-case.decide", { id: protectionCase.id, decision: body.decision });
      return reply(res, 200, protectionCase);
    }

    // ── 世帯員 (Household Members) ────────────────────────────────────────

    const householdMatch = pathname.match(/^\/api\/protection-cases\/([^/]+)\/members$/);
    if (householdMatch) {
      requireRole(actor, "CLERK");
      const members = await readJson(householdMembersFile, []);
      const caseId = householdMatch[1];
      if (req.method === "GET") return reply(res, 200, members.filter((m) => m.caseId === caseId));
      if (req.method === "POST") {
        const body = await parseBody(req);
        if (!body.fullName || !body.birthDate) return reply(res, 400, { error: "fullName, birthDate required" });
        const member = { id: crypto.randomUUID(), caseId, fullName: body.fullName, birthDate: body.birthDate, relationship: body.relationship ?? "self", createdBy: actor.id };
        members.push(member);
        await writeJson(householdMembersFile, members);
        await audit(req, "household-member.add", { id: member.id, caseId });
        return reply(res, 201, member);
      }
    }

    // ── 支給記録 (Payment Records) ────────────────────────────────────────

    if (pathname === "/api/payment-records" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const payments = (await dbSelect("seikatsu_hogo.payment_record")) ?? await readJson(paymentRecordsFile, []);
      const { caseId } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      return reply(res, 200, caseId ? payments.filter((p) => p.caseId === caseId) : payments);
    }

    if (pathname === "/api/payment-records" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.caseId || !body.period || body.amount === undefined) return reply(res, 400, { error: "caseId, period, amount required" });
      const protectionCase = (await dbSelectOne("seikatsu_hogo.protection_case", body.caseId))
        ?? (await readJson(protectionCasesFile, [])).find((c) => c.id === body.caseId);
      if (!protectionCase) return reply(res, 404, { error: "case not found" });
      if (protectionCase.status !== "approved") return reply(res, 409, { error: "case is not approved" });
      const payments = (await dbSelect("seikatsu_hogo.payment_record")) ?? await readJson(paymentRecordsFile, []);
      const payment = { id: crypto.randomUUID(), caseId: body.caseId, period: body.period, amount: body.amount, status: "scheduled", createdBy: actor.id, createdAt: new Date().toISOString() };
      payments.push(payment);
      await writeJson(paymentRecordsFile, payments);
      await audit(req, "payment-record.create", { id: payment.id, caseId: body.caseId, period: body.period });
      return reply(res, 201, payment);
    }

    const paymentStatusMatch = pathname.match(/^\/api\/payment-records\/([^/]+)\/pay$/);
    if (paymentStatusMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const payment = (await dbSelectOne("seikatsu_hogo.payment_record", paymentStatusMatch[1]))
        ?? (await readJson(paymentRecordsFile, [])).find((p) => p.id === paymentStatusMatch[1]);
      if (!payment) return reply(res, 404, { error: "payment not found" });
      if (payment.status === "paid") return reply(res, 409, { error: "already paid" });
      payment.status = "paid";
      payment.paidAt = new Date().toISOString();
      payment.paidBy = actor.id;
      const upd = await dbUpdate("seikatsu_hogo.payment_record", payment.id, payment); if (upd === null) { const wl = await readJson(paymentRecordsFile, []); const wi = wl.findIndex((x) => x.id === payment.id); if (wi !== -1) { wl[wi] = payment; } await writeJson(paymentRecordsFile, wl); }
      await audit(req, "payment-record.pay", { id: payment.id });
      return reply(res, 200, payment);
    }


    const caseDeleteMatch = pathname.match(/^\/api\/protection-cases\/([^\/]+)$/);
    if (caseDeleteMatch && req.method === "DELETE") {
      requireRole(actor, "ADMIN");
      const delId = caseDeleteMatch[1];
      const deleted = await dbDelete("seikatsu_hogo.protection_case", delId);
      if (deleted === null) {
        const _items = await readJson(protectionCasesFile, []);
        const _idx = _items.findIndex((x) => x.id === delId);
        if (_idx === -1) return reply(res, 404, { error: "protection case not found" });
        _items.splice(_idx, 1);
        await writeJson(protectionCasesFile, _items);
      }
      await audit(req, "protection-case.delete", { id: delId });
      return reply(res, 204, {});
    }


    // ── 最低生活費の算定（標準仕様書準拠）──
    if (pathname === "/api/seikatsu/minimum-cost" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!Array.isArray(body.members) || body.members.length === 0) return reply(res, 400, { error: "members (age) is required" });
      const result = computeMinimumCostOfLiving(body);
      await audit(req, "seikatsu.minimum-cost", { householdSize: result.householdSize, minimumCost: result.minimumCost });
      return reply(res, 200, result);
    }

    // ── 収入認定額の算定（勤労収入−基礎控除−必要経費＋その他収入）──
    if (pathname === "/api/seiho/shunyu-nintei" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const result = computeShunyuNintei(await parseBody(req));
      await audit(req, "seiho.shunyu-nintei", { nintei: result.収入認定額 });
      return reply(res, 200, result);
    }

    // ── 保護費の算定（最低生活費−収入認定額。基準生活費・各扶助・加算・日割を反映）──
    if (pathname === "/api/seiho/hogohi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!Array.isArray(body.members) || body.members.length === 0) {
        return reply(res, 400, { error: "members（世帯員。age）が必須です" });
      }
      const result = computeHogohi(body);
      await audit(req, "seiho.hogohi", { minimumCost: result.最低生活費, hogohi: result.日割後保護費 });
      return reply(res, 200, result);
    }

    // ── 同月内の複数変更の追加支給・過支給相殺 ──
    if (pathname === "/api/seiho/sousai" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!Array.isArray(body.items) || body.items.length === 0) {
        return reply(res, 400, { error: "items[]{kind:追加支給|過支給, amount} が必須です" });
      }
      const result = computeSeihoSousai(body);
      await audit(req, "seiho.sousai", { net: result.相殺後, kubun: result.区分 });
      return reply(res, 200, result);
    }

    // ════ 医療扶助・介護扶助の券管理／レセプト点検／ケース文書／返還金督促（標準仕様書）════

    // ── 券（医療券・調剤券・介護券・施術券）の発券管理 ──
    //   廃止・停止ケースでも保護受給期間内の日付なら作成可。介護券は被保険者番号がH始まりでなければ「併用」。
    //   他法の有効期限内は発券予定から自動除外（suppressedByOtherLaw）。再発行・連名簿（連名介護券）対応。
    const TICKET_KINDS = ["医療券", "調剤券", "介護券", "施術券"];
    if (pathname === "/api/seiho/tickets" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || !TICKET_KINDS.includes(b.kind) || !b.validFrom || !b.validTo) {
        return reply(res, 400, { error: `caseId, kind(${TICKET_KINDS.join("/")}), validFrom, validTo が必須です` });
      }
      const cases = await readJson(protectionCasesFile, []);
      const protectionCase = cases.find((c) => c.id === b.caseId);
      if (!protectionCase) return reply(res, 404, { error: "case not found" });
      // 廃止・停止ケース: 券の有効期間が保護受給期間内に収まる場合のみ作成可
      const caseEnd = protectionCase.endDate ?? protectionCase.closedAt ?? "";
      if (["廃止", "停止", "closed", "suspended"].includes(protectionCase.status) && caseEnd && b.validTo > caseEnd) {
        return reply(res, 409, { error: "廃止・停止ケースは保護受給期間内の券のみ作成できます", caseEnd });
      }
      if (b.otherLawValidTo && b.validFrom <= b.otherLawValidTo) {
        return reply(res, 409, { error: "他法の有効期限内のため発券対象外です（期限終了後に作成してください）", suppressedByOtherLaw: true });
      }
      const tickets = await readJson(seihoTicketsFile, []);
      const combined = b.kind === "介護券" && b.insurerNumber && !String(b.insurerNumber).startsWith("H");
      const ticket = {
        id: crypto.randomUUID(), caseId: b.caseId, kind: b.kind,
        recipientId: b.recipientId ?? "", providerCode: b.providerCode ?? "",
        validFrom: String(b.validFrom), validTo: String(b.validTo),
        insurerNumber: b.insurerNumber ?? "", ticketClass: combined ? "併用" : "単独",
        reissueOf: b.reissueOf ?? null, // 再発行元
        issuedBy: actor.id, issuedAt: new Date().toISOString(),
      };
      tickets.unshift(ticket);
      await writeJson(seihoTicketsFile, tickets);
      await audit(req, "seiho-ticket.issue", { id: ticket.id, kind: ticket.kind, reissue: !!ticket.reissueOf });
      return reply(res, 201, ticket);
    }
    if (pathname === "/api/seiho/tickets" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const tickets = await readJson(seihoTicketsFile, []);
      const kind = searchParams.get("kind") ?? "";
      const caseId = searchParams.get("caseId") ?? "";
      let list = tickets;
      if (kind) list = list.filter((t) => t.kind === kind);
      if (caseId) list = list.filter((t) => t.caseId === caseId);
      // 連名簿（同一機関宛にまとめた連名介護券等）
      if (searchParams.get("registry") === "1") {
        const byProvider = {};
        for (const t of list) (byProvider[t.providerCode || "未指定"] ??= []).push(t);
        return reply(res, 200, Object.entries(byProvider).map(([providerCode, items]) => ({ providerCode, count: items.length, tickets: items })));
      }
      return reply(res, 200, list.slice(0, 500));
    }
    // 有効期限が切れる券の抽出（継続の要否意見書を自動作成する対象）
    if (pathname === "/api/seiho/tickets/expiring" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const asOf = searchParams.get("asOf") ?? new Date().toISOString().slice(0, 10);
      const withinDays = Number(searchParams.get("withinDays") ?? 30);
      const limit = new Date(asOf);
      limit.setDate(limit.getDate() + withinDays);
      const limitStr = limit.toISOString().slice(0, 10);
      const tickets = await readJson(seihoTicketsFile, []);
      const expiring = tickets.filter((t) => t.validTo >= asOf && t.validTo <= limitStr);
      return reply(res, 200, expiring.map((t) => ({ ...t, document: "継続の要否意見書（自動作成対象）" })));
    }

    // ── レセプト点検（資格突合・複数月点検・自動実行・進捗）──
    if (pathname === "/api/seiho/receipt-checks" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.receipts) || !b.month) return reply(res, 400, { error: "month, receipts[] required" });
      const tickets = await readJson(seihoTicketsFile, []);
      const findings = [];
      for (const r of b.receipts) {
        // 資格突合: 診療月に有効な券（医療/介護）があるか
        const valid = tickets.some((t) => t.recipientId === r.recipientId && t.validFrom <= `${r.month}-31` && t.validTo >= `${r.month}-01`);
        if (!valid) findings.push({ recipientId: r.recipientId, providerCode: r.providerCode ?? "", month: r.month ?? b.month, issue: "資格なし（券発行履歴と不突合）" });
      }
      // 複数月点検: 同一機関×同一患者の重複請求
      const seen = new Set();
      for (const r of b.receipts) {
        const key = `${r.providerCode}|${r.recipientId}|${r.month ?? b.month}`;
        if (seen.has(key)) findings.push({ recipientId: r.recipientId, providerCode: r.providerCode, month: r.month ?? b.month, issue: "同一機関・同一患者の重複" });
        seen.add(key);
      }
      const checks = await readJson(seihoReceiptChecksFile, []);
      const run = { id: crypto.randomUUID(), month: b.month, total: b.receipts.length, findings, auto: b.auto === true, by: actor.id, at: new Date().toISOString(), status: "完了" };
      checks.unshift(run);
      await writeJson(seihoReceiptChecksFile, checks);
      await audit(req, "seiho-receipt.check", { month: b.month, total: run.total, findings: findings.length });
      return reply(res, 200, run);
    }
    if (pathname === "/api/seiho/receipt-checks" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const checks = await readJson(seihoReceiptChecksFile, []);
      return reply(res, 200, { runs: checks.slice(0, 100), progress: { total: checks.length, done: checks.filter((c) => c.status === "完了").length } });
    }

    // ── ケース文書管理（訪問計画・主治医訪問調査票・ケース診断会議記録票・検診命令書・扶養義務調査依頼書 等）──
    //   「以下の情報をシステム上の画面で確認できること」要件群: 文書種別×ケースで登録・照会できる。
    if (pathname === "/api/seiho/case-docs" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || !b.docType) return reply(res, 400, { error: "caseId, docType required" });
      const docs = await readJson(seihoCaseDocsFile, []);
      const doc = { id: crypto.randomUUID(), caseId: String(b.caseId), docType: String(b.docType),
        targetType: b.targetType === "個人" ? "個人" : "世帯", // 世帯・個人単位の作成
        content: b.content ?? {}, status: b.status ?? "作成済", by: actor.id, at: new Date().toISOString() };
      docs.unshift(doc);
      await writeJson(seihoCaseDocsFile, docs);
      await audit(req, "seiho-doc.create", { caseId: doc.caseId, docType: doc.docType });
      return reply(res, 201, doc);
    }
    if (pathname === "/api/seiho/case-docs" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const docs = await readJson(seihoCaseDocsFile, []);
      const caseId = searchParams.get("caseId") ?? "";
      const docType = searchParams.get("docType") ?? "";
      let list = docs;
      if (caseId) list = list.filter((d) => d.caseId === caseId);
      if (docType) list = list.filter((d) => d.docType === docType);
      return reply(res, 200, list.slice(0, 300));
    }

    // ── 返還金（63条返還・78条徴収）の調定と督促状（分割債権ごと・名宛人選択・履歴）──
    if (pathname === "/api/seiho/repayments" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || b.amount === undefined) return reply(res, 400, { error: "caseId, amount required" });
      const repayments = await readJson(seihoRepaymentsFile, []);
      const item = { id: crypto.randomUUID(), caseId: String(b.caseId),
        legalBasis: b.legalBasis === "78条" ? "78条" : "63条", amount: Number(b.amount),
        paidTotal: 0, installments: Math.max(1, Number(b.installments) || 1), // 分割債権数
        reminders: [], by: actor.id, at: new Date().toISOString() };
      repayments.unshift(item);
      await writeJson(seihoRepaymentsFile, repayments);
      await audit(req, "seiho-repayment.create", { id: item.id, legalBasis: item.legalBasis, amount: item.amount });
      return reply(res, 201, item);
    }
    {
      const remindMatch = pathname.match(/^\/api\/seiho\/repayments\/([^/]+)\/remind$/);
      if (remindMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const addressees = ["本人", "世帯員", "相続人", "法定代理人", "代理人"];
        const addressee = addressees.includes(b.addressee) ? b.addressee : "本人";
        const repayments = await readJson(seihoRepaymentsFile, []);
        const idx = repayments.findIndex((r) => r.id === remindMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "repayment not found" });
        const installment = Math.min(Math.max(1, Number(b.installment) || 1), repayments[idx].installments);
        const reminder = { id: crypto.randomUUID(), installment, addressee, at: new Date().toISOString(), by: actor.id };
        repayments[idx].reminders.push(reminder);
        await writeJson(seihoRepaymentsFile, repayments);
        await audit(req, "seiho-repayment.remind", { id: repayments[idx].id, installment, addressee });
        return reply(res, 201, { ...reminder, history: repayments[idx].reminders.length });
      }
    }

    // ════ 支払締め/保留・全銀協・消込/共通納税・不納欠損・EUC/グラフ・ケース記録・要否判定ほか・レセプト再審査（標準仕様書）════

    const seihoShimeFile = join(dataRoot, "seiho-shime.json");
    const seihoShunoFile = join(dataRoot, "seiho-shuno.json");
    const seihoCaseExtFile = join(dataRoot, "seiho-case-ext.json");
    const seihoSaishinsaFile = join(dataRoot, "seiho-saishinsa.json");

    // ── 支払締め処理（定例/随時/施設払い/業者払い×本締め/仮締め・取消・保留/解除・締め前自動バックアップ）──
    if (pathname === "/api/seiho/pay/shime" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["定例", "随時", "施設払い", "業者払い"];
      const modes = ["本締め", "仮締め"];
      if (!kinds.includes(b.kind) || !modes.includes(b.mode)) return reply(res, 400, { error: `kind(${kinds.join("/")}), mode(${modes.join("/")}) required` });
      const store = await readJson(seihoShimeFile, { runs: [] });
      const run = { id: crypto.randomUUID(), kind: b.kind, mode: b.mode, unit: b.unit ?? "支給単位",
        backupSaved: true, status: "締め済", at: new Date().toISOString(), by: actor.id };
      store.runs.unshift(run);
      await writeJson(seihoShimeFile, store);
      await audit(req, "seiho-pay.shime", { kind: b.kind, mode: b.mode });
      return reply(res, 201, { ...run, note: "締め処理前にバックアップデータを自動保存" });
    }
    {
      const m = pathname.match(/^\/api\/seiho\/pay\/shime\/([^/]+)\/cancel$/);
      if (m && req.method === "POST") {
        requireRole(actor, "CLERK");
        const store = await readJson(seihoShimeFile, { runs: [] });
        const idx = store.runs.findIndex((r) => r.id === m[1]);
        if (idx === -1) return reply(res, 404, { error: "締め処理が見つかりません" });
        store.runs[idx] = { ...store.runs[idx], status: "締め解除", cancelledAt: new Date().toISOString() };
        await writeJson(seihoShimeFile, store);
        await audit(req, "seiho-pay.shime-cancel", { id: m[1] });
        return reply(res, 200, { ...store.runs[idx], note: "支給単位で締め処理を取消（締め処理解除）" });
      }
    }
    if (pathname === "/api/seiho/pay/hold" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const actions = ["保留", "解除"];
      if (!b.recipientId || !actions.includes(b.action)) return reply(res, 400, { error: `recipientId, action(${actions.join("/")}) required` });
      await audit(req, "seiho-pay.hold", { recipientId: b.recipientId, action: b.action });
      return reply(res, 200, { recipientId: String(b.recipientId), action: b.action,
        ...(b.action === "解除" ? { payHeldMonths: b.heldMonths ?? 1, note: "保留解除時に支給保留月分を支給" } : {}) });
    }

    // ── 支給予定額（支給対象者ごと振込先別の金額確認・扶助内訳/細目つき）──
    if (pathname === "/api/seiho/pay/plan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{recipientId,account,fujo,saimoku,amount} required" });
      const byRecipient = {};
      for (const r of rows) {
        const rec = (byRecipient[r.recipientId] ??= { recipientId: String(r.recipientId), byAccount: {} });
        const acc = (rec.byAccount[r.account ?? "窓口"] ??= { total: 0, breakdown: [] });
        acc.total += Number(r.amount ?? 0);
        acc.breakdown.push({ 扶助内訳: r.fujo ?? "", 扶助細目: r.saimoku ?? "", amount: Number(r.amount ?? 0) });
      }
      await audit(req, "seiho-pay.plan", { recipients: Object.keys(byRecipient).length });
      return reply(res, 200, { recipients: Object.values(byRecipient).map((r) => ({
        recipientId: r.recipientId,
        accounts: Object.entries(r.byAccount).map(([account, v]) => ({ account, ...v })) })) });
    }

    // ── 代理納付の超過エラー抽出（代理納付額＞扶助費）──
    if (pathname === "/api/seiho/pay/dairi-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{recipientId,fujoAmount,dairiAmount} required" });
      const errors = rows.filter((r) => Number(r.dairiAmount ?? 0) > Number(r.fujoAmount ?? 0))
        .map((r) => ({ recipientId: r.recipientId, issue: "代理納付額が扶助費を超過" }));
      await audit(req, "seiho-pay.dairi-check", { errors: errors.length });
      return reply(res, 200, { checked: rows.length, ok: errors.length === 0, errors });
    }

    // ── 全銀協ファイル（総合振込・口座振替依頼＝全銀協/独自フォーマット）──
    if (pathname === "/api/seiho/pay/zengin" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["総合振込", "口座振替依頼"];
      const formats = ["全銀協", "独自"];
      if (!kinds.includes(b.kind)) return reply(res, 400, { error: `kind(${kinds.join("/")}) required` });
      const format = formats.includes(b.format) ? b.format : "全銀協";
      const rows = Array.isArray(b.rows) ? b.rows : [];
      await audit(req, "seiho-pay.zengin", { kind: b.kind, format, rows: rows.length });
      return reply(res, 200, { kind: b.kind, format: format === "全銀協" ? "全国銀行協会フォーマット" : "独自フォーマット",
        count: rows.length, records: rows.map((r) => ({ bankCode: r.bankCode ?? "0000",
          accountNumber: r.accountNumber ?? "", kanaName: r.kanaName ?? "", amount: Number(r.amount ?? 0) })) });
    }

    // ── 収納消込（窓口/コンビニ/共通納税IFS取込→消込用データ→一括/個別/仮消込・本料/延滞金別・時効中断）──
    if (pathname === "/api/seiho/shuno/import-ifs" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const sources = ["共通納税IFS", "公金収納サービス", "コンビニ収納"];
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!sources.includes(b.source) || !rows.length) return reply(res, 400, { error: `source(${sources.join("/")}), rows[] required` });
      const store = await readJson(seihoShunoFile, { pending: [] });
      const items = rows.map((r) => ({ id: crypto.randomUUID(), levyId: String(r.levyId ?? ""),
        principal: Number(r.principal ?? 0), entaikin: Number(r.entaikin ?? 0),
        flash: b.flash === true, source: b.source }));
      store.pending.unshift(...items);
      await writeJson(seihoShunoFile, store);
      await audit(req, "seiho-shuno.import", { source: b.source, rows: items.length, flash: b.flash === true });
      return reply(res, 200, { source: b.source, imported: items.length,
        keshikomiData: items, note: "取り込んだ納付データから消込用データを作成" });
    }
    if (pathname === "/api/seiho/shuno/keshikomi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["一括", "個別", "仮消込"];
      if (!modes.includes(b.mode)) return reply(res, 400, { error: `mode(${modes.join("/")}) required` });
      const rows = Array.isArray(b.rows) ? b.rows : [];
      const results = rows.map((r) => ({ levyId: String(r.levyId ?? ""),
        principal: Number(r.principal ?? 0), entaikin: Number(r.entaikin ?? 0),
        status: b.mode === "仮消込" ? "仮消込（速報）" : "消込済",
        note: "本料と延滞金をそれぞれの調定に対して消込" }));
      await audit(req, "seiho-shuno.keshikomi", { mode: b.mode, rows: results.length, channel: b.channel ?? "窓口" });
      return reply(res, 200, { mode: b.mode, channel: b.channel ?? "窓口",
        supportedChannels: ["窓口", "コンビニ収納", "共通納税", "口座振替", "窓口支給（事務所払）消込"],
        count: results.length, results });
    }
    if (pathname === "/api/seiho/shuno/jiko-chudan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.levyId || !b.reason) return reply(res, 400, { error: "levyId, reason(承認/一部納付/督促等) required" });
      await audit(req, "seiho-shuno.jiko-chudan", { levyId: b.levyId });
      return reply(res, 200, { levyId: String(b.levyId), 時効中断: true, reason: String(b.reason),
        newBaseDate: new Date().toISOString().slice(0, 10) });
    }

    // ── 不納欠損（条件指定抽出・個別/一括処理・取消）──
    if (pathname === "/api/seiho/funo-kesson" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const actions = ["抽出", "個別", "一括", "取消"];
      if (!actions.includes(b.action)) return reply(res, 400, { error: `action(${actions.join("/")}) required` });
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (b.action === "抽出") {
        const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
        const targets = rows.filter((r) => (r.expiresAt ?? "9999") <= asOf || r.shobunTeishi === true)
          .map((r) => ({ saikenId: r.saikenId, reason: r.shobunTeishi ? "滞納処分停止" : "時効到来" }));
        await audit(req, "seiho-funo-kesson.extract", { targets: targets.length });
        return reply(res, 200, { action: "抽出", conditions: b.conditions ?? "福祉事務所指定条件", count: targets.length, targets });
      }
      const processed = (b.action === "個別" ? rows.slice(0, 1) : rows)
        .map((r) => ({ saikenId: r.saikenId, status: b.action === "取消" ? "不納欠損取消" : "不納欠損処理済" }));
      await audit(req, "seiho-funo-kesson", { action: b.action, processed: processed.length });
      return reply(res, 200, { action: b.action, processed });
    }

    // ── EUC（演算子抽出・グラフ作成）──
    if (pathname === "/api/seiho/euc/query" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[] required" });
      const OPS = { "=": (a, v) => String(a) === String(v), "≧": (a, v) => Number(a) >= Number(v),
        "≦": (a, v) => Number(a) <= Number(v), "部分一致": (a, v) => String(a).includes(String(v)) };
      const conditions = Array.isArray(b.conditions) ? b.conditions : [];
      const hits = conditions.length ? rows.filter((r) => conditions.every((c) => (OPS[c.op] ?? (() => false))(r[c.field], c.value))) : rows;
      let chart = null;
      if (b.chart?.groupBy) {
        const groups = {};
        for (const r of hits) groups[String(r[b.chart.groupBy])] = (groups[String(r[b.chart.groupBy])] ?? 0) + 1;
        chart = { type: b.chart.type ?? "bar", labels: Object.keys(groups), values: Object.values(groups) };
      }
      await audit(req, "seiho-euc.query", { hits: hits.length, chart: !!chart });
      return reply(res, 200, { count: hits.length, results: hits, chart });
    }

    // ── ケース記録の拡張（会議区分コード追加・診断結果引用・イメージ起案添付・タブレット取込/反映）──
    if (pathname === "/api/seiho/case-records/meeting-codes" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.code || !b.label) return reply(res, 400, { error: "code, label required" });
      const ext = await readJson(seihoCaseExtFile, { meetingCodes: [], records: [] });
      ext.meetingCodes.push({ code: String(b.code), label: String(b.label) });
      await writeJson(seihoCaseExtFile, ext);
      await audit(req, "seiho-case.meeting-code", { code: b.code });
      return reply(res, 201, { added: { code: String(b.code), label: String(b.label) }, codes: ext.meetingCodes });
    }
    if (pathname === "/api/seiho/case-records/from-diagnosis" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || !b.diagnosisResult) return reply(res, 400, { error: "caseId, diagnosisResult required" });
      const ext = await readJson(seihoCaseExtFile, { meetingCodes: [], records: [] });
      const record = { id: crypto.randomUUID(), caseId: String(b.caseId), kind: "ケース記録",
        body: `【ケース診断実施結果より引用】${b.diagnosisResult}`, at: new Date().toISOString(), by: actor.id };
      ext.records.unshift(record);
      await writeJson(seihoCaseExtFile, ext);
      await audit(req, "seiho-case.from-diagnosis", { caseId: b.caseId });
      return reply(res, 201, record);
    }
    if (pathname === "/api/seiho/case-records/tablet-import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const items = Array.isArray(b.items) ? b.items : [];
      if (!b.caseId || !items.length) return reply(res, 400, { error: "caseId, items[]{kind(記録/画像),content} required" });
      const ext = await readJson(seihoCaseExtFile, { meetingCodes: [], records: [] });
      const imported = items.map((i) => ({ id: crypto.randomUUID(), caseId: String(b.caseId),
        kind: i.kind === "画像" ? "訪問撮影画像" : "訪問記録", source: "タブレット端末",
        content: String(i.content ?? ""), reflectedToCaseRecord: true, at: new Date().toISOString() }));
      ext.records.unshift(...imported);
      await writeJson(seihoCaseExtFile, ext);
      await audit(req, "seiho-case.tablet-import", { caseId: b.caseId, items: imported.length });
      return reply(res, 200, { imported: imported.length, records: imported,
        note: "タブレット保存情報を取込み、記録・撮影画像をケース記録へ反映" });
    }
    if (pathname === "/api/seiho/kian/attach-image" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.kianId || !b.imageId) return reply(res, 400, { error: "kianId, imageId required" });
      await audit(req, "seiho-kian.attach", { kianId: b.kianId });
      return reply(res, 200, { kianId: String(b.kianId), attached: [String(b.imageId)],
        note: "取り込んだイメージデータを起案時に添付" });
    }

    // ── 要否判定・検診料・査察確認・廃止停止ケース・長期入院・一時扶助随時払 ──
    if (pathname === "/api/seiho/youhi/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const income = Number(b.householdIncome ?? 0);
      const minimum = Number(b.minimumCost ?? 0);
      if (!minimum) return reply(res, 400, { error: "householdIncome, minimumCost required" });
      const required = income < minimum;
      await audit(req, "seiho-youhi.judge", { required });
      return reply(res, 200, { stage: "申請受理後", 要否: required ? "要保護" : "否", gap: minimum - income });
    }
    if (pathname === "/api/seiho/kenshin-fee/certify" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.householdId) return reply(res, 400, { error: "householdId required" });
      await audit(req, "seiho-kenshin-fee.certify", { householdId: b.householdId, rejected: b.rejected === true });
      return reply(res, 200, { householdId: String(b.householdId),
        certified: true, note: b.rejected === true ? "却下世帯にも検診料を認定可能" : "検診料認定" });
    }
    if (pathname === "/api/seiho/sasatsu/confirm" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId) return reply(res, 400, { error: "caseId required" });
      await audit(req, "seiho-sasatsu.confirm", { caseId: b.caseId });
      return reply(res, 200, { caseId: String(b.caseId), 査察指導確認済: true, confirmedBy: actor.id });
    }
    if (pathname === "/api/seiho/haishi-case/doc" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.caseId || !b.docDate || !b.protectionFrom || !b.protectionTo) {
        return reply(res, 400, { error: "caseId, docDate, protectionFrom, protectionTo required" });
      }
      const within = b.docDate >= b.protectionFrom && b.docDate <= b.protectionTo;
      if (!within) return reply(res, 400, { error: "廃止・停止ケースは保護受給期間内の日付でのみ作成可能" });
      await audit(req, "seiho-haishi-case.doc", { caseId: b.caseId });
      return reply(res, 200, { caseId: String(b.caseId), created: true, docDate: b.docDate,
        note: "廃止・停止ケースの保護受給期間内作成" });
    }
    if (pathname === "/api/seiho/long-hospital/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const patients = Array.isArray(b.patients) ? b.patients : [];
      if (!patients.length) return reply(res, 400, { error: "patients[]{personId,admissions[{from,to}]} required" });
      const threshold = Number(b.thresholdDays ?? 180);
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const targets = patients.map((p) => {
        const first = (p.admissions ?? []).map((a) => a.from).sort()[0];
        if (!first) return null;
        const days = Math.floor((new Date(asOf) - new Date(first)) / 86400000);
        return { personId: p.personId, firstAdmission: first, days,
          transfers: (p.admissions ?? []).length - 1, target: days >= threshold };
      }).filter((t) => t && t.target);
      await audit(req, "seiho-long-hospital.extract", { targets: targets.length });
      return reply(res, 200, { thresholdDays: threshold, count: targets.length, targets,
        note: "転院含め実際の入院日から通算して抽出" });
    }
    if (pathname === "/api/seiho/ichiji-fujo/pay" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["福祉用具", "住宅改修", "その他一時扶助"];
      const methods = ["現物給付", "現金給付", "口座振替給付"];
      if (!kinds.includes(b.kind) || !methods.includes(b.method)) {
        return reply(res, 400, { error: `kind(${kinds.join("/")}), method(${methods.join("/")}) required` });
      }
      await audit(req, "seiho-ichiji-fujo.pay", { kind: b.kind, method: b.method });
      return reply(res, 200, { kind: b.kind, method: b.method, payType: "随時払",
        amount: Number(b.amount ?? 0) });
    }

    // ── レセプト: 資格エラーリスト（実施機関別）・再審査請求一式・支援計画書・保健指導紐づけ ──
    if (pathname === "/api/seiho/receipt/shikaku-errors" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!rows.length) return reply(res, 400, { error: "rows[]{office,receiptId,issue} required" });
      const byOffice = {};
      for (const r of rows) (byOffice[r.office ?? "本所"] ??= []).push(r);
      await audit(req, "seiho-receipt.shikaku-errors", { offices: Object.keys(byOffice).length });
      return reply(res, 200, { document: "資格エラーリスト（実施機関別）",
        offices: Object.entries(byOffice).map(([office, items]) => ({ office, count: items.length, items })) });
    }
    if (pathname === "/api/seiho/saishinsa" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.receiptId) return reply(res, 400, { error: "receiptId required" });
      const list = await readJson(seihoSaishinsaFile, []);
      const item = { id: crypto.randomUUID(), receiptId: String(b.receiptId), status: "データ作成済",
        originalStatus: "非原本", at: new Date().toISOString(), by: actor.id };
      list.unshift(item);
      await writeJson(seihoSaishinsaFile, list);
      await audit(req, "seiho-saishinsa.create", { receiptId: item.receiptId });
      return reply(res, 201, item);
    }
    {
      const m = pathname.match(/^\/api\/seiho\/saishinsa\/([^/]+)\/(confirm|unconfirm|release-status|letter|result)$/);
      if (m && req.method === "POST") {
        requireRole(actor, "CLERK");
        const list = await readJson(seihoSaishinsaFile, []);
        const idx = list.findIndex((s) => s.id === m[1]);
        if (idx === -1) return reply(res, 404, { error: "再審査請求が見つかりません" });
        if (m[2] === "confirm") list[idx].status = "再審査確定";
        else if (m[2] === "unconfirm") list[idx].status = "確定解除";
        else if (m[2] === "release-status") list[idx].originalStatus = "原本";
        await writeJson(seihoSaishinsaFile, list);
        await audit(req, `seiho-saishinsa.${m[2]}`, { id: m[1] });
        if (m[2] === "letter") return reply(res, 200, { document: "送達状",
          addressee: "医療情報基盤・診療報酬審査支払機構", receiptId: list[idx].receiptId });
        if (m[2] === "result") return reply(res, 200, { document: "再審査等請求結果データ（診療報酬等請求内訳データ等）",
          receiptId: list[idx].receiptId, rows: [] });
        return reply(res, 200, list[idx]);
      }
    }
    if (pathname === "/api/seiho/shien-keikaku" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.personId || !b.program) return reply(res, 400, { error: "personId, program(個別事業) required" });
      await audit(req, "seiho-shien-keikaku", { personId: b.personId, program: b.program });
      return reply(res, 200, { document: `支援計画書（${b.program}）`, personId: String(b.personId), byProgram: true });
    }
    if (pathname === "/api/seiho/hoken-shidou/link" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.shidouId || !b.personId) return reply(res, 400, { error: "shidouId, personId required" });
      await audit(req, "seiho-hoken-shidou.link", { shidouId: b.shidouId, personId: b.personId });
      return reply(res, 200, { shidouId: String(b.shidouId), personId: String(b.personId),
        linked: true, mode: "手動紐づけ", note: "加入者番号未付与の保健指導情報を被保護者情報から手動で紐づけ" });
    }

    // ════ 医療扶助のオンライン資格確認（医療保険者等向け中間サーバー／医療情報基盤・診療報酬審査支払機構所管）════
    const iryoShikakuFile = join(dataRoot, "seiho-iryo-shikaku.json");
    // 被保護者（資格）情報の登録／変更／削除IFファイルを作成（0211947/0211954 等）
    if (pathname === "/api/seiho/iryo-shikaku/if" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["登録", "変更", "削除"];
      if (!kinds.includes(b.種別)) return reply(res, 400, { error: `種別 は ${kinds.join("/")} のいずれかが必須` });
      const records = Array.isArray(b.records) ? b.records : [];
      const list = await readJson(iryoShikakuFile, []);
      const ifFile = { id: crypto.randomUUID(), 区分: "資格IF", 種別: b.種別, 件数: records.length, records,
        宛先: "医療保険者等向け中間サーバー", 状態: "作成済", 結果: null, createdAt: new Date().toISOString(), by: actor.id };
      list.unshift(ifFile);
      await writeJson(iryoShikakuFile, list);
      await audit(req, "seiho-iryo-shikaku.if", { 種別: b.種別, 件数: records.length });
      const { records: _r, ...meta } = ifFile;
      return reply(res, 201, meta);
    }
    // 登録結果の確認（照会）・被保険者枝番の取得、個人番号誤入力／資格重複チェック結果一覧の取得（0212008/0212009）
    if (pathname === "/api/seiho/iryo-shikaku/result" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const list = await readJson(iryoShikakuFile, []);
      const target = b.ifId ? list.find((x) => x.id === b.ifId) : null;
      if (b.ifId && !target) return reply(res, 404, { error: "資格IFが見つかりません" });
      // 中間サーバーからの結果（被保険者枝番、誤入力/重複チェック結果一覧）を取り込み確認
      const 結果 = {
        枝番一覧: Array.isArray(b.枝番一覧) ? b.枝番一覧 : [], // {宛名番号,被保険者枝番}
        個人番号誤入力チェック: Array.isArray(b.個人番号誤入力) ? b.個人番号誤入力 : [],
        資格重複チェック: Array.isArray(b.資格重複) ? b.資格重複 : [],
        取得日時: new Date().toISOString(),
      };
      if (target) { target.状態 = "結果確認済"; target.結果 = 結果; await writeJson(iryoShikakuFile, list); }
      await audit(req, "seiho-iryo-shikaku.result", { 枝番件数: 結果.枝番一覧.length });
      return reply(res, 200, { ...結果, 被保険者枝番取得件数: 結果.枝番一覧.length });
    }
    // J-LIS突合情報・資格確認情報・マイナンバーカード状態の取込と差分確認（0211896/0212028 等）
    if (pathname === "/api/seiho/iryo-shikaku/import" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const types = ["J-LIS突合", "資格確認", "カード状態"];
      const 種別 = types.includes(b.種別) ? b.種別 : "資格確認";
      const records = Array.isArray(b.records) ? b.records : [];
      // 既存情報との差分確認（宛名番号単位で値が異なるものを差分として返す）
      const 既存 = b.既存 && typeof b.既存 === "object" ? b.既存 : {};
      const 差分 = records.filter((r) => r.宛名番号 != null
        && JSON.stringify(既存[String(r.宛名番号)] ?? null) !== JSON.stringify(r.値 ?? r));
      await audit(req, "seiho-iryo-shikaku.import", { 種別, 件数: records.length, 差分: 差分.length });
      return reply(res, 200, { 種別, 取込件数: records.length, 差分件数: 差分.length, 差分 });
    }
    // 被保護者の健診情報を特定健診等データ収集システムへ登録／IF作成・取込（0212015/0212016/0212017/0211572）
    if (pathname === "/api/seiho/kenshin-joho/if" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["登録", "修正", "削除", "取込", "引継"];
      const mode = modes.includes(b.mode) ? b.mode : "登録";
      const records = Array.isArray(b.records) ? b.records : [];
      const errors = records.filter((r) => r.エラー === true || (r.値 == null && r.必須));
      await audit(req, "seiho-kenshin-joho.if", { mode, 件数: records.length, エラー: errors.length });
      return reply(res, 200, { mode, 宛先: "特定健診等データ収集システム", 件数: records.length,
        エラー件数: errors.length, エラー: errors,
        note: mode === "取込" ? "提供IFを取り込みエラー状況等を管理" : mode === "引継" ? "他福祉事務所の引継ぎ結果ファイルを管理" : "健診情報の登録/修正/削除IFファイルを作成" });
    }

  return false;
}
