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

// ── 後期高齢者医療 保険料算定（標準仕様書: 均等割＋所得割。賦課限度額・軽減は国基準、
//   均等割額・所得割率は広域連合の条例パラメータ。既定は令和6年度の例示値）──
const KOUKI_RATES = {
  basicDeduction: 430000,
  perCapita: 50000,    // 均等割額（年額）
  incomeRate: 0.0954,  // 所得割率
  cap: 800000,         // 賦課限度額（令和6年度）
  reduction: { base: 430000, fivePerPerson: 295000, twoPerPerson: 545000 },
};
function computeKoukiPremium({ income, dependents = 0 }, rates = KOUKI_RATES) {
  const inc = Math.max(0, Number(income) || 0);
  const dep = Math.max(0, Number(dependents) || 0);
  const N = dep + 1; // 軽減判定の被保険者数（本人＋同一世帯の後期被保険者）
  const incomeLevy = Math.floor(Math.max(0, inc - rates.basicDeduction) * rates.incomeRate);
  let reductionRate = 0;
  if (inc <= rates.reduction.base) reductionRate = 0.7;
  else if (inc <= rates.reduction.base + rates.reduction.fivePerPerson * N) reductionRate = 0.5;
  else if (inc <= rates.reduction.base + rates.reduction.twoPerPerson * N) reductionRate = 0.2;
  const perCapitaLevy = Math.floor(rates.perCapita * (1 - reductionRate));
  const total = Math.min(rates.cap, incomeLevy + perCapitaLevy);
  return { total, incomeLevy, perCapitaLevy, reductionRate, capped: incomeLevy + perCapitaLevy > rates.cap };
}

// ── 月割賦課（75歳到達・転入転出等。資格取得月〜喪失前月の月数で年額を按分）──
function computeKoukiTsukiwari({ annual = 0, acquireMonth = null, loseMonth = null }) {
  const idx = (m) => {
    if (m == null || m === "") return null;
    const mm = typeof m === "string" && m.includes("-") ? Number(m.split("-")[1]) : Number(m);
    return ((mm - 4) % 12 + 12) % 12; // 4月=0
  };
  const acq = idx(acquireMonth);
  const lose = idx(loseMonth);
  const startIdx = acq == null ? 0 : acq;
  const endIdx = lose == null ? 11 : lose - 1;
  const months = Math.max(0, endIdx - startIdx + 1);
  const prorated = Math.floor((Number(annual) || 0) * months / 12);
  return { annual: Number(annual) || 0, months, prorated, note: "資格取得月〜喪失前月の月数で月割賦課" };
}
// ── 仮徴収額の平準化（標準仕様書 250312/250399。年6期(4/6/8/10/12/2月)で均等化）──
function computeKoukiHeijunka({ annualEstimate = 0, aprilProvisional = 0, mode = "6月8月" }) {
  const annual = Math.max(0, Number(annualEstimate) || 0);
  const ideal = Math.floor(annual / 6 / 100) * 100;
  const april = Number(aprilProvisional) || 0;
  const jun = ideal, aug = ideal;
  const hon = Math.max(0, annual - (april + jun + aug));
  const honPer = Math.floor(hon / 3 / 100) * 100;
  return { mode, annualEstimate: annual, ideal,
    仮徴収: { "4月": april, "6月": jun, "8月": aug },
    本徴収: { "10月": honPer, "12月": honPer, "2月": hon - honPer * 2 },
    note: "仮徴収額の平準化: 年6期で均等化（4月確定・6月/8月を平準化対象として調整）" };
}

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 elderlyInsuredFile = join(dataRoot, "elderly-insured.json");
  const benefitsFile = join(dataRoot, "benefits.json");
  const premiumsFile = join(dataRoot, "premiums.json");
  const koukiRefundsFile = join(dataRoot, "kouki-refunds.json");
  const koukiExportsFile = join(dataRoot, "kouki-exports.json");
  const koukiShunoFile = join(dataRoot, "kouki-shuno.json");


    // ── 後期高齢者被保険者 (Elderly Insured) ────────────────────────────────

    if (pathname === "/api/elderly-insured" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const insured = (await dbSelect("elderly_healthcare.elderly_insured")) ?? await readJson(elderlyInsuredFile, []);
      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: "elderly-insured" }));
        if (!applied.length || hasRole(actor, "SUPPORT_ACCESS")) return p;
        return { id: p.id, status: p.status, insuredNumber: p.insuredNumber, restricted: true, fullName: "（支援措置対象）", address: null, birthDate: null, createdAt: p.createdAt };
      }));
    }

    if (pathname === "/api/elderly-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; }
      const insured = (await dbSelect("elderly_healthcare.elderly_insured")) ?? await readJson(elderlyInsuredFile, []);
      const item = {
        id: crypto.randomUUID(),
        insuredNumber: "KR-" + Date.now().toString(36).toUpperCase(),
        fullName: String(body.fullName),
        birthDate: String(body.birthDate),
        address: body.address ?? null,
        myNumberMasked: body.myNumberLast4 ? "*******" + String(body.myNumberLast4).slice(-4) : null,
        premiumCategory: body.premiumCategory ?? "general",
        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("elderly_healthcare.elderly_insured", item); if (dbInserted === null) { insured.unshift(item); await writeJson(elderlyInsuredFile, insured); }
      await audit(req, "elderly-insured.create", { id: item.id, insuredNumber: item.insuredNumber });
      return reply(res, 201, item);
    }

    const elderlyInsuredMatch = pathname.match(/^\/api\/elderly-insured\/([^/]+)$/);
    if (elderlyInsuredMatch) {
      requireRole(actor, "CLERK");
      const insured = (await dbSelect("elderly_healthcare.elderly_insured")) ?? await readJson(elderlyInsuredFile, []);
      const index = insured.findIndex((p) => p.id === elderlyInsuredMatch[1]);
      if (index === -1) return reply(res, 404, { error: "elderly insured 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, premiumCategory: body.premiumCategory ?? insured[index].premiumCategory, status: body.status ?? insured[index].status, notes: body.notes ?? insured[index].notes, updatedAt: new Date().toISOString(), updatedBy: actor.id };
        const upd = await dbUpdate("elderly_healthcare.elderly_insured", item.id, item);
        if (upd === null) { insured[index] = item; await writeJson(elderlyInsuredFile, insured); }
        await audit(req, "elderly-insured.update", { id: item.id });
        return reply(res, 200, item);
      }
    }

    // ── 給付申請 (Benefits) ───────────────────────────────────────────────

    if (pathname === "/api/benefits" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const benefits = (await dbSelect("elderly_healthcare.benefit")) ?? await readJson(benefitsFile, []);
      const insuredId = searchParams.get("insuredId");
      const status = searchParams.get("status");
      let result = benefits;
      if (insuredId) result = result.filter((b) => b.insuredId === insuredId);
      if (status) result = result.filter((b) => b.status === status);
      return reply(res, 200, result);
    }

    if (pathname === "/api/benefits" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.insuredId || !body.benefitType) { const e = new Error("insuredId and benefitType are required"); e.statusCode = 400; throw e; }
      const benefits = (await dbSelect("elderly_healthcare.benefit")) ?? await readJson(benefitsFile, []);
      const item = { id: crypto.randomUUID(), insuredId: String(body.insuredId), benefitType: String(body.benefitType), requestDate: body.requestDate ?? new Date().toISOString().slice(0, 10), status: "applied", amount: body.amount ?? null, approvedAt: null, approvedBy: null, notes: body.notes ?? "", createdAt: new Date().toISOString(), createdBy: actor.id };
      const dbInserted = await dbInsert("elderly_healthcare.benefit", item); if (dbInserted === null) { benefits.unshift(item); await writeJson(benefitsFile, benefits); }
      await audit(req, "benefit.apply", { id: item.id, insuredId: item.insuredId, benefitType: item.benefitType });
      return reply(res, 201, item);
    }

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

    // ── 保険料 (Premiums) ─────────────────────────────────────────────────

    if (pathname === "/api/premiums" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const premiums = (await dbSelect("elderly_healthcare.premium")) ?? await readJson(premiumsFile, []);
      const insuredId = searchParams.get("insuredId");
      let result = premiums;
      if (insuredId) result = result.filter((p) => p.insuredId === insuredId);
      return reply(res, 200, result);
    }

    if (pathname === "/api/premiums" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const body = await parseBody(req);
      if (!body.insuredId || !body.period || body.amount == null) { const e = new Error("insuredId, period and amount are required"); e.statusCode = 400; throw e; }
      const premiums = (await dbSelect("elderly_healthcare.premium")) ?? await readJson(premiumsFile, []);
      const item = { id: crypto.randomUUID(), insuredId: String(body.insuredId), period: String(body.period), amount: Number(body.amount), status: "billed", paidAt: null, notes: body.notes ?? "", createdAt: new Date().toISOString(), createdBy: actor.id };
      const dbInserted = await dbInsert("elderly_healthcare.premium", item); if (dbInserted === null) { premiums.unshift(item); await writeJson(premiumsFile, premiums); }
      await audit(req, "premium.bill", { id: item.id, insuredId: item.insuredId, amount: item.amount });
      return reply(res, 201, item);
    }

    const premiumPayMatch = pathname.match(/^\/api\/premiums\/([^/]+)\/pay$/);
    if (premiumPayMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const premiums = (await dbSelect("elderly_healthcare.premium")) ?? await readJson(premiumsFile, []);
      const index = premiums.findIndex((p) => p.id === premiumPayMatch[1]);
      if (index === -1) return reply(res, 404, { error: "premium not found" });
      const item = { ...premiums[index], status: "paid", paidAt: new Date().toISOString(), paidBy: actor.id };
      const upd = await dbUpdate("elderly_healthcare.premium", item.id, item);
      if (upd === null) { premiums[index] = item; await writeJson(premiumsFile, premiums); }
      await audit(req, "premium.pay", { id: item.id, insuredId: item.insuredId });
      return reply(res, 200, item);
    }


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


    // ── 後期高齢者医療 保険料の賦課算定（標準仕様書準拠）──
    if (pathname === "/api/kouki/premium" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (body.income === undefined || body.income === null) return reply(res, 400, { error: "income is required" });
      const result = computeKoukiPremium({ income: body.income, dependents: body.dependents ?? 0 });
      await audit(req, "kouki.premium-calc", { total: result.total });
      return reply(res, 200, result);
    }

    // ── 月割賦課（75歳到達・転入転出。資格取得月〜喪失前月で年額を按分）──
    if (pathname === "/api/kouki/fuka/tsukiwari" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (b.annual === undefined) return reply(res, 400, { error: "annual（年額保険料）が必須です" });
      const result = computeKoukiTsukiwari(b);
      await audit(req, "kouki-fuka.tsukiwari", { months: result.months, prorated: result.prorated });
      return reply(res, 200, result);
    }

    // ── 仮徴収額の平準化（年6期で均等化・6月/8月を調整）──
    if (pathname === "/api/kouki/fuka/heijunka" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (b.annualEstimate === undefined) return reply(res, 400, { error: "annualEstimate（年額見込）が必須です" });
      const result = computeKoukiHeijunka(b);
      await audit(req, "kouki-fuka.heijunka", { ideal: result.ideal });
      return reply(res, 200, result);
    }

    // ════ 広域連合連携・還付管理・年齢到達・口座振替ファイル・簡易申告書（標準仕様書）════

    // ── 広域連合向けデータ作成（相当年度単位で作成対象を選択できる）──
    if (pathname === "/api/kouki/kouiki/export" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const datasets = ["資格異動", "賦課", "収納", "住所地特例", "81通知"];
      if (!datasets.includes(b.dataset) || !Array.isArray(b.fiscalYears) || !b.fiscalYears.length) {
        return reply(res, 400, { error: `dataset(${datasets.join("/")}) と fiscalYears[]（相当年度・複数選択可）が必須です` });
      }
      const insured = await readJson(elderlyInsuredFile, []);
      // 81通知（住所地特例該当者通知）は普通徴収者（過去に特別徴収だった者を含む）にも作成できる
      const includeFutsuchou = b.dataset === "81通知" ? b.includeFutsuchou !== false : undefined;
      const exportsList = await readJson(koukiExportsFile, []);
      const record = { id: crypto.randomUUID(), dataset: b.dataset, fiscalYears: b.fiscalYears.map(Number),
        includeFutsuchou, count: insured.length, target: "後期高齢者医療広域連合",
        at: new Date().toISOString(), by: actor.id };
      exportsList.unshift(record);
      await writeJson(koukiExportsFile, exportsList);
      await audit(req, "kouki-kouiki.export", { dataset: b.dataset, fiscalYears: record.fiscalYears, count: record.count });
      return reply(res, 200, record);
    }

    // ── 年齢到達者の把握（住民記録情報を基に75歳到達者・65〜74歳の障害認定候補を抽出）──
    if (pathname === "/api/kouki/age-attainment" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.residents)) return reply(res, 400, { error: "residents[]{atenaId,birthDate} required" });
      const asOf = new Date(b.asOf ?? new Date().toISOString().slice(0, 10));
      const ageOf = (birth) => {
        const bd = new Date(birth);
        let age = asOf.getFullYear() - bd.getFullYear();
        if (asOf.getMonth() < bd.getMonth() || (asOf.getMonth() === bd.getMonth() && asOf.getDate() < bd.getDate())) age -= 1;
        return age;
      };
      const reaching75 = b.residents.filter((r) => {
        const bd = new Date(r.birthDate);
        return ageOf(r.birthDate) === 75 && bd.getMonth() === asOf.getMonth(); // 当月75歳到達
      });
      const disability65to74 = b.residents.filter((r) => { const a = ageOf(r.birthDate); return a >= 65 && a <= 74; });
      await audit(req, "kouki.age-attainment", { reaching75: reaching75.length });
      return reply(res, 200, { asOf: asOf.toISOString().slice(0, 10), reaching75, disabilityCandidates65to74: disability65to74.length });
    }

    // ── 還付管理（過誤納→還付通知→隔地払い一括消込／処理中抽出（滞納判定つき）／時効処理／再発行除外）──
    if (pathname === "/api/kouki/refunds" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId || b.amount === undefined) return reply(res, 400, { error: "atenaId, amount required" });
      const refunds = await readJson(koukiRefundsFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), amount: Number(b.amount),
        occurredAt: b.occurredAt ?? new Date().toISOString().slice(0, 10),
        hasArrears: b.hasArrears === true, arrearsSuspended: b.arrearsSuspended === true,
        method: b.method === "窓口" ? "窓口" : "口座振込", status: "通知済", noticedAt: new Date().toISOString(),
        excludedFromReissue: false, by: actor.id };
      refunds.unshift(item);
      await writeJson(koukiRefundsFile, refunds);
      await audit(req, "kouki-refund.create", { atenaId: item.atenaId, amount: item.amount });
      return reply(res, 201, item);
    }
    // 窓口払い（隔地払い）の一括支払い済（支払予定日を指定）
    if (pathname === "/api/kouki/refunds/pay-bulk" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.payDate) return reply(res, 400, { error: "payDate required" });
      const refunds = await readJson(koukiRefundsFile, []);
      let paid = 0;
      for (const r of refunds) {
        if (r.method === "窓口" && r.status === "通知済") { r.status = "支払済"; r.paidAt = b.payDate; paid += 1; }
      }
      await writeJson(koukiRefundsFile, refunds);
      await audit(req, "kouki-refund.pay-bulk", { paid, payDate: b.payDate });
      return reply(res, 200, { paid, payDate: b.payDate });
    }
    // 還付処理中（支払未完了）の期間指定抽出。滞納の有無・滞納処分停止の有無を判定して返す
    if (pathname === "/api/kouki/refunds" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const refunds = await readJson(koukiRefundsFile, []);
      if (searchParams.get("pending") === "1") {
        const from = searchParams.get("from") ?? "0000-00-00";
        const to = searchParams.get("to") ?? "9999-12-31";
        const pending = refunds.filter((r) => r.status === "通知済" && r.occurredAt >= from && r.occurredAt <= to)
          .map((r) => ({ ...r, arrearsJudgement: r.hasArrears ? (r.arrearsSuspended ? "滞納あり（処分停止中）" : "滞納あり") : "滞納なし" }));
        return reply(res, 200, pending);
      }
      return reply(res, 200, refunds.slice(0, 300));
    }
    // 時効完成した過誤納金の還付時効処理（一括・個別）
    if (pathname === "/api/kouki/refunds/jikou" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const limit = new Date(asOf);
      limit.setFullYear(limit.getFullYear() - 5); // 還付請求権は5年で時効
      const limitStr = limit.toISOString().slice(0, 10);
      const refunds = await readJson(koukiRefundsFile, []);
      let processed = 0;
      for (const r of refunds) {
        if (b.refundId && r.id !== b.refundId) continue;
        if (r.status === "通知済" && r.occurredAt <= limitStr) { r.status = "時効"; r.jikouAt = asOf; processed += 1; }
      }
      await writeJson(koukiRefundsFile, refunds);
      await audit(req, "kouki-refund.jikou", { processed, asOf });
      return reply(res, 200, { processed, threshold: limitStr });
    }
    // 個別指定の対象者を再発行の出力対象から除外
    {
      const exclMatch = pathname.match(/^\/api\/kouki\/refunds\/([^/]+)\/exclude-reissue$/);
      if (exclMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const refunds = await readJson(koukiRefundsFile, []);
        const idx = refunds.findIndex((r) => r.id === exclMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "refund not found" });
        refunds[idx].excludedFromReissue = true;
        await writeJson(koukiRefundsFile, refunds);
        await audit(req, "kouki-refund.exclude-reissue", { id: exclMatch[1] });
        return reply(res, 200, refunds[idx]);
      }
    }

    // ── 口座振替依頼ファイル（金融機関ごとに分割・指定期別＋随時振替）──
    if (pathname === "/api/kouki/furikae/files" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.requests) || !b.requests.length) return reply(res, 400, { error: "requests[]{bankCode,atenaId,amount} required" });
      const byBank = {};
      for (const r of b.requests) (byBank[r.bankCode ?? "0000"] ??= []).push(r);
      const files = Object.entries(byBank).map(([bankCode, rows]) => ({
        bankCode, fileName: `furikae_${bankCode}_${b.period ?? "zuiji"}.dat`, count: rows.length,
        total: rows.reduce((s, r) => s + (Number(r.amount) || 0), 0),
      }));
      await audit(req, "kouki-furikae.files", { banks: files.length, period: b.period ?? "随時", total: b.requests.length });
      return reply(res, 200, { period: b.period ?? "随時振替", files });
    }

    // ── 簡易申告書の作成（一括・個別。所得未申告者（世帯員含む）を対象選択でき、個別除外も可）──
    if (pathname === "/api/kouki/kani-shinkoku" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.targets)) return reply(res, 400, { error: "targets[]{atenaId,unreported} required" });
      const exclude = new Set((b.excludeIds ?? []).map(String));
      const selected = b.targets.filter((t) => !exclude.has(String(t.atenaId)) && (b.unreportedOnly !== true || t.unreported === true));
      await audit(req, "kouki-kani-shinkoku.create", { selected: selected.length, excluded: exclude.size });
      return reply(res, 200, { document: "簡易申告書", count: selected.length,
        targets: selected.map((t) => t.atenaId), excluded: [...exclude] });
    }

    // ════ 収納消込（仮消込・過不足チェック・共通納税）（標準仕様書）════

    // ── 窓口納付の仮消込（領収書をシステム出力した期別を自動仮消込。調定額と延滞金をそれぞれ消込）──
    if (pathname === "/api/kouki/shuno/kari" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.payments) || !b.payments.length) {
        return reply(res, 400, { error: "payments[]{levyId,principal,delinquentCharge} required" });
      }
      const shuno = await readJson(koukiShunoFile, []);
      const results = b.payments.map((p) => {
        const entry = {
          id: crypto.randomUUID(), levyId: String(p.levyId ?? ""),
          principal: Number(p.principal) || 0, // 調定額への仮消込
          delinquentCharge: Number(p.delinquentCharge) || 0, // 延滞金への仮消込（別建て）
          kind: "仮消込", trigger: "窓口領収書出力", at: new Date().toISOString(), by: actor.id,
        };
        shuno.unshift(entry);
        return entry;
      });
      await writeJson(koukiShunoFile, shuno);
      await audit(req, "kouki-shuno.kari", { count: results.length });
      return reply(res, 200, { count: results.length, results });
    }
    // 消込データの過不足等チェック
    if (pathname === "/api/kouki/shuno/check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.rows)) return reply(res, 400, { error: "rows[]{levyId,expected,paid} required" });
      const issues = [];
      for (const r of b.rows) {
        const diff = (Number(r.paid) || 0) - (Number(r.expected) || 0);
        if (diff > 0) issues.push({ levyId: r.levyId, issue: "過納", diff });
        else if (diff < 0) issues.push({ levyId: r.levyId, issue: "不足", diff });
      }
      await audit(req, "kouki-shuno.check", { rows: b.rows.length, issues: issues.length });
      return reply(res, 200, { checked: b.rows.length, ok: issues.length === 0, issues });
    }
    // 共通納税（eLTAX共通納税）の消込処理（一括・個別。調定額と延滞金をそれぞれ消込）
    if (pathname === "/api/kouki/shuno/kyotsu" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.rows) || !b.rows.length) return reply(res, 400, { error: "rows[]{levyId,principal,delinquentCharge} required" });
      const mode = b.mode === "個別" ? "個別" : "一括";
      const shuno = await readJson(koukiShunoFile, []);
      let principalTotal = 0;
      let chargeTotal = 0;
      for (const r of b.rows) {
        const entry = { id: crypto.randomUUID(), levyId: String(r.levyId ?? ""),
          principal: Number(r.principal) || 0, delinquentCharge: Number(r.delinquentCharge) || 0,
          kind: "共通納税消込", mode, at: new Date().toISOString(), by: actor.id };
        shuno.unshift(entry);
        principalTotal += entry.principal;
        chargeTotal += entry.delinquentCharge;
      }
      await writeJson(koukiShunoFile, shuno);
      await audit(req, "kouki-shuno.kyotsu", { mode, count: b.rows.length });
      return reply(res, 200, { mode, count: b.rows.length, principalTotal, delinquentChargeTotal: chargeTotal });
    }

    // ════ 再振替・過誤納内訳・充当取消・督促/催告・決算繰越・分納・滞納処分停止（標準仕様書）════

    const koukiJutoFile = join(dataRoot, "kouki-juto.json");

    // ── 残高不足の期別への口座再振替依頼 ──
    if (pathname === "/api/kouki/furikae/retry" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const failures = Array.isArray(b.failures) ? b.failures : [];
      if (!failures.length) return reply(res, 400, { error: "failures[]{levyId,period,failReason} required" });
      const eligible = failures.filter((f) => f.failReason === "残高不足");
      await audit(req, "kouki-furikae.retry", { eligible: eligible.length });
      return reply(res, 200, { retried: eligible.length, excluded: failures.length - eligible.length,
        items: eligible.map((f) => ({ levyId: f.levyId, period: f.period, action: "口座再振替依頼" })),
        note: "再振替対象は失敗事由が残高不足の期別のみ" });
    }

    // ── 過誤納金の発生状況（内訳: 本料・延滞金・督促手数料）の確認 ──
    if (pathname === "/api/kouki/kagono/breakdown" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const items = Array.isArray(b.items) ? b.items : [];
      if (!items.length) return reply(res, 400, { error: "items[]{levyId,principal,entaikin,tokusokuTesuryo} required" });
      const rows = items.map((i) => ({ levyId: String(i.levyId),
        内訳: { 本料: Number(i.principal ?? 0), 延滞金: Number(i.entaikin ?? 0), 督促手数料: Number(i.tokusokuTesuryo ?? 0) },
        合計: Number(i.principal ?? 0) + Number(i.entaikin ?? 0) + Number(i.tokusokuTesuryo ?? 0) }));
      await audit(req, "kouki-kagono.breakdown", { items: rows.length });
      return reply(res, 200, { kind: "過誤納金の発生状況（内訳確認）", items: rows });
    }

    // ── 充当処理の取消（出納閉鎖後は取消不可に制御）──
    if (pathname === "/api/kouki/juto" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.levyId || !Number(b.amount)) return reply(res, 400, { error: "levyId, amount required" });
      const list = await readJson(koukiJutoFile, []);
      const item = { id: crypto.randomUUID(), levyId: String(b.levyId), amount: Number(b.amount),
        fiscalYear: Number(b.fiscalYear ?? new Date().getFullYear()), status: "充当済",
        at: new Date().toISOString(), by: actor.id };
      list.unshift(item);
      await writeJson(koukiJutoFile, list);
      await audit(req, "kouki-juto.apply", { levyId: item.levyId });
      return reply(res, 201, item);
    }
    {
      const cancelMatch = pathname.match(/^\/api\/kouki\/juto\/([^/]+)\/cancel$/);
      if (cancelMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const list = await readJson(koukiJutoFile, []);
        const idx = list.findIndex((j) => j.id === cancelMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "充当記録がありません" });
        const item = list[idx];
        // 出納閉鎖（会計年度翌年度5/31）後は取消不可
        const closeDate = `${item.fiscalYear + 1}-05-31`;
        const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
        if (asOf > closeDate) {
          await audit(req, "kouki-juto.cancel-blocked", { id: item.id, closeDate });
          return reply(res, 409, { error: "出納閉鎖後のため充当処理の取消はできません", closeDate });
        }
        list[idx] = { ...item, status: "取消", cancelledAt: new Date().toISOString(), cancelledBy: actor.id };
        await writeJson(koukiJutoFile, list);
        await audit(req, "kouki-juto.cancel", { id: item.id });
        return reply(res, 200, list[idx]);
      }
    }

    // ── 督促状の出力（速報・督促停止・分納誓約を加味した対象制御＋延滞金・督促手数料加味＋発送不要者確認）──
    if (pathname === "/api/kouki/tokusoku/run" && 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[]{levyId,unpaid,paidRecently,stopFlag,bunnoSeiyaku,entaikin,tesuryo} required" });
      const targets = [], excluded = [];
      for (const r of rows) {
        if (r.paidRecently === true) excluded.push({ levyId: r.levyId, reason: "速報（直近納付あり）" });
        else if (r.stopFlag === true) excluded.push({ levyId: r.levyId, reason: "督促停止情報" });
        else if (r.bunnoSeiyaku === true) excluded.push({ levyId: r.levyId, reason: "分納誓約" });
        else if (Number(r.unpaid ?? 0) > 0) targets.push({ levyId: r.levyId, unpaid: Number(r.unpaid),
          entaikin: Number(r.entaikin ?? 0), tokusokuTesuryo: Number(r.tesuryo ?? 100),
          total: Number(r.unpaid) + Number(r.entaikin ?? 0) + Number(r.tesuryo ?? 100) });
      }
      await audit(req, "kouki-tokusoku.run", { targets: targets.length, excluded: excluded.length });
      return reply(res, 200, { document: "督促状", targets, excluded,
        note: "延滞金・督促手数料を加味した金額で出力" });
    }
    // 督促状・催告書の発送不要者確認（出力後の納付を期間指定で突合）
    if (pathname === "/api/kouki/tokusoku/unnecessary" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const docKinds = ["督促状", "催告書"];
      const issued = Array.isArray(b.issued) ? b.issued : [];
      const payments = Array.isArray(b.payments) ? b.payments : [];
      if (!docKinds.includes(b.docKind) || !issued.length) return reply(res, 400, { error: `docKind(${docKinds.join("/")}), issued[]{levyId,issuedAt}, payments[]{levyId,paidAt} required` });
      const from = b.from ?? "0000-00-00", to = b.to ?? "9999-12-31";
      const paidSet = new Map(payments.filter((p) => p.paidAt >= from && p.paidAt <= to).map((p) => [String(p.levyId), p.paidAt]));
      const unnecessary = issued.filter((i) => paidSet.has(String(i.levyId)))
        .map((i) => ({ levyId: i.levyId, issuedAt: i.issuedAt, paidAt: paidSet.get(String(i.levyId)), status: "発送不要（出力後納付）" }));
      await audit(req, "kouki-tokusoku.unnecessary", { docKind: b.docKind, unnecessary: unnecessary.length });
      return reply(res, 200, { docKind: b.docKind, period: { from, to }, count: unnecessary.length, list: unnecessary });
    }

    // ── 現年度会計の決算処理（滞納分の一括繰越）──
    if (pathname === "/api/kouki/shuno/kessan" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const arrears = Array.isArray(b.arrears) ? b.arrears : [];
      if (!b.fiscalYear || !arrears.length) return reply(res, 400, { error: "fiscalYear, arrears[]{levyId,unpaid} required" });
      const carried = arrears.filter((a) => Number(a.unpaid ?? 0) > 0)
        .map((a) => ({ levyId: String(a.levyId), unpaid: Number(a.unpaid), toYear: Number(b.fiscalYear) + 1, kubun: "滞納繰越分" }));
      await audit(req, "kouki-shuno.kessan", { fiscalYear: b.fiscalYear, carried: carried.length });
      return reply(res, 200, { fiscalYear: Number(b.fiscalYear), bulk: true, settled: true, carried });
    }

    // ── 分納相談（延滞金・督促手数料を含めた分納計画の試算）──
    if (pathname === "/api/kouki/bunno/plan" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const principal = Number(b.totalArrears);
      const monthly = Number(b.monthlyAmount);
      if (!principal || !monthly) return reply(res, 400, { error: "totalArrears, monthlyAmount required" });
      const entaikin = Number(b.entaikin ?? 0);
      const tesuryo = Number(b.tokusokuTesuryo ?? 0);
      const total = principal + entaikin + tesuryo;
      const months = Math.ceil(total / monthly);
      const schedule = Array.from({ length: months }, (_, i) => ({
        month: i + 1, amount: i === months - 1 ? total - monthly * (months - 1) : monthly,
      }));
      await audit(req, "kouki-bunno.plan", { total, months });
      return reply(res, 200, { breakdown: { 本料: principal, 延滞金: entaikin, 督促手数料: tesuryo },
        total, monthlyAmount: monthly, months, schedule,
        note: "延滞金・督促手数料を含めた分納計画" });
    }

    // ── 滞納処分の停止期別選択（要件充足は自動選択・非充足は手動可・交付要求と重複時は停止優先）──
    if (pathname === "/api/kouki/shobun-teishi/select" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const periods = Array.isArray(b.periods) ? b.periods : [];
      if (!periods.length) return reply(res, 400, { error: "periods[]{levyId,meetsRequirements,kofuYokyu,manualSelect?} required" });
      const results = periods.map((p) => {
        const auto = p.meetsRequirements === true;
        const selected = auto || p.manualSelect === true;
        return { levyId: String(p.levyId), selected, selection: auto ? "自動選択（停止要件充足）" : selected ? "手動選択" : "未選択",
          ...(selected && p.kofuYokyu === true
            ? { conflict: "交付要求と重複", resolution: "滞納処分の停止（徴収権消滅）を優先" } : {}) };
      });
      await audit(req, "kouki-shobun-teishi.select", { periods: results.length, selected: results.filter((r) => r.selected).length });
      return reply(res, 200, { results });
    }

    // ── 各種情報の管理（登録・修正・削除・照会）: 振込元/不稼働日/郵便区/居所不明/世帯/特記事項等を種別別CRUD保持 ──
    const elderlyRecordsFile = join(dataRoot, "elderly-records.json");
    if (pathname === "/api/elderly/records" && req.method === "GET") {
      requireRole(actor, "CLERK"); // 照会（種別/宛名番号フィルタ可）
      const 種別 = searchParams.get("種別"), atena = searchParams.get("宛名番号") ?? searchParams.get("被保険者番号");
      const all = await readJson(elderlyRecordsFile, []);
      const list = all.filter((x) => (!種別 || x.種別 === 種別) && (!atena || String(x.宛名番号 ?? x.被保険者番号 ?? "") === String(atena)));
      return reply(res, 200, { 件数: list.length, records: list });
    }
    if (pathname === "/api/elderly/records" && req.method === "POST") {
      requireRole(actor, "CLERK"); // 登録
      const b = await parseBody(req);
      if (!b.種別) return reply(res, 400, { error: "種別(振込元/不稼働日/郵便区/居所不明/世帯/特記事項 等) が必要です" });
      const all = await readJson(elderlyRecordsFile, []);
      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(elderlyRecordsFile, all);
      await audit(req, "elderly-record.create", { id: item.id, 種別: item.種別 });
      return reply(res, 201, item);
    }
    const elderlyRecordItemMatch = pathname.match(/^\/api\/elderly\/records\/([^/]+)$/);
    if (elderlyRecordItemMatch) {
      const all = await readJson(elderlyRecordsFile, []);
      const idx = all.findIndex((x) => x.id === elderlyRecordItemMatch[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(elderlyRecordsFile, all); await audit(req, "elderly-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(elderlyRecordsFile, all); await audit(req, "elderly-record.delete", { id: removed.id });
        return reply(res, 200, { deleted: true, id: removed.id });
      }
    }

  return false;
}
