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

// ── 健診・検診の対象判定（標準仕様書: 健康増進法等に基づく年齢・性別等の対象要件で判定）──
//   対象年齢・実施間隔は指針に基づく（自治体で上乗せ可）。
function computeCheckupEligibility({ checkupType, age, sex }) {
  const a = Number(age);
  if (!Number.isFinite(a)) return { error: "age is required" };
  const female = sex === "F" || sex === "female" || sex === "女";
  const rules = {
    "特定健診": { ok: a >= 40 && a <= 74, note: "40〜74歳の医療保険加入者" },
    "後期高齢者健診": { ok: a >= 75, note: "75歳以上" },
    "胃がん検診": { ok: a >= 50, note: "50歳以上（原則2年に1回）" },
    "大腸がん検診": { ok: a >= 40, note: "40歳以上（年1回）" },
    "肺がん検診": { ok: a >= 40, note: "40歳以上（年1回）" },
    "子宮頸がん検診": { ok: female && a >= 20, note: "20歳以上の女性（2年に1回）" },
    "乳がん検診": { ok: female && a >= 40, note: "40歳以上の女性（2年に1回）" },
    "骨粗しょう症検診": { ok: female && [40, 45, 50, 55, 60, 65, 70].includes(a), note: "40〜70歳の女性の節目年齢" },
  };
  const r = rules[checkupType];
  if (!r) return { error: "unknown checkupType", checkupType };
  return { checkupType, age: a, eligible: r.ok, criteria: r.note };
}

// ── 予防接種の接種可否判定（標準仕様書: 対象年齢・標準的接種期間・接種間隔・接種回数。定期接種実施規則準拠）──
//   schedule は指針/自治体パラメータ（日数で指定）。既知ワクチンは既定値を内蔵（自治体で上書き可）。
const _daysBetween = (a, b) => Math.floor((new Date(b) - new Date(a)) / 86400000);
const VACCINE_DEFAULTS = {
  "BCG": { minAgeDays: 0, maxAgeDays: 365, standardFromDays: 150, standardToDays: 240, intervalDays: 0, totalDoses: 1 },
  "MR1期": { minAgeDays: 365, maxAgeDays: 730, standardFromDays: 365, standardToDays: 730, intervalDays: 0, totalDoses: 1 },
  "四種混合1期初回": { minAgeDays: 60, maxAgeDays: 2738, standardFromDays: 60, standardToDays: 365, intervalDays: 20, totalDoses: 3 },
};
function computeVaccineEligibility({ vaccine, birthDate, asOfDate, doses = [], schedule = null }) {
  const sc = schedule || VACCINE_DEFAULTS[vaccine];
  if (!birthDate || !sc) return { error: "birthDate と schedule（または既知のvaccine）が必要です" };
  const today = asOfDate || new Date().toISOString().slice(0, 10);
  const ageDays = _daysBetween(birthDate, today);
  const doseCount = Array.isArray(doses) ? doses.length : 0;
  const inAge = ageDays >= sc.minAgeDays && ageDays <= sc.maxAgeDays;
  const inStandard = sc.standardFromDays != null && ageDays >= sc.standardFromDays && ageDays <= sc.standardToDays;
  const totalDoses = Number(sc.totalDoses) || 1;
  const remaining = Math.max(0, totalDoses - doseCount);
  const lastDate = (Array.isArray(doses) ? doses.map((d) => d.date || d).filter(Boolean).sort() : []).pop() || null;
  let nextEarliest = null, intervalOK = true;
  if (lastDate && sc.intervalDays) {
    const ne = new Date(lastDate); ne.setDate(ne.getDate() + Number(sc.intervalDays));
    nextEarliest = ne.toISOString().slice(0, 10);
    intervalOK = today >= nextEarliest;
  }
  const reasons = [];
  if (!inAge) reasons.push(ageDays < sc.minAgeDays ? "対象年齢未満" : "定期接種の対象年齢を超過");
  if (remaining === 0) reasons.push("規定接種回数を完了済");
  if (!intervalOK) reasons.push(`接種間隔不足（次回最短 ${nextEarliest}）`);
  const eligible = inAge && remaining > 0 && intervalOK;
  return {
    vaccine: vaccine ?? null, ageDays, doseCount, totalDoses, remaining,
    対象年齢内: inAge, 標準的接種期間内: inStandard, 接種間隔OK: intervalOK,
    次回接種可能日: nextEarliest, eligible, reasons,
    note: "対象年齢・標準的接種期間・接種間隔・接種回数で予防接種の接種可否を判定（定期接種実施規則準拠）",
  };
}

// ── 母子保健/成人保健の自動計算ヘルパー ──────────────────────────────
function _parseYmd(s) {
  const m = String(s ?? "").match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
  if (!m) return null;
  const [y, mo, d] = [Number(m[1]), Number(m[2]), Number(m[3])];
  const dt = new Date(Date.UTC(y, mo - 1, d));
  if (dt.getUTCFullYear() !== y || dt.getUTCMonth() !== mo - 1 || dt.getUTCDate() !== d) return null;
  return { y, m: mo, d, dt };
}
// 検診の実施時年齢（満年齢）・月齢、及び喫煙指数（ブリンクマン指数=1日喫煙本数×喫煙年数）を自動計算（0190484/0190522）
function computeExamCalc({ 生年月日, 実施日, 一日喫煙本数, 喫煙年数 } = {}) {
  const b = _parseYmd(生年月日), e = _parseYmd(実施日);
  const out = {};
  if (b && e) {
    let age = e.y - b.y;
    if (e.m < b.m || (e.m === b.m && e.d < b.d)) age -= 1;
    out.実施時年齢 = age;
    out.月齢 = (e.y - b.y) * 12 + (e.m - b.m) - (e.d < b.d ? 1 : 0);
  }
  const cpd = Number(一日喫煙本数), yrs = Number(喫煙年数);
  if (Number.isFinite(cpd) && Number.isFinite(yrs)) out.喫煙指数 = cpd * yrs; // ブリンクマン指数
  return { kind: "検診の実施時年齢・月齢・喫煙指数の自動計算", ...out,
    note: "実施時年齢＝生年月日と実施日の満年齢。月齢＝経過月数。喫煙指数＝1日喫煙本数×喫煙年数(ブリンクマン指数)。" };
}
// 母子保健の質問票評価点数の合計（EPDS・赤ちゃんへの気持ち質問票等）を自動計算（0190273/0190528）
function computeQuestionnaireScore({ items = {} } = {}) {
  const sum = (arr) => Array.isArray(arr) ? arr.reduce((s, v) => s + (Number(v) || 0), 0) : null;
  const out = { kind: "母子保健 質問票評価点数の自動計算" };
  if (items.EPDS != null) {
    const v = sum(items.EPDS);
    out.EPDS評価点数 = v; out.EPDS項目数 = Array.isArray(items.EPDS) ? items.EPDS.length : 0;
  }
  if (items.赤ちゃんへの気持ち != null) out.赤ちゃんへの気持ち評価点数 = sum(items.赤ちゃんへの気持ち);
  out.note = "各質問項目の回答点数を合計（EPDSは10項目0-3点で0-30点、赤ちゃんへの気持ち質問票も合計）。";
  return out;
}
// 歯式の情報から現在歯数・要観察歯数・う歯総数・要治療のむし歯本数を自動計算（0190550/0190491）
function computeDentalCount({ 歯式 = [] } = {}) {
  if (!Array.isArray(歯式)) return { error: "歯式[]{状態} が必要です" };
  // 状態: 健全/現在歯=present, 未萌出/欠損/喪失=absent, 要観察(CO), う歯(C・未処置), 処置歯(O・治療済), 要治療
  let 現在歯数 = 0, 要観察歯数 = 0, う歯総数 = 0, 要治療のむし歯本数 = 0;
  for (const t of 歯式) {
    const st = String(t.状態 ?? t.state ?? "");
    const absent = /未萌出|欠損|喪失|なし/.test(st);
    if (!absent) 現在歯数++;
    if (/要観察|CO/i.test(st)) 要観察歯数++;
    if (/う歯|齲歯|むし歯|虫歯|処置歯|^C$|^O$|要治療/i.test(st)) う歯総数++; // 未処置う歯＋処置歯
    if (/要治療|未処置|^C$/i.test(st)) 要治療のむし歯本数++;
  }
  return { kind: "歯式からの歯数自動計算", 現在歯数, 要観察歯数, う歯総数, 要治療のむし歯本数,
    note: "歯式の各歯の状態から、現在歯数(萌出・非欠損)・要観察歯数(CO)・う歯総数(未処置う歯＋処置歯)・要治療のむし歯本数を集計。" };
}
// 妊娠週数・妊娠日数を自動計算（最終月経開始日 or 分娩予定日 と 基準日 から。0190491）
function computeGestation({ 最終月経開始日, 分娩予定日, 基準日 } = {}) {
  const base = _parseYmd(基準日);
  if (!base) return { error: "基準日(健診日)が必要です" };
  let start = _parseYmd(最終月経開始日);
  if (!start && _parseYmd(分娩予定日)) {
    // 分娩予定日は最終月経開始日＋280日(40週0日) → 起算日を逆算
    const edd = _parseYmd(分娩予定日).dt;
    start = { dt: new Date(edd.getTime() - 280 * 86400000) };
  }
  if (!start) return { error: "最終月経開始日 または 分娩予定日 が必要です" };
  const days = Math.floor((base.dt - start.dt) / 86400000);
  if (days < 0) return { error: "基準日が起算日より前です" };
  return { kind: "妊娠週数の自動計算", 妊娠日数: days, 妊娠週数: Math.floor(days / 7), 端数日: days % 7,
    note: "妊娠週数＝(基準日−最終月経開始日)÷7。分娩予定日指定時は280日(40週)から起算日を逆算。" };
}

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 healthCheckupsFile = join(dataRoot, "health-checkups.json");
  const healthResultsFile = join(dataRoot, "health-results.json");
  const kenkoDocsFile = join(dataRoot, "kenko-documents.json");
  const kenkoAssignFile = join(dataRoot, "kenko-assignments.json");

  // 保健帳票の種別カタログ（標準仕様書 機能ID0190126「共通の出力機能」に準じた出力）
  const KENKO_DOC_TYPES = [
    "予診票", "接種券", "予防接種実施依頼書", "接種時期推奨表",
    "妊婦健診受診券", "産婦健診受診券", "妊婦健診費用助成却下通知", "産婦健診費用助成却下通知",
    "新生児聴覚スクリーニング検査費用助成却下通知", "妊産婦個人台帳", "母子カード",
    "フォロー記録票", "継続協議承認書", "継続協議承認書（保護者宛）", "継続協議承認書（医療機関宛）",
    "訪問票（フェイスシート）",
    "検診案内通知", "検診案内通知（複数集約）", "無料クーポン券",
    "精密検査受診票（紹介状）", "精密検査結果票", "検診却下通知",
  ];
  // 障害者福祉システム等への情報提供データセット
  const KENKO_DATASETS = {
    "techo-seishin": { name: "精神障害者保健福祉手帳情報", target: "障害者福祉システム" },
    "jiritsu-seishin": { name: "自立支援医療（精神通院医療）情報", target: "障害者福祉システム" },
    "jiritsu-ikusei": { name: "自立支援医療（育成医療）情報", target: "障害者福祉システム" },
  };


    // ── 健康診断 予約 (Health Checkups) ──────────────────────────────────

    const CHECKUP_TYPES = ["specific_health", "cancer_screening", "dental", "mental_health", "infant", "pregnant"];

    if (pathname === "/api/health-checkups" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const checkups = (await dbSelect("health_mgmt.health_checkup")) ?? await readJson(healthCheckupsFile, []);
      const { checkupType, status } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = checkups;
      if (checkupType) result = result.filter((c) => c.checkupType === checkupType);
      if (status) result = result.filter((c) => c.status === status);
      return reply(res, 200, result);
    }

    if (pathname === "/api/health-checkups" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.subjectName || !body.checkupType || !body.scheduledDate) return reply(res, 400, { error: "subjectName, checkupType, scheduledDate required" });
      if (!CHECKUP_TYPES.includes(body.checkupType)) return reply(res, 400, { error: `checkupType must be one of: ${CHECKUP_TYPES.join(", ")}` });
      const checkups = (await dbSelect("health_mgmt.health_checkup")) ?? await readJson(healthCheckupsFile, []);
      const checkup = { id: crypto.randomUUID(), subjectName: body.subjectName, checkupType: body.checkupType, scheduledDate: body.scheduledDate, facility: body.facility ?? "", status: "reserved", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("health_mgmt.health_checkup", checkup); if (dbInserted === null) { checkups.push(checkup); await writeJson(healthCheckupsFile, checkups); }
      await audit(req, "health-checkup.reserve", { id: checkup.id, checkupType: body.checkupType });
      return reply(res, 201, checkup);
    }

    // ── 健診結果 (Health Results) ─────────────────────────────────────────

    if (pathname === "/api/health-results" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const results = (await dbSelect("health_mgmt.health_result")) ?? await readJson(healthResultsFile, []);
      const { checkupId } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      return reply(res, 200, checkupId ? results.filter((r) => r.checkupId === checkupId) : results);
    }

    if (pathname === "/api/health-results" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.checkupId || !body.examinedDate) return reply(res, 400, { error: "checkupId, examinedDate required" });
      const checkup = (await dbSelectOne("health_mgmt.health_checkup", body.checkupId))
        ?? (await readJson(healthCheckupsFile, [])).find((c) => c.id === body.checkupId);
      if (!checkup) return reply(res, 404, { error: "checkup not found" });
      if (checkup.status === "completed") return reply(res, 409, { error: "result already recorded" });
      const results = (await dbSelect("health_mgmt.health_result")) ?? await readJson(healthResultsFile, []);
      const result = { id: crypto.randomUUID(), checkupId: body.checkupId, examinedDate: body.examinedDate, overallJudgment: body.overallJudgment ?? "A", findings: body.findings ?? {}, requiresFollowUp: body.requiresFollowUp ?? false, recordedBy: actor.id, recordedAt: new Date().toISOString() };
      results.push(result);
      checkup.status = "completed";
      const hri = await dbInsert("health_mgmt.health_result", result); if (hri === null) { await writeJson(healthResultsFile, results); }
      const upd = await dbUpdate("health_mgmt.health_checkup", checkup.id, checkup); if (upd === null) { const wl = await readJson(healthCheckupsFile, []); const wi = wl.findIndex((x) => x.id === checkup.id); if (wi !== -1) { wl[wi] = checkup; } await writeJson(healthCheckupsFile, wl); }
      await audit(req, "health-result.record", { id: result.id, checkupId: body.checkupId });
      return reply(res, 201, result);
    }


    const checkupMatch = pathname.match(/^\/api\/health-checkups\/([^/]+)$/);
    if (checkupMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = checkupMatch[1];
      const checkup = (await dbSelectOne("health_mgmt.health_checkup", id))
        ?? (await readJson(healthCheckupsFile, [])).find((c) => c.id === id);
      if (!checkup) return reply(res, 404, { error: "health checkup not found" });
      if (req.method === "GET") return reply(res, 200, checkup);

      const body = await parseBody(req);
      const editable = ["subjectName", "checkupType", "scheduledDate", "facility"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      if (patch.checkupType && !CHECKUP_TYPES.includes(patch.checkupType)) return reply(res, 400, { error: `checkupType must be one of: ${CHECKUP_TYPES.join(", ")}` });
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("health_mgmt.health_checkup", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(healthCheckupsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(healthCheckupsFile, items);
        result = items[idx];
      }
      await audit(req, "health-checkup.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy") });
      return reply(res, 200, result);
    }

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


    // ── 健診・検診の対象判定（標準仕様書準拠）──
    if (pathname === "/api/checkup/eligibility" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeCheckupEligibility(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "checkup.eligibility", { checkupType: result.checkupType, eligible: result.eligible });
      return reply(res, 200, result);
    }

    // ── 予防接種の接種可否判定（対象年齢・標準的接種期間・接種間隔・接種回数）──
    if (pathname === "/api/kenko/vaccine/eligibility" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeVaccineEligibility(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "kenko-vaccine.eligibility", { vaccine: result.vaccine, eligible: result.eligible });
      return reply(res, 200, result);
    }

    // ════ 保健帳票出力・予診票資格確認・情報提供・集団健診割り振り（標準仕様書）════

    // ── 保健帳票の出力（0190126準拠の共通出力。接種対象者番号・接種済の接種日印字に対応）──
    if (pathname === "/api/kenko/documents/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!KENKO_DOC_TYPES.includes(b.docType) || !b.atenaId) {
        return reply(res, 400, { error: "docType（保健帳票種別）と atenaId が必須です", docTypes: KENKO_DOC_TYPES });
      }
      const docs = await readJson(kenkoDocsFile, []);
      const doc = {
        id: crypto.randomUUID(), docType: String(b.docType), atenaId: String(b.atenaId),
        subjectNumber: b.subjectNumber ?? `KN-${String(docs.length + 1).padStart(6, "0")}`, // 接種対象者番号等の識別印字
        vaccinatedDates: Array.isArray(b.vaccinatedDates) ? b.vaccinatedDates : [], // 接種済の接種日印字（推奨表）
        addressee: b.addressee ?? "", // 継続協議承認書の宛先（保護者/医療機関）
        data: b.data ?? {}, printedBy: actor.id, printedAt: new Date().toISOString(),
      };
      docs.unshift(doc);
      await writeJson(kenkoDocsFile, docs);
      await audit(req, "kenko-doc.print", { docType: doc.docType, atenaId: doc.atenaId });
      return reply(res, 201, doc);
    }
    if (pathname === "/api/kenko/documents" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const docs = await readJson(kenkoDocsFile, []);
      const docType = searchParams.get("docType") ?? "";
      const atenaId = searchParams.get("atenaId") ?? "";
      let list = docs;
      if (docType) list = list.filter((d) => d.docType === docType);
      if (atenaId) list = list.filter((d) => d.atenaId === atenaId);
      return reply(res, 200, list.slice(0, 300));
    }

    // ── 予診票発行時の資格確認（自己負担金: 住民税・生活保護／接種資格: 身体障害者手帳）＋非課税世帯判定 ──
    if (pathname === "/api/kenko/yoshin/eligibility" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.atenaId) return reply(res, 400, { error: "atenaId required" });
      const members = Array.isArray(b.householdTaxAmounts) ? b.householdTaxAmounts.map(Number) : [];
      const nonTaxableHousehold = members.length > 0 && members.every((t) => (t || 0) <= 0);
      const selfPay = b.onSeikatsuHogo === true || nonTaxableHousehold ? 0 : Number(b.standardFee ?? 1000);
      const result = {
        atenaId: String(b.atenaId),
        qualifications: {
          身体障害者手帳: !!b.techoShintai, 住民税非課税世帯: nonTaxableHousehold, 生活保護: b.onSeikatsuHogo === true,
        },
        nonTaxableHousehold, selfPay,
        note: selfPay === 0 ? "自己負担金なし（非課税世帯または生活保護）" : "",
      };
      await audit(req, "kenko-yoshin.eligibility", { atenaId: result.atenaId, selfPay });
      return reply(res, 200, result);
    }

    // ── 情報提供（障害者福祉システム向け）──
    if (pathname === "/api/kenko/provide" && req.method === "GET") {
      requireRole(actor, "ADMIN");
      const dataset = searchParams.get("dataset") ?? "";
      const def = KENKO_DATASETS[dataset];
      if (!def) return reply(res, 400, { error: `dataset は ${Object.keys(KENKO_DATASETS).join("/")} のいずれか` });
      const checkups = await readJson(healthCheckupsFile, []);
      const rows = checkups.map((p) => ({ atenaId: p.atenaId ?? p.id, fullName: p.fullName ?? "", dataset }));
      await audit(req, "kenko.provide", { dataset, name: def.name, target: def.target, count: rows.length });
      return reply(res, 200, { dataset, name: def.name, target: def.target, count: rows.length, rows });
    }

    // ── 集団健診の事業予定に対する対象者割り振り（生年月日範囲・地区条件、定員まで）──
    if (pathname === "/api/kenko/group-checkup/assign" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.event?.type || !b.event?.date || !Array.isArray(b.residents)) {
        return reply(res, 400, { error: "event{type,place,date,time,capacity} と residents[]{atenaId,birthDate,district} が必須です" });
      }
      const c = b.criteria ?? {};
      const capacity = Math.max(1, Number(b.event.capacity ?? 50));
      const matched = b.residents.filter((r) => {
        if (c.birthFrom && (r.birthDate ?? "") < c.birthFrom) return false;
        if (c.birthTo && (r.birthDate ?? "") > c.birthTo) return false;
        if (c.district && (r.district ?? "") !== c.district) return false;
        return true;
      });
      const assigned = matched.slice(0, capacity);
      const assignments = await readJson(kenkoAssignFile, []);
      const record = { id: crypto.randomUUID(), event: b.event, criteria: c,
        assigned: assigned.map((r) => r.atenaId), overflow: Math.max(0, matched.length - assigned.length),
        by: actor.id, at: new Date().toISOString() };
      assignments.unshift(record);
      await writeJson(kenkoAssignFile, assignments);
      await audit(req, "kenko-group.assign", { type: b.event.type, assigned: assigned.length, overflow: record.overflow });
      return reply(res, 200, record);
    }

    // ════ EUC対象者抽出・消除者整理・世帯主出力（標準仕様書）════

    // ── 訪問・相談・教育の対象者抽出（EUC）──
    if (pathname === "/api/kenko/euc/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["訪問", "相談", "教育"];
      if (!kinds.includes(b.kind) || !Array.isArray(b.residents)) {
        return reply(res, 400, { error: `kind(${kinds.join("/")}), residents[]{atenaId,needs[]} required` });
      }
      const matched = b.residents.filter((r) => Array.isArray(r.needs) && r.needs.includes(b.kind));
      await audit(req, "kenko-euc.extract", { kind: b.kind, matched: matched.length });
      return reply(res, 200, { kind: b.kind, count: matched.length, targets: matched.map((r) => r.atenaId) });
    }

    // ── 事業対象者の消除者区別（転出・死亡）と一括予約解除 ──
    if (pathname === "/api/kenko/subjects/cleanup" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.subjects)) return reply(res, 400, { error: "subjects[]{atenaId,status,hasReservation} required" });
      const removed = b.subjects.filter((s) => ["転出", "死亡"].includes(s.status));
      const cancelled = removed.filter((s) => s.hasReservation === true)
        .map((s) => ({ atenaId: s.atenaId, action: "予約解除", reason: s.status }));
      await audit(req, "kenko-subjects.cleanup", { removed: removed.length, cancelled: cancelled.length });
      return reply(res, 200, {
        active: b.subjects.length - removed.length,
        removed: removed.map((s) => ({ atenaId: s.atenaId, status: s.status })), // 消除者の区別
        bulkCancelled: cancelled, // 一括予約解除
      });
    }

    // ── 同一世帯内に対象者が複数いる場合の世帯主1人分出力 ──
    if (pathname === "/api/kenko/output/per-household" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.targets)) return reply(res, 400, { error: "targets[]{atenaId,householdId,isHead} required" });
      const byHousehold = {};
      for (const t of b.targets) (byHousehold[t.householdId ?? t.atenaId] ??= []).push(t);
      const output = Object.values(byHousehold).map((members) =>
        members.find((m) => m.isHead === true) ?? members[0]); // 世帯主を優先して1人分
      await audit(req, "kenko-output.per-household", { households: output.length, total: b.targets.length });
      return reply(res, 200, { households: output.length, recipients: output.map((m) => m.atenaId) });
    }

    // ════ 受診票随時発行・集約案内・受診年度算出・職域がん検診再取込（標準仕様書）════

    // ── 申込情報をもとにした受診票の随時発行 ──
    if (pathname === "/api/kenko/jushinhyo/issue" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const applications = Array.isArray(b.applications) ? b.applications : [];
      if (!applications.length) return reply(res, 400, { error: "applications[]{atenaId,checkupType} required" });
      const issued = applications.map((a) => ({ atenaId: String(a.atenaId), checkupType: a.checkupType ?? "",
        document: "受診票", issuedAt: new Date().toISOString(), mode: "随時発行" }));
      await audit(req, "kenko-jushinhyo.issue", { issued: issued.length });
      return reply(res, 200, { kind: "受診票の随時発行（申込情報ベース）", count: issued.length, issued });
    }

    // ── 複数検診を集約した案内（通知）の出力（0190126準拠）──
    if (pathname === "/api/kenko/documents/multi" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const checkups = Array.isArray(b.checkups) ? b.checkups : [];
      if (!b.atenaId || checkups.length < 2) return reply(res, 400, { error: "atenaId, checkups[](2件以上) required" });
      await audit(req, "kenko-documents.multi", { atenaId: b.atenaId, checkups: checkups.length });
      return reply(res, 200, { docType: "検診案内通知（複数集約）", atenaId: String(b.atenaId),
        basedOn: "機能ID0190126に準じた共通出力", checkups, consolidated: true });
    }

    // ── 記入日から受診年度を算出（4/1年度境界）──
    if (pathname === "/api/kenko/jushin-nendo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!/^\d{4}-\d{2}-\d{2}$/.test(String(b.entryDate ?? ""))) return reply(res, 400, { error: "entryDate(YYYY-MM-DD) required" });
      const [y, m] = b.entryDate.split("-").map(Number);
      const fiscalYear = m >= 4 ? y : y - 1;
      return reply(res, 200, { entryDate: b.entryDate, fiscalYear, note: "記入日から受診年度を自動算出（4月1日年度境界）" });
    }

    // ── 職域等がん検診の受診状況ファイル取込（エラー対応後の再取込可・行ID冪等）──
    if (pathname === "/api/kenko/shokuiki/import" && 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[]{rowId,atenaId,checkupType,result} required" });
      const errors = rows.filter((r) => !r.rowId || !r.atenaId || !r.checkupType)
        .map((r) => ({ rowId: r.rowId ?? "(なし)", issue: "必須項目不足（rowId/atenaId/checkupType）" }));
      const ok = rows.filter((r) => r.rowId && r.atenaId && r.checkupType);
      await audit(req, "kenko-shokuiki.import", { ok: ok.length, errors: errors.length, retry: b.retry === true });
      return reply(res, 200, { kind: "職域等がん検診 受診状況ファイル取込", imported: ok.length,
        errors, retryable: errors.length > 0,
        note: "エラー行は修正後に同ファイルを再取込可能（rowIdで冪等処理）", retry: b.retry === true });
    }

    // ── 検診の実施時年齢・月齢・喫煙指数(ブリンクマン指数)の自動計算（0190484/0190522）──
    if (pathname === "/api/kenko/exam-calc" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      return reply(res, 200, computeExamCalc(b || {}));
    }
    // ── 母子保健 質問票評価点数(EPDS・赤ちゃんへの気持ち)の合計自動計算（0190273/0190528）──
    if (pathname === "/api/kenko/questionnaire-score" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      return reply(res, 200, computeQuestionnaireScore(b || {}));
    }
    // ── 歯式からの現在歯数・要観察歯数・う歯総数・要治療のむし歯本数の自動計算（0190550/0190491）──
    if (pathname === "/api/kenko/dental-count" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const r = computeDentalCount(b || {});
      if (r.error) return reply(res, 400, r);
      return reply(res, 200, r);
    }
    // ── 妊娠週数の自動計算（最終月経開始日/分娩予定日と健診日から。0190491）──
    if (pathname === "/api/kenko/gestation" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const r = computeGestation(b || {});
      if (r.error) return reply(res, 400, r);
      return reply(res, 200, r);
    }

    // ── 保健事業の記録の管理（登録・修正・削除・照会）: 訪問/教育/予防接種等を種別別にCRUD保持 ──
    const kenkoRecordsFile = join(dataRoot, "kenko-records.json");
    if (pathname === "/api/kenko/records" && req.method === "GET") {
      requireRole(actor, "CLERK"); // 照会（種別/宛名番号フィルタ可）
      const 種別 = searchParams.get("種別"), atena = searchParams.get("宛名番号") ?? searchParams.get("atenaId");
      const all = await readJson(kenkoRecordsFile, []);
      const list = all.filter((x) => (!種別 || x.種別 === 種別) && (!atena || String(x.宛名番号 ?? x.atenaId ?? "") === String(atena)));
      return reply(res, 200, { 件数: list.length, records: list });
    }
    if (pathname === "/api/kenko/records" && req.method === "POST") {
      requireRole(actor, "CLERK"); // 登録
      const b = await parseBody(req);
      if (!b.種別) return reply(res, 400, { error: "種別(訪問/教育/予防接種 等) が必要です" });
      const all = await readJson(kenkoRecordsFile, []);
      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(kenkoRecordsFile, all);
      await audit(req, "kenko-record.create", { id: item.id, 種別: item.種別 });
      return reply(res, 201, item);
    }
    const kenkoRecordItemMatch = pathname.match(/^\/api\/kenko\/records\/([^/]+)$/);
    if (kenkoRecordItemMatch) {
      const all = await readJson(kenkoRecordsFile, []);
      const idx = all.findIndex((x) => x.id === kenkoRecordItemMatch[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(kenkoRecordsFile, all); await audit(req, "kenko-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(kenkoRecordsFile, all); await audit(req, "kenko-record.delete", { id: removed.id });
        return reply(res, 200, { deleted: true, id: removed.id });
      }
    }

  return false;
}
