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

// ── 国民年金 保険料・免除区分の算定（標準仕様書: 定額保険料、申請所得による免除区分判定）──
//   保険料は法定（年度別定額）。免除所得基準は政令。既定は令和6年度の値。所得=前年所得。
const NENKIN_RATES = {
  monthlyByYear: { 2024: 16980, 2025: 17510 }, // 月額保険料
  additional: 400, // 付加保険料（月額）
  exemption: {
    full: (dep) => (dep + 1) * 350000 + 320000,   // 全額免除: (扶養+1)×35万+32万
    threeQuarter: (dep) => 880000 + dep * 380000,  // 4分の3免除
    half: (dep) => 1280000 + dep * 380000,         // 半額免除
    quarter: (dep) => 1680000 + dep * 380000,      // 4分の1免除
  },
};
function determineNenkinExemption(income, dependents = 0, rates = NENKIN_RATES) {
  const inc = Math.max(0, Number(income) || 0);
  const dep = Math.max(0, Number(dependents) || 0);
  if (inc <= rates.exemption.full(dep)) return { category: "全額免除", payRatio: 0 };
  if (inc <= rates.exemption.threeQuarter(dep)) return { category: "4分の3免除", payRatio: 0.25 };
  if (inc <= rates.exemption.half(dep)) return { category: "半額免除", payRatio: 0.5 };
  if (inc <= rates.exemption.quarter(dep)) return { category: "4分の1免除", payRatio: 0.75 };
  return { category: "該当なし（全額納付）", payRatio: 1 };
}
function computeNenkin({ year = 2024, income, dependents = 0, withAdditional = false }, rates = NENKIN_RATES) {
  const monthly = rates.monthlyByYear[year] ?? rates.monthlyByYear[2024];
  const ex = income === undefined || income === null ? { category: "判定なし", payRatio: 1 } : determineNenkinExemption(income, dependents, rates);
  const additional = withAdditional ? rates.additional : 0;
  const payableMonthly = Math.round(monthly * ex.payRatio) + additional;
  return { year, monthlyPremium: monthly, additional, exemption: ex.category, payRatio: ex.payRatio, payableMonthly, payableAnnual: payableMonthly * 12 };
}

// ── 学生納付特例の所得基準判定（標準仕様書 260227/260231）。学生本人所得 ≤ 128万＋扶養控除＋社会保険料控除等 ──
function computeStudentException({ studentIncome = 0, dependents = 0, socialInsurance = 0 }) {
  const inc = Math.max(0, Number(studentIncome) || 0);
  const dep = Math.max(0, Number(dependents) || 0);
  const si = Math.max(0, Number(socialInsurance) || 0);
  const threshold = 1280000 + dep * 380000 + si; // 令和3年度〜
  return { eligible: inc <= threshold, studentIncome: inc, threshold,
    note: "学生納付特例: 学生本人の前年所得が 128万円＋扶養控除(38万×人)＋社会保険料控除等 以下で該当" };
}
// ── 免除等の終了期間の自動算定（標準仕様書 260124/260460/260498）──
//   終了期間＝資格喪失日の属する月の前月。ただし資格取得日と喪失日が同月の場合は当月。
function computeNenkinEndPeriod({ acquireDate = null, loseDate = null }) {
  if (!loseDate) return { error: "loseDate（資格喪失日）が必要です" };
  const lose = new Date(loseDate);
  const ym = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`;
  const loseYM = ym(lose);
  const acqYM = acquireDate ? ym(new Date(acquireDate)) : null;
  let end;
  if (acqYM && acqYM === loseYM) {
    end = loseYM; // 同月取得喪失 → 当月
  } else {
    const prev = new Date(lose.getFullYear(), lose.getMonth() - 1, 1); // 喪失月の前月
    end = ym(prev);
  }
  return { acquireDate: acquireDate ?? null, loseDate, endPeriod: end,
    sameMonth: acqYM != null && acqYM === loseYM,
    note: "終了期間＝資格喪失日の属する月の前月（取得日と喪失日が同月の場合は当月）" };
}

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; };
  const insuredFile = join(dataRoot, "insured.json");
  const qualificationsFile = join(dataRoot, "qualifications.json");
  const exemptionsFile = join(dataRoot, "exemptions.json");


    // ── 被保険者 (Insured) ────────────────────────────────────────────────

    const ENROLLMENT_TYPES = ["1", "2", "3"];

    if (pathname === "/api/insured" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const insured = (await dbSelect("nenkin.insured_person")) ?? await readJson(insuredFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      const status = searchParams.get("status");
      let result = insured;
      if (status) result = result.filter((p) => p.status === status);
      return reply(res, 200, result.map((p) => {
        const applied = restrictions.filter((r) => restrictionApplies(r, { id: p.id, domain: "insured" }));
        if (!applied.length || hasRole(actor, "SUPPORT_ACCESS")) return p;
        return { id: p.id, status: p.status, enrollmentType: p.enrollmentType, restricted: true, fullName: "（支援措置対象）", address: null, myNumberMasked: null, createdAt: p.createdAt };
      }));
    }

    if (pathname === "/api/insured" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.fullName || !body.birthDate) { const e = new Error("fullName and birthDate are required"); e.statusCode = 400; throw e; }
      if (!ENROLLMENT_TYPES.includes(body.enrollmentType)) { const e = new Error("enrollmentType must be '1', '2', or '3'"); e.statusCode = 400; throw e; }
      const insured = (await dbSelect("nenkin.insured_person")) ?? await readJson(insuredFile, []);
      const item = {
        id: crypto.randomUUID(),
        insuredNumber: "NP-" + Date.now().toString(36).toUpperCase(),
        myNumberMasked: body.myNumberLast4 ? "*******" + String(body.myNumberLast4).slice(-4) : null,
        fullName: String(body.fullName),
        birthDate: String(body.birthDate),
        address: body.address ?? null,
        enrollmentType: body.enrollmentType,
        status: "enrolled",
        enrollmentDate: body.enrollmentDate ?? new Date().toISOString().slice(0, 10),
        notes: body.notes ?? "",
        createdAt: new Date().toISOString(),
        updatedAt: new Date().toISOString(),
        createdBy: actor.id,
      };
      const dbInserted = await dbInsert("nenkin.insured_person", item); if (dbInserted === null) { insured.unshift(item); await writeJson(insuredFile, insured); }
      await audit(req, "insured.create", { id: item.id, enrollmentType: item.enrollmentType });
      return reply(res, 201, item);
    }

    const insuredMatch = pathname.match(/^\/api\/insured\/([^/]+)$/);
    if (insuredMatch) {
      requireRole(actor, "CLERK");
      const insured = await readJson(insuredFile, []);
      const index = insured.findIndex((p) => p.id === insuredMatch[1]);
      if (index === -1) return reply(res, 404, { error: "insured person not found" });

      if (req.method === "GET") return reply(res, 200, insured[index]);

      if (req.method === "PUT") {
        const body = await parseBody(req);
        const item = {
          ...insured[index],
          fullName: body.fullName ?? insured[index].fullName,
          address: body.address ?? insured[index].address,
          status: body.status ?? insured[index].status,
          enrollmentType: ENROLLMENT_TYPES.includes(body.enrollmentType) ? body.enrollmentType : insured[index].enrollmentType,
          notes: body.notes ?? insured[index].notes,
          updatedAt: new Date().toISOString(),
          updatedBy: actor.id,
        };
        const upd = await dbUpdate("nenkin.insured_person", item.id, item);
        if (upd === null) { insured[index] = item; await writeJson(insuredFile, insured); }
        await audit(req, "insured.update", { id: item.id });
        return reply(res, 200, item);
      }
    }

    // ── 資格管理 (Qualification History) ─────────────────────────────────

    const qualificationsMatch = pathname.match(/^\/api\/insured\/([^/]+)\/qualifications$/);
    if (qualificationsMatch) {
      requireRole(actor, "CLERK");
      const insuredId = qualificationsMatch[1];
      const insured = await readJson(insuredFile, []);
      if (!insured.find((p) => p.id === insuredId)) return reply(res, 404, { error: "insured person not found" });
      const qualifications = (await dbSelect("nenkin.qualification")) ?? await readJson(qualificationsFile, []);

      if (req.method === "GET") return reply(res, 200, qualifications.filter((q) => q.insuredId === insuredId));

      if (req.method === "POST") {
        const body = await parseBody(req);
        const validTypes = ["acquire", "lose", "change"];
        if (!body.eventType || !validTypes.includes(body.eventType)) { const e = new Error(`eventType must be one of: ${validTypes.join(", ")}`); e.statusCode = 400; throw e; }
        if (!body.effectiveDate) { const e = new Error("effectiveDate is required"); e.statusCode = 400; throw e; }
        const item = {
          id: crypto.randomUUID(),
          insuredId,
          eventType: body.eventType,
          reasonCode: body.reasonCode ?? null,
          effectiveDate: String(body.effectiveDate),
          enrollmentType: ENROLLMENT_TYPES.includes(body.enrollmentType) ? body.enrollmentType : null,
          notes: body.notes ?? "",
          createdAt: new Date().toISOString(),
          createdBy: actor.id,
        };
        if (body.eventType === "lose") {
          const insured = await readJson(insuredFile, []);
          const idx = insured.findIndex((p) => p.id === insuredId);
          if (idx !== -1) { insured[idx] = { ...insured[idx], status: "withdrawn", updatedAt: new Date().toISOString(), updatedBy: actor.id }; await writeJson(insuredFile, insured); }
        }
        const dbInserted = await dbInsert("nenkin.qualification", item); if (dbInserted === null) { qualifications.unshift(item); await writeJson(qualificationsFile, qualifications); }
        await audit(req, `qualification.${body.eventType}`, { id: item.id, insuredId, effectiveDate: item.effectiveDate });
        return reply(res, 201, item);
      }
    }

    // ── 免除管理 (Exemptions) ─────────────────────────────────────────────

    const EXEMPTION_TYPES = ["full", "three_quarter", "half", "one_quarter", "student", "young"];

    if (pathname === "/api/exemptions" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const exemptions = (await dbSelect("nenkin.exemption")) ?? await readJson(exemptionsFile, []);
      const insuredId = searchParams.get("insuredId");
      const status = searchParams.get("status");
      let result = exemptions;
      if (insuredId) result = result.filter((e) => e.insuredId === insuredId);
      if (status) result = result.filter((e) => e.status === status);
      return reply(res, 200, result);
    }

    if (pathname === "/api/exemptions" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.insuredId || !body.exemptionType || !body.periodFrom || !body.periodTo) { const e = new Error("insuredId, exemptionType, periodFrom and periodTo are required"); e.statusCode = 400; throw e; }
      if (!EXEMPTION_TYPES.includes(body.exemptionType)) { const e = new Error(`exemptionType must be one of: ${EXEMPTION_TYPES.join(", ")}`); e.statusCode = 400; throw e; }
      const exemptions = (await dbSelect("nenkin.exemption")) ?? await readJson(exemptionsFile, []);
      const item = {
        id: crypto.randomUUID(),
        insuredId: String(body.insuredId),
        exemptionType: body.exemptionType,
        periodFrom: String(body.periodFrom),
        periodTo: String(body.periodTo),
        status: "pending",
        approvedAt: null,
        approvedBy: null,
        notes: body.notes ?? "",
        createdAt: new Date().toISOString(),
        createdBy: actor.id,
      };
      const dbInserted = await dbInsert("nenkin.exemption", item); if (dbInserted === null) { exemptions.unshift(item); await writeJson(exemptionsFile, exemptions); }
      await audit(req, "exemption.apply", { id: item.id, insuredId: item.insuredId, exemptionType: item.exemptionType });
      return reply(res, 201, item);
    }

    const exemptionMatch = pathname.match(/^\/api\/exemptions\/([^/]+)$/);
    if (exemptionMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = exemptionMatch[1];
      const item = (await dbSelectOne("nenkin.exemption", id))
        ?? (await readJson(exemptionsFile, [])).find((e) => e.id === id);
      if (!item) return reply(res, 404, { error: "exemption not found" });
      if (req.method === "GET") return reply(res, 200, item);

      if (item.status !== "pending") return reply(res, 409, { error: "can only edit exemptions in pending status" });
      const body = await parseBody(req);
      const editable = ["exemptionType", "periodFrom", "periodTo", "notes"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      if (patch.exemptionType && !EXEMPTION_TYPES.includes(patch.exemptionType)) {
        return reply(res, 400, { error: `exemptionType must be one of: ${EXEMPTION_TYPES.join(", ")}` });
      }
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("nenkin.exemption", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(exemptionsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(exemptionsFile, items);
        result = items[idx];
      }
      await audit(req, "exemption.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

    const exemptionApprovalMatch = pathname.match(/^\/api\/exemptions\/([^/]+)\/approve$/);
    if (exemptionApprovalMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const action = body.action === "reject" ? "reject" : "approve";
      const exemptions = (await dbSelect("nenkin.exemption")) ?? await readJson(exemptionsFile, []);
      const index = exemptions.findIndex((e) => e.id === exemptionApprovalMatch[1]);
      if (index === -1) return reply(res, 404, { error: "exemption not found" });
      if (exemptions[index].status !== "pending") return reply(res, 409, { error: "exemption is not in pending state" });
      const item = {
        ...exemptions[index],
        status: action === "approve" ? "approved" : "rejected",
        approvedAt: new Date().toISOString(),
        approvedBy: actor.id,
        notes: body.notes ?? exemptions[index].notes,
      };
      const upd = await dbUpdate("nenkin.exemption", item.id, item);
      if (upd === null) { exemptions[index] = item; await writeJson(exemptionsFile, exemptions); }
      await audit(req, `exemption.${action}`, { id: item.id, insuredId: item.insuredId });
      return reply(res, 200, item);
    }


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


    // ── 国民年金 保険料・免除区分の算定（標準仕様書準拠）──
    if (pathname === "/api/nenkin/premium" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeNenkin(body || {});
      await audit(req, "nenkin.premium-calc", { year: result.year, exemption: result.exemption });
      return reply(res, 200, result);
    }

    // ════ 整合性チェック・EUC・予定喪失・住民税連携・免除判定・給付金・機構提供（標準仕様書）════

    // 免除基準額（政令で定める額・令和6年度標準値。扶養親族等の数で決まる前年所得上限）
    const MENJO_LEVELS = [
      { kubun: "全額免除", limit: (deps) => (deps + 1) * 350000 + 320000 },
      { kubun: "3/4免除", limit: (deps) => 880000 + deps * 380000 },
      { kubun: "半額免除", limit: (deps) => 1280000 + deps * 380000 },
      { kubun: "1/4免除", limit: (deps) => 1680000 + deps * 380000 },
    ];
    const judgePersonMenjo = (income, deps) => {
      const thresholds = MENJO_LEVELS.map((l) => ({ kubun: l.kubun, 基準額: l.limit(deps) }));
      const idx = MENJO_LEVELS.findIndex((l) => income <= l.limit(deps));
      return { idx, kubun: idx === -1 ? "該当なし" : MENJO_LEVELS[idx].kubun, thresholds };
    };

    // ── 生年月日と資格期間の整合性チェック ──
    if (pathname === "/api/nenkin/qualification/check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const insured = Array.isArray(b.insured) ? b.insured : [];
      if (!insured.length) return reply(res, 400, { error: "insured[]{atenaId,birthDate,periods[]} required" });
      const issues = [];
      for (const p of insured) {
        const age20 = p.birthDate ? p.birthDate.replace(/^(\d{4})/, (m) => String(Number(m) + 20)) : "";
        for (const period of p.periods ?? []) {
          if (period.to && period.from > period.to) issues.push({ atenaId: p.atenaId, issue: "期間の開始が終了より後", period });
          else if (p.birthDate && period.from < p.birthDate) issues.push({ atenaId: p.atenaId, issue: "資格期間が生年月日より前", period });
          else if (age20 && period.kind !== "任意加入" && period.from < age20) issues.push({ atenaId: p.atenaId, issue: "20歳到達前の第1号期間", period });
        }
      }
      await audit(req, "nenkin-qualification.check", { checked: insured.length, issues: issues.length });
      return reply(res, 200, { checked: insured.length, ok: issues.length === 0, issues });
    }

    // ── EUC機能（抽出・分析・加工・出力。共通機能標準仕様書準拠の演算子）──
    if (pathname === "/api/nenkin/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, "nenkin-euc.query", { hits: hits.length, grouped: !!aggregate });
      return reply(res, 200, { count: hits.length, results: hits, aggregate });
    }

    // ── 高齢任意（60〜64歳）・特例任意（65〜69歳）加入者の予定喪失処理 ──
    if (pathname === "/api/nenkin/voluntary/scheduled-loss" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = { "高齢任意": 65, "特例任意": 70 };
      const members = Array.isArray(b.members) ? b.members : [];
      if (!kinds[b.kind] || !members.length) return reply(res, 400, { error: "kind(高齢任意/特例任意), members[]{atenaId,birthDate,monthsPaid?} required" });
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const lossAge = kinds[b.kind];
      const results = members.map((m) => {
        const lossByAge = m.birthDate ? m.birthDate.replace(/^(\d{4})/, (y) => String(Number(y) + lossAge)) : "";
        const byMonths = b.kind === "高齢任意" && Number(m.monthsPaid ?? 0) >= 480; // 480月（満額）到達でも喪失
        const due = byMonths || (lossByAge && lossByAge <= asOf);
        return { atenaId: m.atenaId, scheduledLossDate: lossByAge, due,
          reason: due ? (byMonths ? "480月到達" : `${lossAge}歳到達`) : "" };
      });
      const processed = results.filter((r) => r.due);
      await audit(req, "nenkin-voluntary.scheduled-loss", { kind: b.kind, processed: processed.length });
      return reply(res, 200, { kind: b.kind, asOf, processed: processed.length, results });
    }

    // ── 免除・納付猶予/学生納付特例の所得情報取得（個人住民税システム連携）＋未申告/無申告/他市区町村課税判定 ──
    if (pathname === "/api/nenkin/income/from-tax" && 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,role,taxRecord?,residentOnJan1,dependentOfOther,taxedElsewhere} required" });
      const results = applicants.map((a) => {
        let classification, income = null;
        if (a.taxedElsewhere) classification = "他市区町村課税";
        else if (a.taxRecord) { classification = "課税情報あり"; income = Number(a.taxRecord.income ?? 0); }
        else if (a.residentOnJan1 && a.dependentOfOther) classification = "無申告";
        else if (a.residentOnJan1) classification = "未申告";
        else classification = "1月1日住登外";
        return { atenaId: a.atenaId, role: a.role ?? "被保険者", income, classification, source: "個人住民税システム" };
      });
      await audit(req, "nenkin-income.from-tax", { purpose: b.purpose ?? "免除", applicants: results.length });
      return reply(res, 200, { purpose: b.purpose ?? "免除", results });
    }

    // ── 免除等の判定（被保険者・配偶者・世帯主それぞれの政令基準額×年度・納付猶予希望・特例認定区分）──
    if (pathname === "/api/nenkin/menjo/judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const persons = Array.isArray(b.persons) ? b.persons : [];
      if (!b.fiscalYear || !persons.length) return reply(res, 400, { error: "fiscalYear, persons[]{role,income,dependents} required" });
      const perPerson = persons.map((p) => {
        const j = judgePersonMenjo(Number(p.income ?? 0), Number(p.dependents ?? 0));
        return { role: p.role, income: Number(p.income ?? 0), dependents: Number(p.dependents ?? 0),
          kubun: j.kubun, 基準額: j.thresholds };
      });
      // 免除区分は全員のうち最も緩い該当（=最も重い区分側）に揃う
      const worstIdx = Math.max(...persons.map((p) => {
        const j = judgePersonMenjo(Number(p.income ?? 0), Number(p.dependents ?? 0));
        return j.idx === -1 ? 99 : j.idx;
      }));
      const menjoKubun = worstIdx === 99 ? null : MENJO_LEVELS[worstIdx].kubun;
      // 納付猶予: 被保険者と配偶者のみで全額免除基準を判定（50歳未満）
      const youthPair = persons.filter((p) => p.role === "被保険者" || p.role === "配偶者");
      const yuyoEligible = youthPair.length > 0 && youthPair.every((p) => judgePersonMenjo(Number(p.income ?? 0), Number(p.dependents ?? 0)).idx === 0);
      let decision, kubun;
      if (menjoKubun) { decision = "承認"; kubun = menjoKubun; }
      else if (yuyoEligible) { decision = "承認"; kubun = "納付猶予"; }
      else { decision = "却下"; kubun = "該当なし"; }
      // 納付猶予が承認される場合でも、希望しなければ一部免除の判定を採用
      // （希望(wishNofuYuyo=true)時のみ一部免除より納付猶予を優先）
      if (kubun === "納付猶予" && b.wishNofuYuyo === false && menjoKubun) kubun = menjoKubun;
      if (yuyoEligible && menjoKubun && menjoKubun !== "全額免除" && b.wishNofuYuyo === true) kubun = "納付猶予";
      const result = { fiscalYear: Number(b.fiscalYear), decision, kubun,
        tokureiNintei: b.tokureiNintei === true ? "該当" : "不該当", perPerson };
      await audit(req, "nenkin-menjo.judge", { fiscalYear: b.fiscalYear, decision, kubun });
      return reply(res, 200, result);
    }

    // ── 継続免除判定（前年度全額免除・納付猶予の承認者を新年度所得で自動再判定）──
    if (pathname === "/api/nenkin/menjo/continuous" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const applicants = Array.isArray(b.applicants) ? b.applicants : [];
      if (!b.fiscalYear || !applicants.length) return reply(res, 400, { error: "fiscalYear, applicants[]{atenaId,lastResult,persons[]} required" });
      const continuable = ["全額免除", "納付猶予"];
      const results = applicants.map((a) => {
        if (!continuable.includes(a.lastResult)) return { atenaId: a.atenaId, judged: "対象外（継続審査は全額免除・納付猶予のみ）" };
        const allFull = (a.persons ?? []).every((p) => judgePersonMenjo(Number(p.income ?? 0), Number(p.dependents ?? 0)).idx === 0);
        return { atenaId: a.atenaId, lastResult: a.lastResult, judged: allFull ? "継続承認" : "継続不可（要申請）" };
      });
      await audit(req, "nenkin-menjo.continuous", { fiscalYear: b.fiscalYear, applicants: results.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear), results });
    }

    // ── 年金生活者支援給付金の給付判定（受給者・新規請求者）──
    if (pathname === "/api/nenkin/seikatsu-shien/judge" && 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[]{atenaId,kind,pensionIncome,otherIncome,taxExemptHousehold} required" });
      const BASE = 789300, SUPPL = 889300; // 令和6年度基準（所得基準額・補足的所得基準額）
      const results = targets.map((t) => {
        const total = Number(t.pensionIncome ?? 0) + Number(t.otherIncome ?? 0);
        let judged;
        if (!t.taxExemptHousehold) judged = "不該当（住民税課税世帯）";
        else if (total <= BASE) judged = "該当（老齢年金生活者支援給付金）";
        else if (total <= SUPPL) judged = "該当（補足的老齢年金生活者支援給付金）";
        else judged = "不該当（所得超過）";
        return { atenaId: t.atenaId, kind: t.kind ?? "受給者", totalIncome: total, judged };
      });
      await audit(req, "nenkin-seikatsu-shien.judge", { targets: results.length });
      return reply(res, 200, { 基準額: BASE, 補足的基準額: SUPPL, results });
    }

    // ── 日本年金機構への所得情報提供（4/1時点世帯員から配偶者・世帯主等を特定し本人所得込みで取込）──
    if (pathname === "/api/nenkin/kikou/income-extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const households = Array.isArray(b.households) ? b.households : [];
      if (!b.fiscalYear || !households.length) return reply(res, 400, { error: "fiscalYear, households[]{householdId,members[]} required" });
      const baseDate = b.baseDate ?? `${b.fiscalYear}-04-01`; // 当該年度4/1時点の世帯員
      const rows = [];
      for (const h of households) {
        const asOfMembers = (h.members ?? []).filter((m) => !m.joinedAt || m.joinedAt <= baseDate);
        const target = asOfMembers.find((m) => m.isTarget);
        if (!target) continue;
        const provide = asOfMembers.filter((m) =>
          m === target || m.role === "配偶者" || m.role === "世帯主" || (b.scope === "世帯員" && m.role === "世帯員"));
        for (const m of provide) {
          rows.push({ householdId: h.householdId, atenaId: m.atenaId, role: m === target ? "本人" : m.role,
            income: Number(m.income ?? 0), baseDate });
        }
      }
      await audit(req, "nenkin-kikou.income-extract", { fiscalYear: b.fiscalYear, rows: rows.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear), baseDate, target: "日本年金機構", count: rows.length, rows });
    }

    // ── 学生納付特例の所得基準判定 ──
    if (pathname === "/api/nenkin/menjo/student" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const result = computeStudentException(await parseBody(req));
      await audit(req, "nenkin-menjo.student", { eligible: result.eligible });
      return reply(res, 200, result);
    }

    // ── 免除等の終了期間の自動算定（喪失月の前月／同月取得喪失は当月）──
    if (pathname === "/api/nenkin/menjo/end-period" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const result = computeNenkinEndPeriod(await parseBody(req));
      if (result.error) return reply(res, 400, { error: result.error });
      await audit(req, "nenkin-menjo.end-period", { endPeriod: result.endPeriod });
      return reply(res, 200, result);
    }

    // ── 国民年金の各種情報の管理（登録・修正・削除・照会）: その他記録/国内協力者/納付記録/メモ/所得限度額/控除額/異動等を種別別CRUD保持 ──
    const nenkinRecordsFile = join(dataRoot, "nenkin-records.json");
    // ── 異動情報の一覧確認（指定した異動事由で抽出した異動情報／報告を必要としない異動に係る情報を一覧表示）──
    if (pathname === "/api/nenkin/idou/list" && 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 異動事由 = String(b.異動事由 ?? "").trim();
      // 指定した異動事由で抽出（未指定なら全件）。報告要否は各異動データの属性で判定（要否ルール自体は本APIの対象外）。
      const 抽出異動 = 異動事由 ? rows.filter((r) => String(r.異動事由 ?? "") === 異動事由) : rows;
      const 報告不要異動 = rows.filter((r) => r.報告要否 === "不要" || r.報告不要 === true || r.報告要否フラグ === false);
      await audit(req, "nenkin-idou.list", { 異動事由: 異動事由 || "全件", 抽出: 抽出異動.length, 報告不要: 報告不要異動.length });
      return reply(res, 200, { 異動事由: 異動事由 || "全件", 抽出異動件数: 抽出異動.length, 抽出異動,
        報告不要件数: 報告不要異動.length, 報告不要異動 });
    }

    if (pathname === "/api/nenkin/records" && req.method === "GET") {
      requireRole(actor, "CLERK"); // 照会（種別/基礎年金番号フィルタ可）
      const 種別 = searchParams.get("種別"), bango = searchParams.get("基礎年金番号") ?? searchParams.get("被保険者番号");
      const all = await readJson(nenkinRecordsFile, []);
      const list = all.filter((x) => (!種別 || x.種別 === 種別) && (!bango || String(x.基礎年金番号 ?? x.被保険者番号 ?? "") === String(bango)));
      return reply(res, 200, { 件数: list.length, records: list });
    }
    if (pathname === "/api/nenkin/records" && req.method === "POST") {
      requireRole(actor, "CLERK"); // 登録
      const b = await parseBody(req);
      if (!b.種別) return reply(res, 400, { error: "種別(その他記録/国内協力者/納付記録/メモ/所得限度額/控除額/異動 等) が必要です" });
      const all = await readJson(nenkinRecordsFile, []);
      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(nenkinRecordsFile, all);
      await audit(req, "nenkin-record.create", { id: item.id, 種別: item.種別 });
      return reply(res, 201, item);
    }
    const nenkinRecordItemMatch = pathname.match(/^\/api\/nenkin\/records\/([^/]+)$/);
    if (nenkinRecordItemMatch) {
      const all = await readJson(nenkinRecordsFile, []);
      const idx = all.findIndex((x) => x.id === nenkinRecordItemMatch[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(nenkinRecordsFile, all); await audit(req, "nenkin-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(nenkinRecordsFile, all); await audit(req, "nenkin-record.delete", { id: removed.id });
        return reply(res, 200, { deleted: true, id: removed.id });
      }
    }

  return false;
}
