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

// ── 児童手当 支給額算定ロジック（標準仕様書 令和2026年版／2024年10月改正準拠）──
//   支給月額: 第3子以降=30,000円 / 3歳未満(第1・2子)=15,000円 / 3歳以上(第1・2子)=10,000円
//   多子カウント: 18歳到達後最初の3/31を経過〜22歳到達後最初の3/31までの兄姉等も算定対象に含める
//   支給対象: 18歳到達後最初の3/31まで（高校生年代まで）。所得制限は2024年10月改正で撤廃。
function fiscalYearEndAfterAge(birthDate, age) {
  const b = new Date(birthDate);
  const reachYear = b.getFullYear() + age;
  // 到達日（age歳の誕生日）以後、最初の3月31日が属する年
  const yearEnd = b.getMonth() <= 2 ? reachYear : reachYear + 1; // 1〜3月生まれは同年、4〜12月生まれは翌年
  return new Date(yearEnd, 2, 31, 23, 59, 59, 999);
}
function computeChildAllowance(children, asOf = new Date()) {
  const now = asOf instanceof Date ? asOf : new Date(asOf);
  // 算定対象（22歳年度末まで）を年長順（=第1子から）に並べる
  const counted = (children || [])
    .filter((c) => c && c.birthDate && now <= fiscalYearEndAfterAge(c.birthDate, 22))
    .map((c) => ({ ...c, _b: new Date(c.birthDate) }))
    .sort((a, b) => a._b - b._b);
  const lines = [];
  let total = 0;
  counted.forEach((c, idx) => {
    const position = idx + 1; // 第n子
    const payTarget = now <= fiscalYearEndAfterAge(c.birthDate, 18); // 高校生年代まで支給
    if (!payTarget) {
      lines.push({ id: c.id ?? null, fullName: c.fullName ?? "", position, amount: 0, note: "算定対象のみ（支給対象外）" });
      return;
    }
    const thirdBirthday = new Date(c._b.getFullYear() + 3, c._b.getMonth(), c._b.getDate());
    const under3 = now < thirdBirthday;
    const amount = position >= 3 ? 30000 : under3 ? 15000 : 10000;
    total += amount;
    lines.push({ id: c.id ?? null, fullName: c.fullName ?? "", position, under3, amount });
  });
  return { totalMonthly: total, countedChildren: counted.length, payTargetChildren: lines.filter((l) => l.amount > 0).length, children: lines };
}

export async function handle(ctx) {
  const { req, res, pathname, searchParams, actor, send, sendRaw, parseBody, readJson, writeJson, audit,
    maskRecord, restrictionApplies, validateRecord, requireRole, hasRole, dataRoot, restrictionsFile, crypto,
    dbSelect, dbSelectOne, dbInsert, dbUpdate, dbDelete } = ctx;
  const reply = (...a) => { send(...a); return true; };
  function maskRecipient(actor, restrictions, recipient) {
    const applied = restrictions.filter((r) => restrictionApplies(r, { id: recipient.id, domain: "recipient" }));
    if (!applied.length || hasRole(actor, "SUPPORT_ACCESS")) return recipient;
    return {
      id: recipient.id,
      status: recipient.status,
      certificationStatus: recipient.certificationStatus,
      restricted: true,
      restrictionCategories: [...new Set(applied.map((r) => r.category))],
      fullName: "（支援措置対象）",
      address: null,
      bankAccountMasked: null,
      myNumberMasked: null,
      createdAt: recipient.createdAt,
      updatedAt: recipient.updatedAt,
    };
  }
  const recipientsFile = join(dataRoot, "recipients.json");
  const childrenFile = join(dataRoot, "children.json");
  const applicationsFile = join(dataRoot, "applications.json");
  const paymentsFile = join(dataRoot, "payments.json");


    // ── 受給者 (Recipients) ────────────────────────────────────────────────

    if (pathname === "/api/recipients" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, recipients.map((r) => maskRecipient(actor, restrictions, r)));
    }

    if (pathname === "/api/recipients" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.fullName) { const e = new Error("fullName is required"); e.statusCode = 400; throw e; }
      const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
      const item = {
        id: crypto.randomUUID(),
        recipientNumber: "CA-" + Date.now().toString(36).toUpperCase(),
        status: "active",
        certificationStatus: body.certificationStatus ?? "pending",
        fullName: String(body.fullName),
        address: body.address ?? null,
        bankAccountMasked: body.bankAccountLast4 ? "****" + String(body.bankAccountLast4).slice(-4) : null,
        myNumberMasked: body.myNumberLast4 ? "*******" + String(body.myNumberLast4).slice(-4) : null,
        certificationDate: body.certificationDate ?? null,
        notes: body.notes ?? "",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        createdBy: actor.id,
      };
      const dbInserted = await dbInsert("child_allowance.recipient", item); if (dbInserted === null) { recipients.unshift(item); await writeJson(recipientsFile, recipients); }
      await audit(req, "recipient.create", { id: item.id });
      return reply(res, 201, item);
    }

    const recipientMatch = pathname.match(/^\/api\/recipients\/([^/]+)$/);
    if (recipientMatch) {
      requireRole(actor, "CLERK");
      const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      const index = recipients.findIndex((r) => r.id === recipientMatch[1]);
      if (index === -1) return reply(res, 404, { error: "recipient not found" });

      if (req.method === "GET") return reply(res, 200, maskRecipient(actor, restrictions, recipients[index]));

      if (req.method === "PUT") {
        const body = await parseBody(req);
        const item = {
          ...recipients[index],
          fullName: body.fullName ?? recipients[index].fullName,
          address: body.address ?? recipients[index].address,
          status: body.status ?? recipients[index].status,
          certificationStatus: body.certificationStatus ?? recipients[index].certificationStatus,
          certificationDate: body.certificationDate ?? recipients[index].certificationDate,
          notes: body.notes ?? recipients[index].notes,
          updatedAt: new Date().toISOString(),
          updatedBy: actor.id,
        };
        const upd = await dbUpdate("child_allowance.recipient", item.id, item);
        if (upd === null) { recipients[index] = item; await writeJson(recipientsFile, recipients); }
        await audit(req, "recipient.update", { id: item.id });
        return reply(res, 200, item);
      }
    }

    // ── 児童 (Children) ───────────────────────────────────────────────────

    // ── 児童手当 支給額の自動算定（標準仕様書準拠）──
    const allowanceMatch = pathname.match(/^\/api\/recipients\/([^/]+)\/allowance$/);
    if (allowanceMatch && req.method === "GET") {
      requireRole(actor, "CLERK");
      const recipientId = allowanceMatch[1];
      const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
      if (!recipients.find((r) => r.id === recipientId)) return reply(res, 404, { error: "recipient not found" });
      const all = await readJson(childrenFile, []);
      const mine = all.filter((c) => c.recipientId === recipientId);
      const asOf = searchParams.get("asOf");
      const result = computeChildAllowance(mine, asOf ? new Date(asOf) : new Date());
      return reply(res, 200, { recipientId, asOf: asOf ?? new Date().toISOString().slice(0, 10), ...result });
    }

    const childrenMatch = pathname.match(/^\/api\/recipients\/([^/]+)\/children$/);
    if (childrenMatch) {
      requireRole(actor, "CLERK");
      const recipientId = childrenMatch[1];
      const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
      if (!recipients.find((r) => r.id === recipientId)) return reply(res, 404, { error: "recipient not found" });
      const children = await readJson(childrenFile, []);

      if (req.method === "GET") return reply(res, 200, children.filter((c) => c.recipientId === recipientId));

      if (req.method === "POST") {
        const body = await parseBody(req);
        if (!body.fullName || !body.birthDate) {
          const e = new Error("fullName and birthDate are required"); e.statusCode = 400; throw e;
        }
        const item = {
          id: crypto.randomUUID(),
          recipientId,
          fullName: String(body.fullName),
          birthDate: String(body.birthDate),
          certificationCategory: body.certificationCategory ?? 1,
          disabilityGrade: body.disabilityGrade ?? null,
          createdAt: new Date().toISOString(),
          createdBy: actor.id,
        };
        children.unshift(item);
        await writeJson(childrenFile, children);
        await audit(req, "child.create", { id: item.id, recipientId });
        return reply(res, 201, item);
      }
    }

    // ── 申請 (Applications / 認定請求) ────────────────────────────────────

    const APPLICATION_TYPES = ["new_certification", "renewal", "amount_change", "child_addition", "status_change", "termination"];

    if (pathname === "/api/applications" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const applications = (await dbSelect("child_allowance.application")) ?? await readJson(applicationsFile, []);
      const status = searchParams.get("status");
      const recipientId = searchParams.get("recipientId");
      let result = applications;
      if (status) result = result.filter((a) => a.status === status);
      if (recipientId) result = result.filter((a) => a.recipientId === recipientId);
      return reply(res, 200, result);
    }

    if (pathname === "/api/applications" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.recipientId || !body.applicationType) {
        const e = new Error("recipientId and applicationType are required"); e.statusCode = 400; throw e;
      }
      if (!APPLICATION_TYPES.includes(body.applicationType)) {
        const e = new Error(`applicationType must be one of: ${APPLICATION_TYPES.join(", ")}`); e.statusCode = 400; throw e;
      }
      const applications = (await dbSelect("child_allowance.application")) ?? await readJson(applicationsFile, []);
      const item = {
        id: crypto.randomUUID(),
        recipientId: String(body.recipientId),
        applicationType: body.applicationType,
        status: "submitted",
        submittedAt: new Date().toISOString(),
        submittedBy: actor.id,
        requestedAmount: body.requestedAmount ?? null,
        childIds: Array.isArray(body.childIds) ? body.childIds : [],
        notes: body.notes ?? "",
        reviewedAt: null,
        reviewedBy: null,
        decision: null,
        decisionNotes: "",
        createdAt: new Date().toISOString(),
      };
      const dbInserted = await dbInsert("child_allowance.application", item); if (dbInserted === null) { applications.unshift(item); await writeJson(applicationsFile, applications); }
      await audit(req, "application.submit", { id: item.id, applicationType: item.applicationType, recipientId: item.recipientId });
      return reply(res, 201, item);
    }

    const applicationMatch = pathname.match(/^\/api\/applications\/([^/]+)$/);
    if (applicationMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = applicationMatch[1];
      const app = (await dbSelectOne("child_allowance.application", id))
        ?? (await readJson(applicationsFile, [])).find((a) => a.id === id);
      if (!app) return reply(res, 404, { error: "application not found" });
      if (req.method === "GET") return reply(res, 200, app);

      if (app.status !== "submitted") return reply(res, 409, { error: "can only edit applications in submitted status" });
      const body = await parseBody(req);
      const editable = ["applicationType", "requestedAmount", "childIds", "notes"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      if (patch.applicationType && !APPLICATION_TYPES.includes(patch.applicationType)) {
        return reply(res, 400, { error: `applicationType must be one of: ${APPLICATION_TYPES.join(", ")}` });
      }
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("child_allowance.application", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(applicationsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(applicationsFile, items);
        result = items[idx];
      }
      await audit(req, "application.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

    const applicationReviewMatch = pathname.match(/^\/api\/applications\/([^/]+)\/review$/);
    if (applicationReviewMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const action = body.action === "reject" ? "reject" : "approve";
      const applications = (await dbSelect("child_allowance.application")) ?? await readJson(applicationsFile, []);
      const index = applications.findIndex((a) => a.id === applicationReviewMatch[1]);
      if (index === -1) return reply(res, 404, { error: "application not found" });
      const app = applications[index];
      if (app.status !== "submitted" && app.status !== "reviewing") {
        return reply(res, 409, { error: "application is not in a reviewable state" });
      }
      const updated = {
        ...app,
        status: action === "approve" ? "approved" : "rejected",
        reviewedAt: new Date().toISOString(),
        reviewedBy: actor.id,
        decision: action,
        decisionNotes: body.notes ?? "",
      };
      if (action === "approve") {
        const recipients = (await dbSelect("child_allowance.recipient")) ?? await readJson(recipientsFile, []);
        const rIndex = recipients.findIndex((r) => r.id === app.recipientId);
        if (rIndex !== -1) {
          recipients[rIndex] = { ...recipients[rIndex], certificationStatus: "certified", updatedAt: new Date().toISOString(), updatedBy: actor.id };
          await writeJson(recipientsFile, recipients);
        }
      }
      applications[index] = updated;
      await writeJson(applicationsFile, applications);
      await audit(req, `application.${action}`, { id: updated.id, recipientId: updated.recipientId });
      return reply(res, 200, updated);
    }

    // ── 支払 (Payments) ───────────────────────────────────────────────────

    if (pathname === "/api/payments" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const payments = (await dbSelect("child_allowance.payment")) ?? await readJson(paymentsFile, []);
      const recipientId = searchParams.get("recipientId");
      const month = searchParams.get("month");
      let result = payments;
      if (recipientId) result = result.filter((p) => p.recipientId === recipientId);
      if (month) result = result.filter((p) => p.paymentMonth === month);
      return reply(res, 200, result);
    }

    if (pathname === "/api/payments" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.recipientId || !body.paymentMonth || body.amount == null) {
        const e = new Error("recipientId, paymentMonth and amount are required"); e.statusCode = 400; throw e;
      }
      const payments = (await dbSelect("child_allowance.payment")) ?? await readJson(paymentsFile, []);
      const item = {
        id: crypto.randomUUID(),
        recipientId: String(body.recipientId),
        paymentMonth: String(body.paymentMonth),
        amount: Number(body.amount),
        status: "scheduled",
        bankTransferRef: body.bankTransferRef ?? null,
        notes: body.notes ?? "",
        scheduledAt: new Date().toISOString(),
        scheduledBy: actor.id,
        paidAt: null,
        createdAt: new Date().toISOString(),
      };
      const dbInserted = await dbInsert("child_allowance.payment", item); if (dbInserted === null) { payments.unshift(item); await writeJson(paymentsFile, payments); }
      await audit(req, "payment.create", { id: item.id, recipientId: item.recipientId, amount: item.amount });
      return reply(res, 201, item);
    }

    const paymentStatusMatch = pathname.match(/^\/api\/payments\/([^/]+)\/status$/);
    if (paymentStatusMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      const validStatuses = ["scheduled", "paid", "suspended", "failed"];
      if (!validStatuses.includes(body.status)) {
        const e = new Error(`status must be one of: ${validStatuses.join(", ")}`); e.statusCode = 400; throw e;
      }
      const payments = (await dbSelect("child_allowance.payment")) ?? await readJson(paymentsFile, []);
      const index = payments.findIndex((p) => p.id === paymentStatusMatch[1]);
      if (index === -1) return reply(res, 404, { error: "payment not found" });
      const item = {
        ...payments[index],
        status: body.status,
        paidAt: body.status === "paid" ? new Date().toISOString() : payments[index].paidAt,
        updatedAt: new Date().toISOString(),
        updatedBy: actor.id,
      };
      const upd = await dbUpdate("child_allowance.payment", item.id, item);
      if (upd === null) { payments[index] = item; await writeJson(paymentsFile, payments); }
      await audit(req, `payment.${body.status}`, { id: item.id, recipientId: item.recipientId });
      return reply(res, 200, item);
    }


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


    // ── 児童手当 支給額の試算（児童の生年月日リストから直接算定。受給者登録不要）──
    if (pathname === "/api/allowance/calc" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const birthDates = String(body.birthDates ?? "").split(/[,\n]/).map((s) => s.trim()).filter(Boolean);
      const children = birthDates.map((bd) => ({ birthDate: bd }));
      const result = computeChildAllowance(children, body.asOf ? new Date(body.asOf) : new Date());
      return reply(res, 200, result);
    }

    // ── 15日特例の自動判定（児童手当法: 出生・転入等の事由発生日の翌日から15日以内の申請は、
    //     事由発生月の翌月分から支給。それ以外は申請月の翌月分から）──
    if (pathname === "/api/jidou/special15" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.eventDate || !b.applicationDate) return reply(res, 400, { error: "eventDate, applicationDate required" });
      const event = new Date(b.eventDate);
      const app = new Date(b.applicationDate);
      if (Number.isNaN(event.getTime()) || Number.isNaN(app.getTime()) || app < event) {
        return reply(res, 400, { error: "日付が不正です（申請日は事由発生日以降）" });
      }
      const DAY = 24 * 60 * 60 * 1000;
      const daysAfter = Math.floor((app - event) / DAY); // 翌日から起算した経過日数
      const special15Applied = daysAfter >= 1 ? daysAfter <= 15 : true; // 当日申請も特例範囲
      const baseDate = special15Applied ? event : app;
      const start = new Date(baseDate.getFullYear(), baseDate.getMonth() + 1, 1);
      const paymentStartMonth = start.getFullYear() + "-" + String(start.getMonth() + 1).padStart(2, "0");
      await audit(req, "jidou.special15", { special15Applied, paymentStartMonth });
      return reply(res, 200, { special15Applied, daysAfter, paymentStartMonth,
        rule: special15Applied ? "15日特例該当（事由発生月の翌月分から支給）" : "通常（申請月の翌月分から支給）" });
    }

    // ════ 別業務連携・住基/生保提供・再転入・EUC・現況届・資格消滅・支払訂正/差止（標準仕様書）════

    const jidouReentryFile = join(dataRoot, "jidou-reentry.json");
    const jidouConfigFile = join(dataRoot, "jidou-config.json");
    const jidouSashitomeFile = join(dataRoot, "jidou-sashitome.json");

    // ── 別業務システムの情報閲覧・画面遷移（児童扶養手当等の児童福祉業務）──
    if (pathname === "/api/jidou/related-systems" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, { systems: [
        { name: "児童扶養手当", url: "https://child-rearing-demo.highdefinitionaudiodriver.workers.dev", scope: "児童福祉業務" },
        { name: "子ども・子育て支援", url: "https://kosodate-shien-demo.highdefinitionaudiodriver.workers.dev", scope: "児童福祉業務" },
      ], note: "別業務の情報閲覧・画面遷移を容易にするリンク集（UIのナビから遷移可能）" });
    }

    // ── 住所の住民基本台帳連携取得 ──
    if (pathname === "/api/jidou/address/from-juki" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.jukiAddress) return reply(res, 400, { error: "atenaId, jukiAddress required" });
      await audit(req, "jidou-address.from-juki", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), address: String(b.jukiAddress),
        source: "住民基本台帳", note: "住所は住民基本台帳からの連携値を正とする" });
    }

    // ── 情報提供（住民基本台帳へ児童手当情報・生活保護へ受給資格/支給情報）──
    if (pathname === "/api/jidou/provide" && req.method === "GET") {
      requireRole(actor, "ADMIN");
      const DATASETS = {
        "juki": { name: "児童手当情報", target: "住民基本台帳（住民記録システム）" },
        "seiho": { name: "受給資格情報・支給情報", target: "生活保護システム" },
      };
      const dataset = searchParams.get("dataset") ?? "";
      const def = DATASETS[dataset];
      if (!def) return reply(res, 400, { error: `dataset は ${Object.keys(DATASETS).join("/")} のいずれか` });
      const recipients = await readJson(recipientsFile, []);
      const rows = recipients.map((r) => ({ atenaId: r.atenaId ?? r.id, fullName: r.fullName ?? "",
        ...(dataset === "seiho" ? { paymentStatus: r.status ?? "支給中" } : {}) }));
      await audit(req, "jidou.provide", { dataset, target: def.target, count: rows.length });
      return reply(res, 200, { dataset, name: def.name, target: def.target, count: rows.length, rows });
    }

    // ── 再転入者の転出時点情報の保存・確認・引き続き利用（過誤払い残額の同一受給者引継含む）──
    if (pathname === "/api/jidou/reentry" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || !b.snapshot) return reply(res, 400, { error: "atenaId, snapshot required" });
      const list = await readJson(jidouReentryFile, []);
      list.unshift({ id: crypto.randomUUID(), atenaId: String(b.atenaId), snapshot: b.snapshot,
        outstandingOverpayment: Number(b.outstandingOverpayment ?? 0),
        movedOutAt: b.movedOutAt ?? new Date().toISOString().slice(0, 10), at: new Date().toISOString(), by: actor.id });
      await writeJson(jidouReentryFile, list);
      await audit(req, "jidou-reentry.save", { atenaId: b.atenaId });
      return reply(res, 201, { saved: true });
    }
    {
      const reentryMatch = pathname.match(/^\/api\/jidou\/reentry\/([^/]+)$/);
      if (reentryMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const list = await readJson(jidouReentryFile, []);
        const hits = list.filter((r) => r.atenaId === reentryMatch[1]);
        if (!hits.length) return reply(res, 404, { error: "転出時点の情報がありません" });
        await audit(req, "jidou-reentry.recall", { atenaId: reentryMatch[1] });
        return reply(res, 200, { atenaId: reentryMatch[1], samePerson: true, reusable: true,
          outstandingOverpayment: hits[0].outstandingOverpayment ?? 0,
          note: "同一受給者として取扱い、過誤払い残額も引き継ぐ", snapshots: hits });
      }
    }

    // ── 保存期間の任意指定 ──
    if (pathname === "/api/jidou/config/retention" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.docKind || !Number(b.years)) return reply(res, 400, { error: "docKind, years required" });
      const config = await readJson(jidouConfigFile, {});
      config.retention = { ...(config.retention ?? {}), [String(b.docKind)]: Number(b.years) };
      await writeJson(jidouConfigFile, config);
      await audit(req, "jidou-config.retention", { docKind: b.docKind, years: Number(b.years) });
      return reply(res, 200, config.retention);
    }

    // ── EUC（共通機能標準仕様書準拠の演算子・抽出・分析・加工・出力）──
    if (pathname === "/api/jidou/euc/query" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rows = Array.isArray(b.rows) ? b.rows : [];
      const conditions = Array.isArray(b.conditions) ? b.conditions : [];
      if (!rows.length) return reply(res, 400, { error: "rows[] required" });
      const OPS = {
        "=": (a, v) => String(a) === String(v), "≠": (a, v) => String(a) !== String(v),
        ">": (a, v) => Number(a) > Number(v), "<": (a, v) => Number(a) < Number(v),
        "≧": (a, v) => Number(a) >= Number(v), "≦": (a, v) => Number(a) <= Number(v),
        "部分一致": (a, v) => String(a).includes(String(v)),
        "前方一致": (a, v) => String(a).startsWith(String(v)),
        "後方一致": (a, v) => String(a).endsWith(String(v)),
      };
      const logic = b.logic === "or" ? "or" : "and";
      const test = (row) => conditions.map((c) => (OPS[c.op] ?? (() => false))(row[c.field], c.value));
      let hits = conditions.length ? rows.filter((r) => (logic === "and" ? test(r).every(Boolean) : test(r).some(Boolean))) : rows;
      if (Array.isArray(b.fields) && b.fields.length) hits = hits.map((r) => Object.fromEntries(b.fields.map((f) => [f, r[f]])));
      let aggregate = null;
      if (b.groupBy) {
        const groups = {};
        for (const r of hits) groups[String(r[b.groupBy])] = (groups[String(r[b.groupBy])] ?? 0) + 1;
        aggregate = Object.entries(groups).map(([key, count]) => ({ [b.groupBy]: key, count }));
      }
      await audit(req, "jidou-euc.query", { hits: hits.length });
      return reply(res, 200, { count: hits.length, results: hits, aggregate });
    }

    // ── 帳票PDFの山分け（管区・郵便局単位のファイル分割）──
    if (pathname === "/api/jidou/print/split" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const keys = { "管区": "kanku", "郵便局": "postOffice" };
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!keys[b.splitBy] || !rows.length) return reply(res, 400, { error: "splitBy(管区/郵便局), rows[]{recipientId,kanku,postOffice} required" });
      const groups = {};
      for (const r of rows) (groups[String(r[keys[b.splitBy]] ?? "不明")] ??= []).push(r);
      const files = Object.entries(groups).map(([key, items]) => ({ file: `現況届_${b.splitBy}_${key}.pdf`, count: items.length }));
      await audit(req, "jidou-print.split", { splitBy: b.splitBy, files: files.length });
      return reply(res, 200, { splitBy: b.splitBy, files });
    }

    // ── 新規認定請求の取消（履歴を残す）──
    if (pathname === "/api/jidou/claims/cancel" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.claimId) return reply(res, 400, { error: "claimId required" });
      const applications = await readJson(applicationsFile, []);
      const idx = applications.findIndex((a) => a.id === String(b.claimId));
      if (idx === -1) return reply(res, 404, { error: "claim not found" });
      const history = applications[idx].history ?? [];
      history.push({ action: "取消", reason: b.reason ?? "", at: new Date().toISOString(), by: actor.id,
        before: { status: applications[idx].status } });
      applications[idx] = { ...applications[idx], status: "cancelled", history };
      await writeJson(applicationsFile, applications);
      await audit(req, "jidou-claims.cancel", { claimId: b.claimId });
      return reply(res, 200, { claimId: String(b.claimId), status: "cancelled", historyKept: true, history });
    }

    // ── 現況届の個別出力（3様式・消込用バーコード印字）──
    if (pathname === "/api/jidou/genkyo/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const docTypes = ["児童手当・特例給付　現況届", "児童手当　現況届", "児童手当　現況届（施設等受給者用）"];
      if (!b.recipientId || !docTypes.includes(b.docType)) {
        return reply(res, 400, { error: "recipientId, docType required", docTypes });
      }
      const fy = Number(b.fiscalYear ?? new Date().getFullYear());
      await audit(req, "jidou-genkyo.print", { docType: b.docType, recipientId: b.recipientId });
      return reply(res, 200, { docType: b.docType, recipientId: String(b.recipientId), fiscalYear: fy,
        barcode: `JIDG-${fy}-${b.recipientId}`, barcodeNote: "消込作業を容易にする消込用バーコードを印字",
        output: "個別出力" });
    }

    // ── 現況審査: 所得上限超過者（受給者・配偶者）の受給資格消滅処理 ──
    if (pathname === "/api/jidou/shikaku/shometsu" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const targets = Array.isArray(b.targets) ? b.targets : [];
      if (!targets.length) return reply(res, 400, { error: "targets[]{recipientId,income,spouseIncome,limit} required" });
      const results = targets.map((t) => {
        const over = Number(t.income ?? 0) > Number(t.limit ?? Infinity) || Number(t.spouseIncome ?? 0) > Number(t.limit ?? Infinity);
        return { recipientId: String(t.recipientId), overLimit: over, status: over ? "受給資格消滅" : "継続" };
      });
      const extinguished = results.filter((r) => r.overLimit);
      await audit(req, "jidou-shikaku.shometsu", { checked: results.length, extinguished: extinguished.length });
      return reply(res, 200, { checked: results.length, extinguished: extinguished.length, results });
    }

    // ── 審査での年金情報（被用者・非被用者区分）の確認 ──
    if (pathname === "/api/jidou/shinsa/pension-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const applicants = Array.isArray(b.applicants) ? b.applicants : [];
      if (!applicants.length) return reply(res, 400, { error: "applicants[]{atenaId,pensionKubun} required" });
      const kubuns = ["被用者", "非被用者"];
      const results = applicants.map((a) => ({ atenaId: String(a.atenaId),
        pensionKubun: kubuns.includes(a.pensionKubun) ? a.pensionKubun : "不明",
        confirmed: kubuns.includes(a.pensionKubun) }));
      await audit(req, "jidou-shinsa.pension-check", { applicants: results.length });
      return reply(res, 200, { kind: "年金情報（被用者・非被用者区分）の確認", results });
    }

    // ── 支払データの訂正（申出徴収・特別徴収・寄附）──
    if (pathname === "/api/jidou/payments/adjust" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["申出徴収", "特別徴収", "寄附"];
      if (!b.recipientId || !kinds.includes(b.kind) || !Number(b.amount)) {
        return reply(res, 400, { error: `recipientId, kind(${kinds.join("/")}), amount required` });
      }
      const gross = Number(b.grossAmount ?? 0);
      const deduction = Number(b.amount);
      await audit(req, "jidou-payments.adjust", { recipientId: b.recipientId, kind: b.kind, amount: deduction });
      return reply(res, 200, { recipientId: String(b.recipientId), kind: b.kind,
        deduction, grossAmount: gross, netAmount: Math.max(0, gross - deduction),
        note: b.kind === "寄附" ? "寄附申出による支払データ訂正" : "保育料・学校給食費等の徴収申出による支払データ訂正" });
    }

    // ── 児童手当支払の一時差止・解除 ──
    if (pathname === "/api/jidou/payments/sashitome" && 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` });
      const sashitome = await readJson(jidouSashitomeFile, {});
      sashitome[String(b.recipientId)] = { status: b.action === "差止" ? "一時差止" : "解除",
        reason: b.reason ?? "", at: new Date().toISOString(), by: actor.id };
      await writeJson(jidouSashitomeFile, sashitome);
      await audit(req, "jidou-payments.sashitome", { recipientId: b.recipientId, action: b.action });
      return reply(res, 200, { recipientId: String(b.recipientId), ...sashitome[String(b.recipientId)] });
    }

    // ── 児童手当の各種情報の管理（登録・修正・削除・照会）: 施設/不現住/世帯/特記/住民税/認定請求/額改定/支払等を種別別CRUD保持 ──
    const jidouRecordsFile = join(dataRoot, "jidou-records.json");
    if (pathname === "/api/jidou/records" && req.method === "GET") {
      requireRole(actor, "CLERK"); // 照会（種別/宛名番号フィルタ可）
      const 種別 = searchParams.get("種別"), atena = searchParams.get("宛名番号") ?? searchParams.get("受給者ID");
      const all = await readJson(jidouRecordsFile, []);
      const list = all.filter((x) => (!種別 || x.種別 === 種別) && (!atena || String(x.宛名番号 ?? x.受給者ID ?? "") === String(atena)));
      return reply(res, 200, { 件数: list.length, records: list });
    }
    if (pathname === "/api/jidou/records" && req.method === "POST") {
      requireRole(actor, "CLERK"); // 登録
      const b = await parseBody(req);
      if (!b.種別) return reply(res, 400, { error: "種別(施設/不現住/世帯/特記/住民税/認定請求/額改定/支払 等) が必要です" });
      const all = await readJson(jidouRecordsFile, []);
      const { id: _i, createdAt: _c, ...fields } = b || {};
      const item = { id: crypto.randomUUID(), ...fields, createdAt: new Date().toISOString(), by: actor.id };
      all.unshift(item);
      await writeJson(jidouRecordsFile, all);
      await audit(req, "jidou-record.create", { id: item.id, 種別: item.種別 });
      return reply(res, 201, item);
    }
    const jidouRecordItemMatch = pathname.match(/^\/api\/jidou\/records\/([^/]+)$/);
    if (jidouRecordItemMatch) {
      const all = await readJson(jidouRecordsFile, []);
      const idx = all.findIndex((x) => x.id === jidouRecordItemMatch[1]);
      if (req.method === "GET") { requireRole(actor, "CLERK"); return idx === -1 ? reply(res, 404, { error: "記録が見つかりません" }) : reply(res, 200, all[idx]); }
      if (req.method === "PUT") {
        requireRole(actor, "CLERK"); if (idx === -1) return reply(res, 404, { error: "記録が見つかりません" });
        const b = await parseBody(req); const { id: _i, createdAt: _c, ...patch } = b || {};
        all[idx] = { ...all[idx], ...patch, updatedAt: new Date().toISOString() };
        await writeJson(jidouRecordsFile, all); await audit(req, "jidou-record.update", { id: all[idx].id });
        return reply(res, 200, all[idx]);
      }
      if (req.method === "DELETE") {
        requireRole(actor, "CLERK"); if (idx === -1) return reply(res, 404, { error: "記録が見つかりません" });
        const [removed] = all.splice(idx, 1);
        await writeJson(jidouRecordsFile, all); await audit(req, "jidou-record.delete", { id: removed.id });
        return reply(res, 200, { deleted: true, id: removed.id });
      }
    }

  return false;
}