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

// ── 人口動態 動態率の算定（人口動態調査: 人口千対の各率と自然増減・合計特殊出生率）──
function computeVitalRates({ births = 0, deaths = 0, marriages = 0, divorces = 0, infantDeaths = 0, population = 0, ageSpecificBirths = null }) {
  const pop = Number(population) || 0;
  const b = Number(births) || 0, d = Number(deaths) || 0;
  const per1000 = (n) => (pop > 0 ? Math.round((n / pop) * 1000 * 10) / 10 : 0);
  const result = {
    population: pop,
    birthRate: per1000(b),                        // 出生率（人口千対）
    deathRate: per1000(d),                        // 死亡率（人口千対）
    marriageRate: per1000(Number(marriages) || 0),// 婚姻率（人口千対）
    divorceRate: per1000(Number(divorces) || 0),  // 離婚率（人口千対）
    naturalIncrease: b - d,                        // 自然増減
    naturalIncreaseRate: per1000(b - d),           // 自然増減率（人口千対）
    infantMortalityRate: b > 0 ? Math.round((Number(infantDeaths) || 0) / b * 1000 * 10) / 10 : 0, // 乳児死亡率（出生千対）
  };
  // 合計特殊出生率（15〜49歳の年齢別出生率の合計）。ageSpecificBirths={age:{births,women}}
  if (ageSpecificBirths && typeof ageSpecificBirths === "object") {
    let tfr = 0;
    for (const k of Object.keys(ageSpecificBirths)) {
      const e = ageSpecificBirths[k];
      if (e && Number(e.women) > 0) tfr += Number(e.births) / Number(e.women);
    }
    result.totalFertilityRate = Math.round(tfr * 100) / 100;
  }
  return result;
}

// ── 届書の入力項目の妥当性確認（標準仕様書: 各入力値が選択肢/範囲/マスタの範囲内かを確認し、範囲外はメッセージ）──
//   出生届・死亡届・婚姻届・離婚届の主要項目を範囲/選択肢で検証する（コードは政令様式準拠）。
const VITAL_SPECS = {
  "出生届": [
    { field: "年号", values: [1, 2, 3], message: "年号は 1昭和/2平成/3令和 のいずれかです" },
    { field: "午前午後", values: [1, 2], message: "午前/午後は 1午前/2午後 です" },
    { field: "時間", min: 0, max: 11, message: "時間は 0〜11 です" },
    { field: "出生場所種別", values: [1, 2, 3, 4, 5], message: "出生場所種別は 1病院/2診療所/3助産所/4自宅/5その他 です" },
  ],
  "死亡届": [
    { field: "年号", values: [1, 2, 3], message: "年号は 1昭和/2平成/3令和 のいずれかです" },
    { field: "午前午後", values: [1, 2], message: "午前/午後は 1午前/2午後 です" },
    { field: "配偶者", values: [1, 2, 3, 4, 5], message: "配偶者は 1いる/2未婚/3死別/4離別/5不詳 です" },
    { field: "死亡場所種別", values: [1, 2, 3, 4, 5], message: "死亡の場所の種別は範囲内である必要があります" },
  ],
  "婚姻届": [
    { field: "初再婚別_夫", values: [1, 2, 3], message: "初婚再婚の別は 1初婚/2再婚(死別)/3再婚(離別) です" },
    { field: "初再婚別_妻", values: [1, 2, 3], message: "初婚再婚の別は 1初婚/2再婚(死別)/3再婚(離別) です" },
  ],
  "離婚届": [
    { field: "種別", values: [1, 2, 3, 4], message: "離婚の種別は 1協議/2調停/3審判/4裁判 です" },
  ],
};
function computeVitalValidation({ notificationType, record = {}, spec: extra = null }) {
  const builtin = VITAL_SPECS[notificationType] || [];
  // 内蔵スペックに、呼び出し側が渡す任意項目スペック（マスタ/範囲）をマージ。
  //   各項目: { field, values?:[...], min?,max?, message?, required? }。同名はcaller側で上書き。
  const byField = {};
  for (const f of [...builtin, ...(Array.isArray(extra) ? extra : [])]) byField[f.field] = { ...byField[f.field], ...f };
  const spec = Object.values(byField);
  if (spec.length === 0) return { error: "notificationType（出生届/死亡届/婚姻届/離婚届）か spec[] のいずれかが必要です" };
  const errors = [];
  for (const f of spec) {
    const msg = f.message || `${f.field}の入力値が範囲外です`;
    const v = record[f.field];
    if (v === undefined || v === null || v === "") { if (f.required) errors.push({ field: f.field, message: `${f.field}は必須です` }); continue; }
    const num = Number(v);
    if (Array.isArray(f.values)) { if (!f.values.includes(num)) errors.push({ field: f.field, message: msg }); }
    else if (f.min !== undefined) { if (!(num >= f.min && num <= f.max)) errors.push({ field: f.field, message: msg }); }
  }
  // 条件付きクロス項目妥当性（死亡届の婚姻適齢整合: 性別×計算年齢×配偶者）。
  //   性別・生年月日・死亡日が揃ったときだけ評価し、婚姻適齢未満なら配偶者＝2(いない・未婚)を強制する。
  if (notificationType === "死亡届") {
    const rec = record || {};
    const has = (...keys) => keys.some((k) => rec[k] !== undefined && rec[k] !== null && rec[k] !== "");
    if (has("性別") && has("生年月日", "出生年月日") && has("死亡日", "死亡したとき")) {
      const mc = computeMarriageAgeCheck(rec);
      if (!mc.error) for (const e of mc.errors) errors.push({ field: e.field, message: e.message });
    }
  }
  return { notificationType, ok: errors.length === 0, checked: spec.length, errorCount: errors.length, errors,
    note: "届書の各入力項目が選択肢/範囲/マスタの範囲内かを確認。範囲外はメッセージを返す。" };
}

// ── 年齢計算ヘルパ（core /api/age の computeAge と同一の満年齢ロジック: 誕生日到来で加齢）──
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 };
}
function fullYears(b, a) { // 満年齢（誕生日到来で加齢）
  let age = a.y - b.y;
  if (a.m < b.m || (a.m === b.m && a.d < b.d)) age -= 1;
  return age;
}

// ── 死亡届の婚姻適齢整合チェック（条件付きクロス項目妥当性: 性別×計算年齢×配偶者）──
//   死亡日を基準日に満年齢を計算し、婚姻適齢に満たない場合は配偶者＝2(いない・未婚)であることを強制する。
//   民法改正（2022/4/1）で男女とも婚姻適齢は18歳。改正前の女性16歳には経過措置がある。
//   0380214: 性別＝1(男)かつ計算年齢18歳未満 → 配偶者＝2
//   0380215: 死亡日が2022/3/31以前かつ性別＝2(女)かつ計算年齢16歳未満 → 配偶者＝2
//   0380216: 死亡日が2022/4/1以降かつ性別＝2(女)かつ生年月日が2006/4/2以降かつ計算年齢18歳未満 → 配偶者＝2
function computeMarriageAgeCheck(src = {}) {
  const pick = (...keys) => { for (const k of keys) { const v = src[k]; if (v !== undefined && v !== null && v !== "") return v; } return undefined; };
  const sexNum = Number(pick("性別", "sex"));
  const spouseRaw = pick("配偶者", "spouse");
  const spouseNum = spouseRaw === undefined ? NaN : Number(spouseRaw);
  const b = parseYmd(pick("生年月日", "出生年月日", "birthDate"));
  const a = parseYmd(pick("死亡日", "死亡したとき", "deathDate"));
  if (!b) return { error: "生年月日(生年月日/birthDate)はYYYY-MM-DDで指定してください" };
  if (!a) return { error: "死亡日(死亡日/死亡したとき/deathDate)はYYYY-MM-DDで指定してください" };
  if (a.dt < b.dt) return { error: "死亡日が生年月日より前です" };
  if (![1, 2].includes(sexNum)) return { error: "性別(性別/sex)は 1男/2女 で指定してください" };
  const age = fullYears(b, a); // 死亡日を基準日とした満年齢
  const APR2022 = parseYmd("2022-04-01").dt;   // 婚姻適齢18歳の施行日
  const APR2006_2 = parseYmd("2006-04-02").dt; // 施行日に18歳未満となる生年月日の下限（経過措置）
  const errors = [];
  const appliedRules = [];
  const requireSingle = (id, label) => {
    appliedRules.push(id);
    if (spouseNum !== 2) errors.push({ rule: id, field: "配偶者",
      message: `${label}のため、配偶者は 2（いない・未婚）である必要があります` });
  };
  if (sexNum === 1 && age < 18) requireSingle("0380214", "男性・計算年齢が18歳未満");
  if (a.dt < APR2022 && sexNum === 2 && age < 16) requireSingle("0380215", "女性・死亡日が2022/3/31以前・計算年齢が16歳未満");
  if (a.dt >= APR2022 && sexNum === 2 && b.dt >= APR2006_2 && age < 18) requireSingle("0380216", "女性・死亡日が2022/4/1以降・生年月日が2006/4/2以降・計算年齢が18歳未満");
  const pad = (n) => String(n).padStart(2, "0");
  return {
    kind: "死亡届の婚姻適齢整合チェック",
    sex: sexNum, spouse: Number.isNaN(spouseNum) ? null : spouseNum, age,
    birthDate: `${b.y}-${pad(b.m)}-${pad(b.d)}`, deathDate: `${a.y}-${pad(a.m)}-${pad(a.d)}`,
    appliedRules, ok: errors.length === 0, errorCount: errors.length, errors,
    note: "性別×(死亡日基準の計算年齢)×配偶者の条件付きクロス項目妥当性。婚姻適齢未満は配偶者=2(いない・未婚)を強制（民法改正2022/4/1・経過措置）。",
  };
}

// ── 出生届の父母年齢確認（条件付きクロス項目妥当性: 続柄×出生日×父母の計算年齢）──
//   父母の生年月日から「子が生まれたとき」「同居を始めたとき」の年齢を計算し、続柄・出生日で定まる下限を満たすか確認する。
//   0380187: 出生日≤2022/3/31 & 嫡出 → 父(出生時)≥18・母(出生時)≥16
//   0380190: 非嫡出 → 母(出生時)≥15
//   0380193: 嫡出 → 父母(同居開始時)≥15
//   0380194: 出生日≥2022/4/1 & 嫡出 → 父(出生時)≥18・母生年月日2006/4/2以降は母(出生時)≥18（経過措置で未満は16）
function computeBirthParentAgeCheck(src = {}) {
  const pick = (...keys) => { for (const k of keys) { const v = src[k]; if (v !== undefined && v !== null && v !== "") return v; } return undefined; };
  const zoku = Number(pick("続柄", "続き柄", "relation"));
  const 出生日 = parseYmd(pick("出生日", "出生日時", "birthDate"));
  const 父b = parseYmd(pick("父生年月日", "父の生年月日", "fatherBirth"));
  const 母b = parseYmd(pick("母生年月日", "母の生年月日", "motherBirth"));
  const 同居 = parseYmd(pick("同居開始日", "同居を始めたとき", "cohabitStart"));
  if (![1, 2].includes(zoku)) return { error: "続柄(続柄/relation)は 1嫡出子/2非嫡出子 で指定してください" };
  if (!出生日) return { error: "出生日(出生日/birthDate)はYYYY-MM-DDで指定してください" };
  const APR2022 = parseYmd("2022-04-01").dt;
  const APR2006_2 = parseYmd("2006-04-02").dt;
  const errors = [], appliedRules = [];
  const need = (cond, id, label) => { appliedRules.push(id); if (cond === false) errors.push({ rule: id, message: label }); };
  if (zoku === 2) {
    if (母b) need(fullYears(母b, 出生日) >= 15, "0380190", "非嫡出子: 母の出生時年齢は15歳以上である必要があります");
  } else { // 嫡出子
    if (出生日.dt < APR2022) {
      if (父b) need(fullYears(父b, 出生日) >= 18, "0380187", "嫡出子(出生日2022/3/31以前): 父の出生時年齢は18歳以上である必要があります");
      if (母b) need(fullYears(母b, 出生日) >= 16, "0380187", "嫡出子(出生日2022/3/31以前): 母の出生時年齢は16歳以上である必要があります");
    } else {
      if (父b) need(fullYears(父b, 出生日) >= 18, "0380194", "嫡出子(出生日2022/4/1以降): 父の出生時年齢は18歳以上である必要があります");
      if (母b) { const min = 母b.dt >= APR2006_2 ? 18 : 16;
        need(fullYears(母b, 出生日) >= min, "0380194", `嫡出子(出生日2022/4/1以降): 母の出生時年齢は${min}歳以上である必要があります`); }
    }
    if (同居) {
      if (父b) need(fullYears(父b, 同居) >= 15, "0380193", "嫡出子: 父の同居開始時年齢は15歳以上である必要があります");
      if (母b) need(fullYears(母b, 同居) >= 15, "0380193", "嫡出子: 母の同居開始時年齢は15歳以上である必要があります");
    }
  }
  return {
    kind: "出生届の父母年齢確認",
    続柄: zoku,
    父出生時年齢: 父b ? fullYears(父b, 出生日) : null, 母出生時年齢: 母b ? fullYears(母b, 出生日) : null,
    appliedRules, ok: errors.length === 0, errorCount: errors.length, errors,
    note: "続柄×出生日×父母の計算年齢の条件付きクロス項目妥当性。婚姻適齢/出生時年齢の下限を満たさない場合にエラー。",
  };
}

// ── 届書のスペース整合の妥当性確認（条件付き: 条件成立時に対象項目がスペース/非スペースであることを確認）──
//   無条件「○○はスペースでないこと」、条件付き「○のとき△はスペースである/でないこと」、
//   逆条件「△がスペースでないとき○であること」、「△がスペースなのは○の場合のみ」を汎用に検証する。
//   rules[]: { when?:[{field,equals?/in?/notIn?/notSpace?/space?}], target(単一/配列), mustBeSpace, message?, id? }
//            または { ifSpace:対象, require:{field,equals/in}, message?, id? }
function computeSpaceConsistency({ rules = [], record = {} } = {}) {
  const rec = record || {};
  const isSpace = (v) => v === undefined || v === null || String(v).trim() === "";
  const one = (cnd) => {
    if (cnd.notSpace) return !isSpace(rec[cnd.notSpace === true ? cnd.field : cnd.notSpace]);
    if (cnd.space) return isSpace(rec[cnd.space === true ? cnd.field : cnd.space]);
    const v = rec[cnd.field];
    if (Array.isArray(cnd.in)) return cnd.in.map(String).includes(String(v));
    if (Array.isArray(cnd.notIn)) return !cnd.notIn.map(String).includes(String(v));
    if (cnd.equals !== undefined) return String(v) === String(cnd.equals);
    return true;
  };
  const condMet = (when) => (Array.isArray(when) ? when : when ? [when] : []).every(one);
  const errors = [];
  for (const r of (Array.isArray(rules) ? rules : [])) {
    if (r.ifSpace !== undefined) { // 対象がスペースなのは条件成立時のみ
      const targets = Array.isArray(r.ifSpace) ? r.ifSpace : [r.ifSpace];
      if (targets.some((t) => isSpace(rec[t])) && !condMet(r.require)) errors.push({ rule: r.id ?? null, message: r.message ?? "条件に反してスペースです" });
      continue;
    }
    if (r.when !== undefined && !condMet(r.when)) continue; // 条件不成立はスキップ
    const targets = Array.isArray(r.target) ? r.target : [r.target];
    if (r.mustBeSpace) { if (targets.some((t) => !isSpace(rec[t]))) errors.push({ rule: r.id ?? null, message: r.message ?? "スペースである必要があります" }); }
    else { if (targets.some((t) => isSpace(rec[t]))) errors.push({ rule: r.id ?? null, message: r.message ?? "いずれもスペースでない必要があります" }); }
  }
  return { ok: errors.length === 0, errorCount: errors.length, errors,
    note: "条件付きスペース整合の妥当性確認。条件成立時に対象項目がスペース/非スペースであることを確認し、反する場合はメッセージを返す。" };
}

// ── 届書の日付妥当性確認（暦上の有効性＋日付の前後関係）──
//   checks[]: { type:"valid", 年,月,日,時?,分?, id?, message? }（月1-12・実在日・時0-23・分0-59）
//             | { type:"order", a, b, op("≦"/"≧"/"<"/">"), id?, message? }（a と b の日付前後関係。受付年月日≦入力年月日 等）
function computeDateValidity({ checks = [] } = {}) {
  const errors = [];
  const validDate = (y, m, d) => {
    y = Number(y); m = Number(m); d = Number(d);
    if (!Number.isInteger(m) || m < 1 || m > 12) return false;
    const last = new Date(Date.UTC(y, m, 0)).getUTCDate();
    return Number.isInteger(d) && d >= 1 && d <= last;
  };
  for (const ck of (Array.isArray(checks) ? checks : [])) {
    if (ck.type === "order") {
      const a = parseYmd(ck.a), b = parseYmd(ck.b);
      if (!a || !b) { errors.push({ id: ck.id ?? null, message: ck.message ?? "日付が不正です" }); continue; }
      const cmp = a.dt.getTime() - b.dt.getTime();
      const op = ck.op ?? "≦";
      const ok = op === "≦" ? cmp <= 0 : op === "≧" ? cmp >= 0 : op === "<" ? cmp < 0 : op === ">" ? cmp > 0 : cmp === 0;
      if (!ok) errors.push({ id: ck.id ?? null, message: ck.message ?? `日付の前後関係(${op})に反します` });
    } else { // valid（暦上の有効性）
      let ok = validDate(ck.年, ck.月, ck.日);
      if (ok && ck.時 !== undefined && ck.時 !== "" && ck.時 !== null) { const h = Number(ck.時); ok = Number.isInteger(h) && h >= 0 && h <= 23; }
      if (ok && ck.分 !== undefined && ck.分 !== "" && ck.分 !== null) { const mi = Number(ck.分); ok = Number.isInteger(mi) && mi >= 0 && mi <= 59; }
      if (!ok) errors.push({ id: ck.id ?? null, message: ck.message ?? "日付・時間が入力可能な範囲外です" });
    }
  }
  return { ok: errors.length === 0, errorCount: errors.length, errors,
    note: "届書の日付妥当性。type=valid(暦上の有効性: 月1-12・実在日・時0-23・分0-59)/type=order(日付の前後関係)を確認。" };
}

// ── 婚姻届/離婚届の妻の年齢確認（受付年月日の2022/4/1境界で16/18歳以上か。同居開始時の15歳以上等は minAge で指定）──
//   妻の生年月と計算基準日(既定=受付年月日。離婚届は死・離別年月、同居要件は同居開始日)から妻の年齢を計算し、下限以上かを確認する。
//   minAge 未指定時は受付年月日が2022/3/31以前=16歳/2022/4/1以降=18歳（婚姻適齢）。minAge 指定時はその値（同居開始時15歳等）。
function computeWifeMarriageAgeCheck({ 受付年月日, 妻生年月, 計算基準日, minAge } = {}) {
  const norm = (s) => { const m = String(s ?? "").match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?/); return m ? `${m[1]}-${m[2].padStart(2, "0")}-${(m[3] || "01").padStart(2, "0")}` : null; };
  const uketsuke = parseYmd(norm(受付年月日));
  const wifeB = parseYmd(norm(妻生年月));
  const base = parseYmd(norm(計算基準日 ?? 受付年月日));
  if (!uketsuke && minAge === undefined) return { error: "受付年月日(またはminAge)を指定してください" };
  if (!wifeB) return { error: "妻生年月はYYYY-MM(-DD)で指定してください" };
  if (!base) return { error: "計算基準日が不正です" };
  const APR2022 = parseYmd("2022-04-01").dt;
  const 下限 = minAge !== undefined ? Number(minAge) : (uketsuke.dt < APR2022 ? 16 : 18);
  const 妻の年齢 = fullYears(wifeB, base);
  const ok = 妻の年齢 >= 下限;
  return { kind: "婚姻届/離婚届の妻の年齢確認", 受付年月日: uketsuke ? norm(受付年月日) : null, 妻の年齢, 下限,
    ok, errorCount: ok ? 0 : 1, errors: ok ? [] : [{ message: `妻の年齢は${下限}歳以上である必要があります` }],
    note: "妻の生年月と計算基準日から妻の年齢を計算し下限以上かを確認。下限は受付年月日の2022/4/1境界(16/18歳)、またはminAge指定(同居開始時15歳等)。" };
}
// ── JIS第1,2水準(JIS X 0208)の文字集合を構築（0380066/0380092/0380129/0380131/0380160/0380173）──
//   Shift_JISの区点→2バイトを生成しTextDecoderで復号して収集。第1,2水準は1-8区(非漢字)＋16-84区
//   (第1水準16-47/第2水準48-84)。9-15区はJIS X 0208では未定義(CP932のNEC特殊文字①㈱等)なので除外し、
//   補助漢字(JIS X 0212)・IBM拡張(髙﨑等)・BMP外(𠮷)も非該当として正しく弾く。結果は規格通り6879字。
let _jisX0208Set = null;
function jisX0208Set() {
  if (_jisX0208Set) return _jisX0208Set;
  const dec = new TextDecoder("shift_jis", { fatal: false });
  const set = new Set();
  const kus = [];
  for (let k = 1; k <= 8; k++) kus.push(k);
  for (let k = 16; k <= 84; k++) kus.push(k);
  for (const ku of kus) {
    const s1 = ((ku + 1) >> 1) + (ku <= 62 ? 0x80 : 0xC0);
    for (let ten = 1; ten <= 94; ten++) {
      const s2 = ku % 2 === 1 ? (ten < 64 ? ten + 0x3F : ten + 0x40) : ten + 0x9E;
      const ch = dec.decode(Uint8Array.from([s1, s2]));
      if (ch && ch !== "�") set.add(ch);
    }
  }
  _jisX0208Set = set;
  return set;
}
// 氏名等の入力値が全てJIS第1,2水準以内か確認し、範囲外文字があればメッセージを返す。
function computeJisLevelCheck({ fields } = {}) {
  const set = jisX0208Set();
  const entries = Array.isArray(fields)
    ? fields.map((f) => [String(f.label ?? f.項目 ?? f.ラベル ?? ""), String(f.value ?? f.値 ?? "")])
    : Object.entries(fields || {}).map(([k, v]) => [String(k), String(v ?? "")]);
  if (!entries.length) return { error: "fields{項目:値} または fields[]{label,value} が必要です" };
  const results = [];
  let allOk = true;
  for (const [label, value] of entries) {
    const bad = [];
    let pos = 0;
    for (const ch of value) { // コードポイント単位で走査（サロゲートペアも1文字として判定）
      if (!set.has(ch)) bad.push({ 文字: ch, コードポイント: "U+" + ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0"), 位置: pos });
      pos++;
    }
    const ok = bad.length === 0;
    if (!ok) allOk = false;
    results.push({ 項目: label, 値: value, 適合: ok, 範囲外文字: bad,
      メッセージ: ok ? null : `「${label}」にJIS第1,2水準以外の文字が含まれています: ${bad.map((b) => b.文字).join("、")}` });
  }
  return { kind: "JIS第1,2水準 文字チェック", 規格文字数: set.size, 全項目適合: allOk, 件数: results.length, 結果: results };
}

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 vitalNotificationsFile = join(dataRoot, "vital-notifications.json");
  const vitalStatsFile = join(dataRoot, "vital-stats.json");


    // ── 人口動態届 (Vital Notifications) ────────────────────────────────

    const VITAL_TYPES = ["birth", "death", "marriage", "divorce", "stillbirth", "fetal_death"];

    if (pathname === "/api/vital-notifications" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const notifications = (await dbSelect("vital_statistics.vital_notification")) ?? await readJson(vitalNotificationsFile, []);
      const { type, status } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = notifications;
      if (type) result = result.filter((n) => n.type === type);
      if (status) result = result.filter((n) => n.status === status);
      return reply(res, 200, result);
    }

    if (pathname === "/api/vital-notifications" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.type || !VITAL_TYPES.includes(body.type)) return reply(res, 400, { error: `type must be one of: ${VITAL_TYPES.join(", ")}` });
      if (!body.occurrenceDate) return reply(res, 400, { error: "occurrenceDate required" });
      const notifications = (await dbSelect("vital_statistics.vital_notification")) ?? await readJson(vitalNotificationsFile, []);
      const notificationNumber = "JD-" + Date.now().toString(36).toUpperCase();
      const notification = { id: crypto.randomUUID(), notificationNumber, type: body.type, occurrenceDate: body.occurrenceDate, receivedDate: body.receivedDate ?? new Date().toISOString().slice(0, 10), declarantName: body.declarantName ?? "", detail: body.detail ?? {}, status: "received", createdBy: actor.id, createdAt: new Date().toISOString() };
      const dbInserted = await dbInsert("vital_statistics.vital_notification", notification); if (dbInserted === null) { notifications.push(notification); await writeJson(vitalNotificationsFile, notifications); }
      await audit(req, `vital.${body.type}`, { id: notification.id, notificationNumber });
      return reply(res, 201, notification);
    }

    const notificationMatch = pathname.match(/^\/api\/vital-notifications\/([^/]+)$/);
    if (notificationMatch) {
      requireRole(actor, "CLERK");
      const notification = (await dbSelectOne("vital_statistics.vital_notification", notificationMatch[1]))
        ?? (await readJson(vitalNotificationsFile, [])).find((n) => n.id === notificationMatch[1]);
      if (!notification) return reply(res, 404, { error: "notification not found" });
      if (req.method === "GET") return reply(res, 200, notification);
      if (req.method === "PUT") {
        const body = await parseBody(req);
        Object.assign(notification, body, { id: notification.id, notificationNumber: notification.notificationNumber, updatedAt: new Date().toISOString() });
        const upd = await dbUpdate("vital_statistics.vital_notification", notification.id, notification); if (upd === null) { const wl = await readJson(vitalNotificationsFile, []); const wi = wl.findIndex((x) => x.id === notification.id); if (wi !== -1) { wl[wi] = notification; } await writeJson(vitalNotificationsFile, wl); }
        await audit(req, "vital.update", { id: notification.id });
        return reply(res, 200, notification);
      }
    }

    const notificationProcessMatch = pathname.match(/^\/api\/vital-notifications\/([^/]+)\/process$/);
    if (notificationProcessMatch && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const notification = (await dbSelectOne("vital_statistics.vital_notification", notificationProcessMatch[1]))
        ?? (await readJson(vitalNotificationsFile, [])).find((n) => n.id === notificationProcessMatch[1]);
      if (!notification) return reply(res, 404, { error: "notification not found" });
      if (notification.status !== "received") return reply(res, 409, { error: "notification is not in received status" });
      notification.status = "processed";
      notification.processedAt = new Date().toISOString();
      notification.processedBy = actor.id;
      const upd = await dbUpdate("vital_statistics.vital_notification", notification.id, notification); if (upd === null) { const wl = await readJson(vitalNotificationsFile, []); const wi = wl.findIndex((x) => x.id === notification.id); if (wi !== -1) { wl[wi] = notification; } await writeJson(vitalNotificationsFile, wl); }
      await audit(req, "vital.process", { id: notification.id, type: notification.type });
      return reply(res, 200, notification);
    }

    // ── 人口動態統計集計 (Vital Statistics) ──────────────────────────────

    if (pathname === "/api/vital-stats" && req.method === "GET") {
      requireRole(actor, "REPORT_VIEWER");
      const notifications = (await dbSelect("vital_statistics.vital_notification")) ?? await readJson(vitalNotificationsFile, []);
      const { year, month } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      const prefix = year && month ? `${year}-${month.padStart(2, "0")}` : year ? year : null;
      const filtered = prefix ? notifications.filter((n) => n.occurrenceDate.startsWith(prefix)) : notifications;
      const counts = {};
      for (const type of VITAL_TYPES) counts[type] = filtered.filter((n) => n.type === type).length;
      return reply(res, 200, { period: prefix ?? "all", counts, total: filtered.length });
    }


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


    // ── 人口動態率の算定（標準仕様書準拠）──
    if (pathname === "/api/vital/rates" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeVitalRates(body || {});
      await audit(req, "vital.rates-calc", { population: result.population });
      return reply(res, 200, result);
    }

    // ── 届書の入力項目の妥当性確認（各入力値が選択肢/範囲/マスタの範囲内か。範囲外はメッセージ）──
    if (pathname === "/api/vital/validate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeVitalValidation(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "vital.validate", { notificationType: result.notificationType, errors: result.errorCount });
      return reply(res, 200, result);
    }

    // ── 届書のクロス項目妥当性確認（汎用cross-fieldバリデータ：ルール型をcallerが指定）──
    //   一方の欄には記載(oneOfRequired)/両方必須(bothRequired)/条件付き必須(requiredIf)/
    //   スペースなら指定値(spaceImpliesValue)/大小・不一致比較(compare:=,≠,≧,≦,>,<・項目和も可)/範囲(range)。
    if (pathname === "/api/vital/cross-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const rec = (b.record && typeof b.record === "object") ? b.record : {};
      const rules = Array.isArray(b.rules) ? b.rules : [];
      if (!rules.length) return reply(res, 400, { error: "rules[] required (type: oneOfRequired/bothRequired/requiredIf/spaceImpliesValue/compare/range)" });
      const isSpace = (v) => v === null || v === undefined || /^[\s　]*$/.test(String(v));
      const num = (v) => Number(String(v).replace(/[^\d.\-]/g, ""));
      const val = (f) => rec[f];
      const sumOf = (fs) => fs.reduce((a, f) => { const n = num(val(f)); return a + (Number.isFinite(n) ? n : 0); }, 0);
      const cmp = (L, op, R) => ({ "=": L === R, "≠": L !== R, "≧": L >= R, "≦": L <= R, ">": L > R, "<": L < R })[op] ?? false;
      const parseDate = (s) => {
        if (s == null) return null;
        const m = String(s).match(/^\s*(\d{4})[-/年]?(\d{1,2})?[-/月]?(\d{1,2})?/);
        if (!m) return null;
        const d = new Date(Number(m[1]), Number(m[2] || 1) - 1, Number(m[3] || 1));
        return Number.isNaN(d.getTime()) ? null : d;
      };
      const ageOf = (birth, ref) => { // 生年月(日)と基準日から満年齢を算出
        const b = parseDate(birth), r = parseDate(ref);
        if (!b || !r) return null;
        let a = r.getFullYear() - b.getFullYear();
        if (r.getMonth() < b.getMonth() || (r.getMonth() === b.getMonth() && r.getDate() < b.getDate())) a -= 1;
        return a;
      };
      const daysBetween = (from, to) => { const a = parseDate(from), z = parseDate(to); return (a && z) ? Math.round((z - a) / 86400000) : null; };
      const evalCond = (cond) => {
        if (!cond) return true;
        if (Array.isArray(cond.all)) return cond.all.every(evalCond);
        if (Array.isArray(cond.any)) return cond.any.some(evalCond);
        if (cond.not) return !evalCond(cond.not);
        if (cond.ageOf) { const a = ageOf(val(cond.ageOf.birth), val(cond.ageOf.ref)); return a == null ? false : cmp(a, cond.op, Number(cond.value)); }
        if (cond.daysBetween) { const d = daysBetween(val(cond.daysBetween.from), val(cond.daysBetween.to)); return d == null ? false : cmp(d, cond.op, Number(cond.value)); }
        if (cond.leftField !== undefined && cond.rightField !== undefined) return cmp(num(val(cond.leftField)), cond.op, num(val(cond.rightField)));
        const v = val(cond.field);
        if (cond.op === "present") return !isSpace(v);
        if (cond.op === "space") return isSpace(v);
        if (["dateLE", "dateGE", "dateLT", "dateGT"].includes(cond.op)) {
          const dv = parseDate(v), dt = parseDate(cond.value);
          if (!dv || !dt) return false;
          return { dateLE: dv <= dt, dateGE: dv >= dt, dateLT: dv < dt, dateGT: dv > dt }[cond.op];
        }
        if (["=", "≠"].includes(cond.op)) return cond.op === "=" ? String(v) === String(cond.value) : String(v) !== String(cond.value);
        return cmp(num(v), cond.op, Number(cond.value));
      };
      const errors = [];
      for (const r of rules) {
        let ok = true; const t = r.type;
        if (t === "oneOfRequired") ok = (r.fields || []).some((f) => !isSpace(val(f)));
        else if (t === "bothRequired") ok = (r.fields || []).every((f) => !isSpace(val(f)));
        else if (t === "requiredIf") ok = !evalCond(r.when) || evalCond(r.then);
        else if (t === "assert") ok = evalCond(r.cond); // 任意の条件式を満たすこと
        else if (t === "ageAtLeast") { const a = ageOf(val(r.birthField), val(r.refField)); ok = evalCond(r.when) ? (a != null && a >= Number(r.min)) : true; } // 生年月＋基準日→満年齢≧min
        else if (t === "dayDiffWithin") { const d = daysBetween(val(r.fromField), val(r.toField)); ok = evalCond(r.when) ? (d != null && d <= Number(r.maxDays)) : true; } // 2日付間の日数≦maxDays
        else if (t === "spaceImpliesValue") {
          const sp = isSpace(val(r.field));
          if (sp) ok = [].concat(r.equals ?? []).map(String).includes(String(val(r.target)));
          else if (r.elseEquals !== undefined) ok = [].concat(r.elseEquals).map(String).includes(String(val(r.target)));
        } else if (t === "compare") {
          const L = r.leftFields ? sumOf(r.leftFields) : num(val(r.left));
          const R = r.rightFields ? sumOf(r.rightFields) : (r.right !== undefined && rec[r.right] === undefined ? Number(r.right) : num(val(r.right)));
          ok = cmp(L, r.op, R);
        } else if (t === "range") { const x = num(val(r.field)); ok = x >= Number(r.min) && x <= Number(r.max); }
        else { ok = true; }
        if (!ok) errors.push({ type: t, message: r.message || `${t} に違反しています`, fields: (r.fields || [r.field, r.target, r.left, r.right]).filter((x) => x != null) });
      }
      await audit(req, "vital.cross-check", { rules: rules.length, errors: errors.length });
      return reply(res, 200, { valid: errors.length === 0, errorCount: errors.length, errors });
    }

    // ── 死亡届の婚姻適齢整合チェック（性別×死亡日基準の計算年齢×配偶者の条件付きクロス項目妥当性）──
    if (pathname === "/api/vital/marriage-age-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeMarriageAgeCheck(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "vital.marriage-age-check", { appliedRules: result.appliedRules, errors: result.errorCount });
      return reply(res, 200, result);
    }
    // ── 出生届の父母年齢確認（続柄×出生日×父母の計算年齢の条件付きクロス項目妥当性）──
    if (pathname === "/api/vital/birth-parent-age-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeBirthParentAgeCheck(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "vital.birth-parent-age-check", { appliedRules: result.appliedRules, errors: result.errorCount });
      return reply(res, 200, result);
    }
    // ── 届書のスペース整合の妥当性確認（条件付きでスペース/非スペースを確認）──
    if (pathname === "/api/vital/space-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeSpaceConsistency(body || {});
      await audit(req, "vital.space-check", { errors: result.errorCount });
      return reply(res, 200, result);
    }
    // ── 届書の日付妥当性確認（暦上の有効性＋日付の前後関係）──
    if (pathname === "/api/vital/date-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeDateValidity(body || {});
      await audit(req, "vital.date-check", { errors: result.errorCount });
      return reply(res, 200, result);
    }
    // ── 婚姻届/離婚届の妻の婚姻適齢確認（受付年月日の2022/4/1境界で16/18歳以上か）──
    if (pathname === "/api/vital/wife-marriage-age-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeWifeMarriageAgeCheck(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "vital.wife-marriage-age-check", { 妻の年齢: result.妻の年齢, ok: result.ok });
      return reply(res, 200, result);
    }
    // ── 氏名等のJIS第1,2水準(JIS X 0208)文字チェック（0380066/0380092/0380129/0380131/0380160/0380173）──
    if (pathname === "/api/vital/jis-check" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeJisLevelCheck(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "vital.jis-check", { 件数: result.件数, 全項目適合: result.全項目適合 });
      return reply(res, 200, result);
    }

    // ════ 新旧コードマスタ・調査票保存・送付票/事件簿番号・死産票抽出（標準仕様書）════

    const vitalMastersFile = join(dataRoot, "vital-masters.json");
    const vitalSurveysFile = join(dataRoot, "vital-surveys.json");
    const vitalSofuhyoFile = join(dataRoot, "vital-sofuhyo.json");

    // ── 新旧2つ＋使用予定の市区町村コード等マスタ（配列替え=使用開始時に予定→新・新→旧へ繰上げ）──
    if (pathname === "/api/vital/masters/codes" && req.method === "PUT") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      if (!b.kind || !b.newCode) return reply(res, 400, { error: "kind(市区町村/支所/保健所/首長名), newCode required" });
      const masters = await readJson(vitalMastersFile, {});
      masters[String(b.kind)] = { old: b.oldCode ?? masters[String(b.kind)]?.new ?? null,
        new: String(b.newCode), planned: b.plannedCode ?? null, updatedAt: new Date().toISOString() };
      await writeJson(vitalMastersFile, masters);
      await audit(req, "vital-masters.update", { kind: b.kind });
      return reply(res, 200, masters[String(b.kind)]);
    }
    if (pathname === "/api/vital/masters/codes/rotate" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const masters = await readJson(vitalMastersFile, {});
      const entry = masters[String(b.kind ?? "")];
      if (!entry || !entry.planned) return reply(res, 400, { error: "kind の使用予定コードが未登録です" });
      masters[String(b.kind)] = { old: entry.new, new: entry.planned, planned: null, rotatedAt: new Date().toISOString() };
      await writeJson(vitalMastersFile, masters);
      await audit(req, "vital-masters.rotate", { kind: b.kind });
      return reply(res, 200, { ...masters[String(b.kind)], note: "使用予定→新・新→旧へ配列替え（新コード初回使用時）" });
    }
    if (pathname === "/api/vital/masters/codes" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, await readJson(vitalMastersFile, {}));
    }

    // ── 調査票の保存（一時保存可）・過去レコード蓄積・画面/備考欄別紙/全件出力 ──
    if (pathname === "/api/vital/surveys" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const statuses = ["保存", "一時保存"];
      if (!b.eventType || !statuses.includes(b.status ?? "保存")) return reply(res, 400, { error: `eventType, status(${statuses.join("/")}) required` });
      const surveys = await readJson(vitalSurveysFile, []);
      const item = { id: crypto.randomUUID(), eventType: String(b.eventType), status: b.status ?? "保存",
        fields: b.fields ?? {}, biko: b.biko ?? "", receiptDate: b.receiptDate ?? new Date().toISOString().slice(0, 10),
        year: Number(b.year ?? new Date().getFullYear()), branch: b.branch ?? "本庁",
        createdAt: new Date().toISOString(), by: actor.id };
      surveys.unshift(item); // 過去レコードも蓄積・保存（上書きせず追加）
      await writeJson(vitalSurveysFile, surveys);
      await audit(req, "vital-surveys.save", { eventType: item.eventType, status: item.status });
      return reply(res, 201, item);
    }
    {
      const printMatch = pathname.match(/^\/api\/vital\/surveys\/([^/]+)\/print$/);
      if (printMatch && req.method === "GET") {
        requireRole(actor, "CLERK");
        const surveys = await readJson(vitalSurveysFile, []);
        const survey = surveys.find((s) => s.id === printMatch[1]);
        if (!survey) return reply(res, 404, { error: "survey not found" });
        await audit(req, "vital-surveys.print", { id: survey.id });
        return reply(res, 200, { kind: "調査票画面の出力", survey,
          bikoAttachment: survey.biko ? { document: "備考欄別紙", content: survey.biko } : null });
      }
    }
    if (pathname === "/api/vital/surveys/export" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const year = Number(searchParams.get("year") ?? new Date().getFullYear());
      const surveys = await readJson(vitalSurveysFile, []);
      const hits = surveys.filter((s) => s.year === year);
      await audit(req, "vital-surveys.export", { year, count: hits.length });
      return reply(res, 200, { kind: "全件出力（該当年に届書受付・調査票作成した事件）", year, count: hits.length, surveys: hits });
    }

    // ── 送付票（新規作成・保存・事件簿番号の自動取得・前回以降の自動出力）──
    if (pathname === "/api/vital/sofuhyo" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const store = await readJson(vitalSofuhyoFile, { counter: 0, lastSentNumber: 0, items: [] });
      const caseNumber = ++store.counter; // 事件簿番号の自動取得
      const item = { id: crypto.randomUUID(), caseNumber, eventType: b.eventType ?? "出生",
        createdAt: new Date().toISOString(), by: actor.id };
      store.items.unshift(item);
      await writeJson(vitalSofuhyoFile, store);
      await audit(req, "vital-sofuhyo.create", { caseNumber });
      return reply(res, 201, { ...item, saved: true });
    }
    if (pathname === "/api/vital/sofuhyo/auto" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const store = await readJson(vitalSofuhyoFile, { counter: 0, lastSentNumber: 0, items: [] });
      const targets = store.items.filter((i) => i.caseNumber > store.lastSentNumber)
        .sort((a, b2) => a.caseNumber - b2.caseNumber);
      const from = store.lastSentNumber + 1;
      store.lastSentNumber = store.counter;
      await writeJson(vitalSofuhyoFile, store);
      await audit(req, "vital-sofuhyo.auto", { from, count: targets.length });
      return reply(res, 200, { kind: "送付票の自動出力", fromCaseNumber: from, count: targets.length, items: targets,
        note: "前回送付票に記載した事件簿番号以降を自動出力" });
    }
    if (pathname === "/api/vital/sofuhyo" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const store = await readJson(vitalSofuhyoFile, { counter: 0, lastSentNumber: 0, items: [] });
      return reply(res, 200, { counter: store.counter, lastSentNumber: store.lastSentNumber, items: store.items.slice(0, 100) });
    }

    // ── 死産票情報の抽出（支所名・死産日・受付日指定）──
    if (pathname === "/api/vital/shizan/extract" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const surveys = await readJson(vitalSurveysFile, []);
      let hits = surveys.filter((s) => s.eventType === "死産");
      if (b.branch) hits = hits.filter((s) => s.branch === b.branch);
      if (b.shizanDate) hits = hits.filter((s) => (s.fields?.shizanDate ?? "") === b.shizanDate);
      if (b.receiptFrom) hits = hits.filter((s) => s.receiptDate >= b.receiptFrom);
      if (b.receiptTo) hits = hits.filter((s) => s.receiptDate <= b.receiptTo);
      await audit(req, "vital-shizan.extract", { hits: hits.length });
      return reply(res, 200, { kind: "死産票情報の抽出", conditions: { branch: b.branch ?? "", shizanDate: b.shizanDate ?? "", receiptFrom: b.receiptFrom ?? "", receiptTo: b.receiptTo ?? "" }, count: hits.length, items: hits });
    }

  return false;
}
