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

// ── 就学援助 認定判定（標準仕様書: 世帯の所得と認定基準額の比較で要保護/準要保護を判定）──
//   認定基準額 = 生活保護基準（需要額）× 倍率（自治体条例パラメータ。多くは1.3倍）。
function computeSchoolAidEligibility({ householdIncome, protectionStandard, multiplier = 1.3, isProtected = false }) {
  if (isProtected) return { eligible: true, category: "要保護", threshold: null };
  const income = Math.max(0, Number(householdIncome) || 0);
  const std = Math.max(0, Number(protectionStandard) || 0);
  const m = Number(multiplier) || 1.3;
  const threshold = Math.floor(std * m);
  const eligible = income <= threshold;
  return { eligible, category: eligible ? "準要保護" : "非該当", threshold, multiplier: m, margin: threshold - income };
}

// ── 生活扶助費の算定（標準仕様書 0180050）──
//   生活扶助費 = A×a + B×b + C×c + D
//   A = max((第1類費①×逓減率① + 第2類費①)×d, (第1類費② × 逓減率② + 第2類費②))
//   B = max((第1類費①×逓減率① + 第2類費①)×e, (第1類費③ × 逓減率③ + 第2類費③))
//   C = 生活扶助本体に係る経過的加算、 D = 冬季加算。 a,b,c,d,e は自治体設定の定数。
function computeSeikatsuFujo(input = {}) {
  const n = (v) => Number(v) || 0;
  const r = input.rui ?? {}; // { first1,taigen1,second1, first2,taigen2,second2, first3,taigen3,second3 }
  const k = input.constants ?? {}; // { a,b,c,d,e }
  const a = n(k.a), b = n(k.b), c = n(k.c), d = n(k.d), e = n(k.e);
  const base1 = n(r.first1) * n(r.taigen1) + n(r.second1); // 第1類費①×逓減率①＋第2類費①
  const base2 = n(r.first2) * n(r.taigen2) + n(r.second2);
  const base3 = n(r.first3) * n(r.taigen3) + n(r.second3);
  const A = Math.max(base1 * d, base2);
  const B = Math.max(base1 * e, base3);
  const C = n(input.keikateki); // 経過的加算
  const D = n(input.touki);     // 冬季加算
  const seikatsuFujo = A * a + B * b + C * c + D;
  return { seikatsuFujo: Math.floor(seikatsuFujo), parts: { A, B, C, D }, constants: { a, b, c, d, e } };
}

// ── 認定基準額の算定（標準仕様書 0180049 / 個別調整 0180051）──
//   認定基準額 = 【生活保護に準ずる基準額】合計 × 【認定基準係数】 + 【その他の生活保護に準ずる基準額】合計（＋調整額）
//   生保準ずる基準額合計 = 生活扶助（第1類費×逓減率＋第2類費）＋経過的加算＋冬季加算＋期末一時扶助＋基礎控除
//                          ＋住宅扶助＋教育扶助＋学校給食費＋通学交通費＋その他
function computeNinteiKijun(input = {}) {
  const n = (v) => Number(v) || 0;
  const base = input.base ?? {}; // 各扶助項目
  // 生活扶助（第1類費×逓減率＋第2類費）。詳細版(0180050)の生活扶助費が渡されればそれを優先。
  const seikatsuFujo = input.seikatsuFujo != null
    ? n(input.seikatsuFujo)
    : n(base.dai1rui) * (base.taigen != null ? n(base.taigen) : 1) + n(base.dai2rui);
  const sumKeys = ["keikateki", "touki", "kimatsu", "kisoKojo", "jutaku", "kyoiku", "kyushoku", "tsugaku"];
  let junzuruSum = seikatsuFujo;
  for (const key of sumKeys) junzuruSum += n(base[key]);
  if (Array.isArray(base.others)) for (const v of base.others) junzuruSum += n(v); // その他任意の値
  const keisu = base.keisu != null ? Number(base.keisu) : (input.keisu != null ? Number(input.keisu) : 1); // 認定基準係数
  // その他の生活保護に準ずる基準額（学校給食費/住宅扶助/その他）
  const other = input.other ?? {};
  let otherSum = n(other.kyushoku) + n(other.jutaku);
  if (Array.isArray(other.others)) for (const v of other.others) otherSum += n(v);
  const adjust = n(input.adjust); // 個別調整額（0180051: 加算・減算）
  const ninteiKijun = Math.floor(junzuruSum * keisu + otherSum + adjust);
  return { ninteiKijun, junzuruSum: Math.floor(junzuruSum), keisu, otherSum, adjust, seikatsuFujo: Math.floor(seikatsuFujo) };
}

// ── 認定要件の判定（標準仕様書 0180244）──
//   いずれかの事由に該当すれば認定（準要保護）。所得・収入は認定基準額未満（以下）で該当。
function judgeShugakuNintei(input = {}) {
  const reasons = [];
  const income = input.income != null ? Number(input.income) : null;
  const ninteiKijun = input.ninteiKijun != null ? Number(input.ninteiKijun) : null;
  if (income != null && ninteiKijun != null && income <= ninteiKijun) reasons.push("所得・収入が認定基準額以下");
  const hogoKijun = input.hogoKijun != null ? Number(input.hogoKijun) : null; // 特別支援教育就学奨励費の保護基準額
  if (income != null && hogoKijun != null && income <= hogoKijun) reasons.push("所得・収入が特支保護基準額以下");
  const flagMap = {
    hogoTeishiHaishi: "生活保護の停止または廃止",
    hikazei: "市区町村民税の非課税",
    genmen: "市区町村民税の減免",
    jidoFuyoTeate: "児童扶養手当の支給",
    jigyozeiGenmen: "個人の事業税の減免",
    seikatsuFukushiShikin: "生活福祉資金による貸付け",
  };
  for (const [k, label] of Object.entries(flagMap)) if (input[k]) reasons.push(label);
  return { eligible: reasons.length > 0, category: reasons.length > 0 ? "準要保護" : "非該当", reasons };
}

// ── 各支給対象費目の支給額自動算定（標準仕様書 0180124）──
//   在籍校・学年・認定区分・支給月等の条件に該当する費目の支給額を合算する。
//   items[]: { himoku(費目), unitPrice(単価), grades?(対象学年[]), categories?(対象認定区分[]), months?(対象支給月[]), schools?(対象校[]) }
//   child: { grade, category, month, school }（未指定の制約はその費目に制約なしとして全件該当扱い）
function computeShugakuShikyu(input = {}) {
  const items = Array.isArray(input.items) ? input.items : [];
  const c = input.child ?? {};
  const inList = (list, v) => !Array.isArray(list) || list.length === 0 || list.map(String).includes(String(v));
  const applied = [];
  let total = 0;
  for (const it of items) {
    if (inList(it.grades, c.grade) && inList(it.categories, c.category)
      && inList(it.months, c.month) && inList(it.schools, c.school)) {
      const amount = Number(it.unitPrice) || 0;
      total += amount;
      applied.push({ himoku: it.himoku ?? null, amount });
    }
  }
  return { total: Math.floor(total), appliedCount: applied.length, applied };
}

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 schoolAidApplicantsFile = join(dataRoot, "school-aid-applicants.json");
  const schoolAidApplicationsFile = join(dataRoot, "school-aid-applications.json");
  const schoolAidPaymentsFile = join(dataRoot, "school-aid-payments.json");


    // ── 就学援助 Domain APIs ────────────────────────────────────
    const APPLICATION_TYPES = ["new", "renewal"];

    if (pathname === "/api/school-aid-applicants" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const applicants = (await dbSelect("school_aid.applicant")) ?? await readJson(schoolAidApplicantsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, applicants.map((a) => maskRecord(actor, restrictions, a)));
    }
    if (pathname === "/api/school-aid-applicants" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.householdHead || !body.address) return reply(res, 400, { error: "householdHead and address are required" });
      const applicants = (await dbSelect("school_aid.applicant")) ?? await readJson(schoolAidApplicantsFile, []);
      const applicant = {
        id: crypto.randomUUID(),
        applicantNumber: "SA-" + Date.now(),
        householdHead: body.householdHead,
        address: body.address,
        childCount: body.childCount ?? 1,
        income: body.income ?? null,
        status: "registered",
        createdAt: new Date().toISOString(),
      };
      const dbInserted = await dbInsert("school_aid.applicant", applicant); if (dbInserted === null) { applicants.push(applicant); await writeJson(schoolAidApplicantsFile, applicants); }
      audit(req, "school-aid-applicant.create", { id: applicant.id });
      return reply(res, 201, applicant);
    }

    if (pathname === "/api/school-aid-applications" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const applications = (await dbSelect("school_aid.aid_application")) ?? await readJson(schoolAidApplicationsFile, []);
      const { applicantId, fiscalYear } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      const filtered = applications.filter((a) =>
        (!applicantId || a.applicantId === applicantId) &&
        (!fiscalYear || String(a.fiscalYear) === fiscalYear)
      );
      return reply(res, 200, filtered);
    }
    if (pathname === "/api/school-aid-applications" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.applicantId || !body.fiscalYear || !body.applicationType)
        return reply(res, 400, { error: "applicantId, fiscalYear, and applicationType are required" });
      if (!APPLICATION_TYPES.includes(body.applicationType))
        return reply(res, 400, { error: `applicationType must be one of: ${APPLICATION_TYPES.join(", ")}` });
      const applicants = (await dbSelect("school_aid.applicant")) ?? await readJson(schoolAidApplicantsFile, []);
      if (!applicants.find((a) => a.id === body.applicantId))
        return reply(res, 404, { error: "applicant not found" });
      const applications = (await dbSelect("school_aid.aid_application")) ?? await readJson(schoolAidApplicationsFile, []);
      const duplicate = applications.find((a) => a.applicantId === body.applicantId && a.fiscalYear === body.fiscalYear);
      if (duplicate) return reply(res, 409, { error: "application already exists for this fiscal year" });
      const application = {
        id: crypto.randomUUID(),
        applicantId: body.applicantId,
        fiscalYear: body.fiscalYear,
        applicationType: body.applicationType,
        itemsRequested: body.itemsRequested ?? [],
        status: "applied",
        certifiedAt: null,
        createdAt: new Date().toISOString(),
      };
      const dbInserted2 = await dbInsert("school_aid.aid_application", application); if (dbInserted2 === null) { applications.push(application); await writeJson(schoolAidApplicationsFile, applications); }
      audit(req, "school-aid-application.create", { id: application.id });
      return reply(res, 201, application);
    }

    const appCertifyMatch = pathname.match(/^\/api\/school-aid-applications\/([^/]+)\/certify$/);
    if (appCertifyMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const applications = await readJson(schoolAidApplicationsFile, []);
      const idx = applications.findIndex((a) => a.id === appCertifyMatch[1]);
      if (idx === -1) return reply(res, 404, { error: "application not found" });
      const app = applications[idx];
      if (app.status !== "applied") return reply(res, 409, { error: `cannot certify application in status: ${app.status}` });
      const body = await parseBody(req);
      app.status = body.decision === "rejected" ? "rejected" : "certified";
      app.certifiedAt = new Date().toISOString();
      app.certificationNote = body.note ?? null;
      applications[idx] = app;
      await writeJson(schoolAidApplicationsFile, applications);
      audit(req, "school-aid-application.certify", { id: app.id, status: app.status });
      return reply(res, 200, app);
    }

    if (pathname === "/api/school-aid-payments" && req.method === "GET") {
      requireRole(actor, "ADMIN");
      const payments = await readJson(schoolAidPaymentsFile, []);
      const { applicationId } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      const filtered = payments.filter((p) => !applicationId || p.applicationId === applicationId);
      return reply(res, 200, filtered);
    }
    if (pathname === "/api/school-aid-payments" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.applicationId || !body.paymentItem || !body.amount)
        return reply(res, 400, { error: "applicationId, paymentItem, and amount are required" });
      const applications = await readJson(schoolAidApplicationsFile, []);
      const app = applications.find((a) => a.id === body.applicationId);
      if (!app) return reply(res, 404, { error: "application not found" });
      if (app.status !== "certified") return reply(res, 409, { error: "can only create payment for certified applications" });
      const payments = await readJson(schoolAidPaymentsFile, []);
      const payment = {
        id: crypto.randomUUID(),
        applicationId: body.applicationId,
        paymentItem: body.paymentItem,
        amount: body.amount,
        status: "pending",
        paidAt: null,
        createdAt: new Date().toISOString(),
      };
      payments.push(payment);
      await writeJson(schoolAidPaymentsFile, payments);
      audit(req, "school-aid-payment.create", { id: payment.id });
      return reply(res, 201, payment);
    }

    const paymentPayMatch = pathname.match(/^\/api\/school-aid-payments\/([^/]+)\/pay$/);
    if (paymentPayMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const payments = await readJson(schoolAidPaymentsFile, []);
      const idx = payments.findIndex((p) => p.id === paymentPayMatch[1]);
      if (idx === -1) return reply(res, 404, { error: "payment not found" });
      const payment = payments[idx];
      if (payment.status === "paid") return reply(res, 409, { error: "payment already paid" });
      payment.status = "paid";
      payment.paidAt = new Date().toISOString();
      payments[idx] = payment;
      await writeJson(schoolAidPaymentsFile, payments);
      audit(req, "school-aid-payment.pay", { id: payment.id });
      return reply(res, 200, payment);
    }


    const applicantMatch = pathname.match(/^\/api\/school-aid-applicants\/([^/]+)$/);
    if (applicantMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = applicantMatch[1];
      const applicant = (await dbSelectOne("school_aid.applicant", id))
        ?? (await readJson(schoolAidApplicantsFile, [])).find((a) => a.id === id);
      if (!applicant) return reply(res, 404, { error: "applicant not found" });
      if (req.method === "GET") return reply(res, 200, applicant);

      const body = await parseBody(req);
      const editable = ["householdHead", "address", "childCount", "income"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("school_aid.applicant", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(schoolAidApplicantsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(schoolAidApplicantsFile, items);
        result = items[idx];
      }
      audit(req, "school-aid-applicant.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

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


    // ── 就学援助の認定判定（標準仕様書準拠）──
    if (pathname === "/api/school-aid/eligibility" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeSchoolAidEligibility(body || {});
      await audit(req, "school-aid.eligibility", { category: result.category, eligible: result.eligible });
      return reply(res, 200, result);
    }

    // ── 生活扶助費の算定（0180050）──
    if (pathname === "/api/school-aid/seikatsu-fujo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeSeikatsuFujo(body || {});
      await audit(req, "school-aid.seikatsu-fujo", { seikatsuFujo: result.seikatsuFujo });
      return reply(res, 200, result);
    }

    // ── 認定基準額の自動算定（0180049・個別調整0180051）──
    if (pathname === "/api/school-aid/nintei-kijun" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeNinteiKijun(body || {});
      await audit(req, "school-aid.nintei-kijun", { ninteiKijun: result.ninteiKijun });
      return reply(res, 200, result);
    }

    // ── 認定要件の判定（0180244: 所得が認定基準額以下/生保停廃止/非課税/減免/児童扶養手当 等いずれか該当で認定）──
    if (pathname === "/api/school-aid/nintei-judge" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = judgeShugakuNintei(body || {});
      await audit(req, "school-aid.nintei-judge", { category: result.category, count: result.reasons.length });
      return reply(res, 200, result);
    }

    // ── 各支給対象費目の支給額自動算定（0180124）──
    if (pathname === "/api/school-aid/shikyu" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeShugakuShikyu(body || {});
      await audit(req, "school-aid.shikyu", { total: result.total, count: result.appliedCount });
      return reply(res, 200, result);
    }

    // ════ 住基連携・差異反映・シミュレーション・全銀協振込・引継/連絡票・ログ/ロック/マニュアル（標準仕様書）════

    const schoolAidLogsFile = join(dataRoot, "school-aid-logs.json");
    const schoolAidLockFile = join(dataRoot, "school-aid-lockouts.json");

    // ── 住所の住民記録システム連携取得 ──
    if (pathname === "/api/schoolaid/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, "schoolaid-address.from-juki", { atenaId: b.atenaId });
      return reply(res, 200, { atenaId: String(b.atenaId), address: String(b.jukiAddress),
        source: "住民記録システム", note: "住所は住民記録システムからの連携値を正とする" });
    }

    // ── 差異データの反映（項目ごと・申請情報単位×一括/個別の選択）──
    if (pathname === "/api/schoolaid/diff/apply" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["一括", "個別"];
      const units = ["項目", "申請情報"];
      const diffs = Array.isArray(b.diffs) ? b.diffs : [];
      if (!modes.includes(b.mode) || !units.includes(b.unit) || !diffs.length) {
        return reply(res, 400, { error: `mode(${modes.join("/")}), unit(${units.join("/")}), diffs[]{applicationId,field,newValue,apply} required` });
      }
      const applied = diffs.filter((d) => b.mode === "一括" || d.apply === true)
        .map((d) => ({ applicationId: String(d.applicationId), field: String(d.field), newValue: d.newValue, status: "反映" }));
      const skipped = diffs.length - applied.length;
      await audit(req, "schoolaid-diff.apply", { mode: b.mode, unit: b.unit, applied: applied.length, skipped });
      return reply(res, 200, { mode: b.mode, unit: b.unit, applied, skipped });
    }

    // ── 仮認定審査シミュレーション（児童生徒単位・世帯単位・申請者全体）──
    if (pathname === "/api/schoolaid/nintei/simulate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const units = ["児童生徒", "世帯", "全体"];
      const applicants = Array.isArray(b.applicants) ? b.applicants : [];
      if (!units.includes(b.unit) || !applicants.length) return reply(res, 400, { error: `unit(${units.join("/")}), applicants[]{id,householdIncome,protectionStandard} required` });
      const results = applicants.map((a) => ({ id: a.id,
        ...computeSchoolAidEligibility({ householdIncome: a.householdIncome, protectionStandard: a.protectionStandard, isProtected: a.isProtected === true }) }));
      const summary = { 認定見込: results.filter((r) => r.eligible).length, 否認定見込: results.filter((r) => !r.eligible).length };
      await audit(req, "schoolaid-nintei.simulate", { unit: b.unit, applicants: results.length });
      return reply(res, 200, { kind: "仮認定審査シミュレーション", unit: b.unit, results, summary });
    }

    // ── 認定結果リスト（認定・否認定・保留＋理由）──
    if (pathname === "/api/schoolaid/nintei/results-list" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["認定", "否認定", "保留"];
      const results = Array.isArray(b.results) ? b.results : [];
      if (!results.length) return reply(res, 400, { error: "results[]{childId,result,reason,holdUntil?} required" });
      const bad = results.find((r) => !kinds.includes(r.result));
      if (bad) return reply(res, 400, { error: `result は ${kinds.join("/")} のいずれか` });
      const list = results.map((r) => ({ childId: String(r.childId), result: r.result, reason: r.reason ?? "",
        ...(r.result === "保留" ? { holdUntil: r.holdUntil ?? "" } : {}) }));
      await audit(req, "schoolaid-nintei.results-list", { rows: list.length });
      return reply(res, 200, { kind: "児童生徒ごとの認定結果リスト", count: list.length, list });
    }

    // ── 支給予定額: 定額費目の自動算出（認定区分×在籍校×学年・年度ごと）＋シミュレーション ──
    if (pathname === "/api/schoolaid/shikyu/auto-amount" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      // 定額表（令和6年度 要保護児童生徒援助費の国基準単価例。条例パラメータで上書き可）
      const AMOUNTS = {
        "新入学児童生徒学用品費": { 小学校: 57060, 中学校: 63000 },
        "学用品費": { 小学校: 11630, 中学校: 22730 },
        "通学用品費": { 小学校: 2270, 中学校: 2270 },
        "修学旅行費": { 小学校: 22690, 中学校: 60910 },
      };
      const items = Array.isArray(b.items) ? b.items : [];
      if (!items.length) return reply(res, 400, { error: "items[]{himoku,schoolKind(小学校/中学校),grade,fiscalYear} required" });
      const results = items.map((i) => {
        const table = AMOUNTS[i.himoku];
        const amount = table ? table[i.schoolKind] ?? null : null;
        return { ...i, amount, basis: amount !== null ? "定額（認定区分・在籍校・学年で自動算出）" : "単価表に未登録" };
      });
      await audit(req, "schoolaid-shikyu.auto-amount", { items: results.length });
      return reply(res, 200, { fiscalYear: b.fiscalYear ?? results[0]?.fiscalYear, results });
    }
    if (pathname === "/api/schoolaid/shikyu/simulate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const modes = ["個別", "一括"];
      const groupKeys = { "学校別": "school", "費目別": "himoku" };
      const rows = Array.isArray(b.rows) ? b.rows : [];
      if (!modes.includes(b.mode) || !groupKeys[b.groupBy] || !rows.length) {
        return reply(res, 400, { error: `mode(${modes.join("/")}), groupBy(学校別/費目別), rows[]{school,himoku,amount,month} required` });
      }
      const from = b.from ?? "00", to = b.to ?? "13";
      const inPeriod = rows.filter((r) => String(r.month ?? "01") >= from && String(r.month ?? "01") <= to);
      const groups = {};
      for (const r of inPeriod) {
        const key = String(r[groupKeys[b.groupBy]] ?? "不明");
        groups[key] = (groups[key] ?? 0) + Number(r.amount ?? 0);
      }
      await audit(req, "schoolaid-shikyu.simulate", { mode: b.mode, groupBy: b.groupBy, groups: Object.keys(groups).length });
      return reply(res, 200, { mode: b.mode, groupBy: b.groupBy, period: { from, to },
        groups: Object.entries(groups).map(([key, total]) => ({ key, total })),
        grandTotal: Object.values(groups).reduce((a, v) => a + v, 0) });
    }

    // ── 全銀協フォーマット振込データ（エラーリスト出力時は作成不可）──
    if (pathname === "/api/schoolaid/furikomi/create" && 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,bankCode,branchCode,accountNumber,kanaName,amount} required" });
      const errors = rows.filter((r) => !r.bankCode || !r.branchCode || !r.accountNumber || r.bankUnknown === true)
        .map((r) => ({ recipientId: r.recipientId, issue: r.bankUnknown ? "金融機関が存在しない" : "口座情報が取得できない" }));
      if (errors.length) {
        await audit(req, "schoolaid-furikomi.error-list", { errors: errors.length });
        return reply(res, 422, { created: false, errorList: errors,
          note: "エラーリスト出力時は振込データを作成しない" });
      }
      const records = rows.map((r) => ({ bankCode: r.bankCode, branchCode: r.branchCode,
        accountType: r.accountType === "checking" ? "2" : "1", accountNumber: r.accountNumber,
        kanaName: r.kanaName ?? "", amount: Number(r.amount ?? 0) }));
      await audit(req, "schoolaid-furikomi.create", { records: records.length });
      return reply(res, 200, { created: true, format: "全国銀行協会フォーマット準拠", count: records.length, records });
    }

    // ── 新入学児童生徒学用品費等の新年度引継 ──
    if (pathname === "/api/schoolaid/shinnyugaku/carryover" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const paid = Array.isArray(b.paidThisYear) ? b.paidThisYear : [];
      if (!b.fiscalYear || !paid.length) return reply(res, 400, { error: "fiscalYear, paidThisYear[]{childId,amount} required" });
      const carried = paid.map((p) => ({ childId: String(p.childId), amount: Number(p.amount ?? 0),
        toFiscalYear: Number(b.fiscalYear) + 1, flag: "新入学児童生徒学用品費等 支給済（入学前支給）" }));
      await audit(req, "schoolaid-shinnyugaku.carryover", { carried: carried.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear), carried });
    }

    // ── 受給済み転出者の転出先自治体への連絡票 ──
    if (pathname === "/api/schoolaid/tenshutsu/renrakuhyo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.childId || !b.name || !b.destinationCity) return reply(res, 400, { error: "childId, name, destinationCity required" });
      await audit(req, "schoolaid-tenshutsu.renrakuhyo", { childId: b.childId });
      return reply(res, 200, { title: "就学援助費受給状況連絡票", childId: String(b.childId), 氏名: String(b.name),
        転出先自治体名: String(b.destinationCity), paidItems: Array.isArray(b.paidItems) ? b.paidItems : [],
        note: "新入学児童生徒学用品費等の入学前支給を含む受給済み情報を転出先へ連絡" });
    }

    // ── ログ管理（イベント・通信・エラーの3種別）＋改ざん防止 ──
    if (pathname === "/api/schoolaid/logs" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["イベント", "通信", "エラー"];
      if (!kinds.includes(b.kind) || !b.message) return reply(res, 400, { error: `kind(${kinds.join("/")}), message required` });
      const logs = await readJson(schoolAidLogsFile, []);
      logs.unshift({ id: crypto.randomUUID(), kind: b.kind, message: String(b.message),
        at: new Date().toISOString(), by: actor.id });
      await writeJson(schoolAidLogsFile, logs);
      return reply(res, 201, { recorded: true, kind: b.kind });
    }
    if (pathname === "/api/schoolaid/logs" && req.method === "GET") {
      requireRole(actor, "AUDITOR");
      const kind = searchParams.get("kind") ?? "";
      const logs = await readJson(schoolAidLogsFile, []);
      const hits = kind ? logs.filter((l) => l.kind === kind) : logs;
      return reply(res, 200, { kinds: ["イベント", "通信", "エラー"], count: hits.length, logs: hits.slice(0, 200) });
    }
    if (pathname === "/api/schoolaid/logs/integrity" && req.method === "POST") {
      requireRole(actor, "AUDITOR");
      const b = await parseBody(req);
      const entries = Array.isArray(b.entries) ? b.entries : [];
      const sha = (s) => crypto.createHash
        ? crypto.createHash("sha256").update(s).digest("hex")
        : Array.from(s).reduce((h, c) => ((h * 31 + c.charCodeAt(0)) >>> 0), 0).toString(16);
      let chain = "GENESIS";
      for (const e of entries) chain = sha(chain + JSON.stringify(e));
      return reply(res, 200, { writeProtected: true, entries: entries.length, chainHash: chain,
        measures: ["追記専用（上書き不可）", "SHA-256ハッシュチェーン", "書込権限の分離（AUDITORは読取のみ）"] });
    }

    // ── アカウントロック（複数回のアクセス失敗）──
    if (pathname === "/api/schoolaid/auth/login-attempt" && req.method === "POST") {
      const b = await parseBody(req);
      if (!b.userId) return reply(res, 400, { error: "userId required" });
      const LOCK_AFTER = 5;
      const locks = await readJson(schoolAidLockFile, {});
      const entry = locks[String(b.userId)] ?? { failures: 0, locked: false };
      if (entry.locked) return reply(res, 423, { userId: String(b.userId), locked: true, note: "アカウントロック中（管理者による解除が必要）" });
      if (b.success === true) entry.failures = 0;
      else {
        entry.failures += 1;
        if (entry.failures >= LOCK_AFTER) { entry.locked = true; entry.lockedAt = new Date().toISOString(); }
      }
      locks[String(b.userId)] = entry;
      await writeJson(schoolAidLockFile, locks);
      await audit(req, "schoolaid-auth.attempt", { userId: b.userId, failures: entry.failures, locked: entry.locked });
      return reply(res, entry.locked ? 423 : 200, { userId: String(b.userId), failures: entry.failures, locked: entry.locked, lockAfter: LOCK_AFTER });
    }
    if (pathname === "/api/schoolaid/auth/unlock" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const locks = await readJson(schoolAidLockFile, {});
      if (!locks[String(b.userId)]) return reply(res, 404, { error: "user not found" });
      locks[String(b.userId)] = { failures: 0, locked: false };
      await writeJson(schoolAidLockFile, locks);
      await audit(req, "schoolaid-auth.unlock", { userId: b.userId });
      return reply(res, 200, { userId: String(b.userId), locked: false });
    }

    // ── マニュアル（オンライン＝画面別ヘルプ／冊子）──
    if (pathname === "/api/schoolaid/manual" && req.method === "GET") {
      const MANUAL = {
        domain: { title: "就学援助管理", body: "申請受付・認定審査・支給管理の操作手順。" },
        reports: { title: "帳票", body: "認定結果リスト・連絡票・振込データの出力手順。" },
        admin: { title: "管理", body: "ログ管理・アカウントロック解除・差異反映の運用手順。" },
      };
      const screen = searchParams.get("screen") ?? "";
      if (screen) {
        const page = MANUAL[screen];
        if (!page) return reply(res, 404, { error: "unknown screen" });
        return reply(res, 200, { screen, ...page, kind: "オンラインマニュアル（画面別ヘルプ）" });
      }
      const format = searchParams.get("format") ?? "online";
      return reply(res, 200, { kind: format === "booklet" ? "冊子マニュアル（印刷用・全章）" : "オンラインマニュアル（目次）",
        chapters: Object.entries(MANUAL).map(([k, v]) => ({ screen: k, title: v.title })) });
    }

  return false;
}
