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

// ── 戸籍の附票 証明手数料の算定（標準仕様書: 証明種別×通数。手数料額は手数料条例）──
const FUHYO_FEES = { "戸籍の附票の写し": 300, "戸籍の附票（除票）の写し": 300 };
const FUHYO_POSTAL = 140;
function computeFuhyoFee({ certType = "戸籍の附票の写し", copies = 1, postal = false }, fees = FUHYO_FEES) {
  const unit = fees[certType];
  if (unit === undefined) return { error: "unknown certType", certType };
  const n = Math.max(1, Number(copies) || 1);
  const subtotal = unit * n;
  const postalFee = postal ? FUHYO_POSTAL : 0;
  return { certType, unitFee: unit, copies: n, subtotal, postalFee, total: subtotal + postalFee };
}

// 和暦変換（明治〜令和）。生年月日・住所を定めた年月日・転出予定年月日の出力に使用
const ERAS = [
  { name: "令和", from: "2019-05-01" },
  { name: "平成", from: "1989-01-08" },
  { name: "昭和", from: "1926-12-25" },
  { name: "大正", from: "1912-07-30" },
  { name: "明治", from: "1868-01-25" },
];
function toWareki(iso) {
  if (!/^\d{4}-\d{2}-\d{2}$/.test(String(iso ?? ""))) return "";
  const era = ERAS.find((e) => iso >= e.from);
  if (!era) return String(iso);
  const y = Number(iso.slice(0, 4)) - Number(era.from.slice(0, 4)) + 1;
  return `${era.name}${y === 1 ? "元" : y}年${Number(iso.slice(5, 7))}月${Number(iso.slice(8, 10))}日`;
}

// 行政手続オンライン化法 附則第1条 第9号/第10号 施行日（記載省略の境界。リクエストで上書き可）
const ONLINE_ACT = { no9: "2022-01-11", no10: "2025-05-26" };

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 fuhyoRequestsFile = join(dataRoot, "fuhyo-requests.json");
  const fuhyoSheetsFile = join(dataRoot, "fuhyo-sheets.json");
  const fuhyoJohyoFile = join(dataRoot, "fuhyo-johyo.json");
  const fuhyoNoticesFile = join(dataRoot, "fuhyo-support-notices.json");
  const fuhyoImagesFile = join(dataRoot, "fuhyo-images.json");
  const fuhyoEditsFile = join(dataRoot, "fuhyo-edit-requests.json");
  const fuhyoReplicaFile = join(dataRoot, "fuhyo-replica.json");
  const fuhyoSealFile = join(dataRoot, "fuhyo-seal.json");
  const fuhyoMeasuresFile = join(dataRoot, "fuhyo-support-measures.json");

  // 住所辞書（全国的に提供されるもの。都道府県コードはJIS X 0401 基本データリストに従う）
  const PREF_CODES = {
    "01": "北海道", "02": "青森県", "03": "岩手県", "04": "宮城県", "05": "秋田県", "06": "山形県",
    "07": "福島県", "08": "茨城県", "09": "栃木県", "10": "群馬県", "11": "埼玉県", "12": "千葉県",
    "13": "東京都", "14": "神奈川県", "15": "新潟県", "16": "富山県", "17": "石川県", "18": "福井県",
    "19": "山梨県", "20": "長野県", "21": "岐阜県", "22": "静岡県", "23": "愛知県", "24": "三重県",
    "25": "滋賀県", "26": "京都府", "27": "大阪府", "28": "兵庫県", "29": "奈良県", "30": "和歌山県",
    "31": "鳥取県", "32": "島根県", "33": "岡山県", "34": "広島県", "35": "山口県", "36": "徳島県",
    "37": "香川県", "38": "愛媛県", "39": "高知県", "40": "福岡県", "41": "佐賀県", "42": "長崎県",
    "43": "熊本県", "44": "大分県", "45": "宮崎県", "46": "鹿児島県", "47": "沖縄県",
  };


    // ── 戸籍副本 交付申請 (Fuhyo Certificate Requests) ───────────────────

    const FUHYO_TYPES = ["tohon", "shohon", "jyuki_tohon", "jyuki_shohon", "gaijin_tohon", "identity_proof"];

    if (pathname === "/api/fuhyo-requests" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const requests = (await dbSelect("koseki_fuhyo.fuhyo_request")) ?? await readJson(fuhyoRequestsFile, []);
      const { status, requestType } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      let result = requests;
      if (status) result = result.filter((r) => r.status === status);
      if (requestType) result = result.filter((r) => r.requestType === requestType);
      return reply(res, 200, result);
    }

    if (pathname === "/api/fuhyo-requests" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.applicantName || !body.requestType) return reply(res, 400, { error: "applicantName, requestType required" });
      if (!FUHYO_TYPES.includes(body.requestType)) return reply(res, 400, { error: `requestType must be one of: ${FUHYO_TYPES.join(", ")}` });
      const requests = (await dbSelect("koseki_fuhyo.fuhyo_request")) ?? await readJson(fuhyoRequestsFile, []);
      const receiptNumber = "FH-" + Date.now().toString(36).toUpperCase();
      const request = { id: crypto.randomUUID(), receiptNumber, applicantName: body.applicantName, requestType: body.requestType, purpose: body.purpose ?? "", copies: body.copies ?? 1, fee: body.copies ? body.copies * 450 : 450, status: "received", receivedBy: actor.id, receivedAt: new Date().toISOString() };
      const dbInserted = await dbInsert("koseki_fuhyo.fuhyo_request", request); if (dbInserted === null) { requests.push(request); await writeJson(fuhyoRequestsFile, requests); }
      await audit(req, "fuhyo-request.receive", { id: request.id, requestType: body.requestType });
      return reply(res, 201, request);
    }

    const fuhyoIssueMatch = pathname.match(/^\/api\/fuhyo-requests\/([^/]+)\/issue$/);
    if (fuhyoIssueMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const req2 = (await dbSelectOne("koseki_fuhyo.fuhyo_request", fuhyoIssueMatch[1]))
        ?? (await readJson(fuhyoRequestsFile, [])).find((r) => r.id === fuhyoIssueMatch[1]);
      if (!req2) return reply(res, 404, { error: "request not found" });
      if (req2.status !== "received") return reply(res, 409, { error: "request is not in received status" });
      req2.status = "issued";
      req2.issuedAt = new Date().toISOString();
      req2.issuedBy = actor.id;
      const upd = await dbUpdate("koseki_fuhyo.fuhyo_request", req2.id, req2); if (upd === null) { const wl = await readJson(fuhyoRequestsFile, []); const wi = wl.findIndex((x) => x.id === req2.id); if (wi !== -1) { wl[wi] = req2; } await writeJson(fuhyoRequestsFile, wl); }
      await audit(req, "fuhyo-request.issue", { id: req2.id, requestType: req2.requestType });
      return reply(res, 200, req2);
    }


    const fuhyoMatch = pathname.match(/^\/api\/fuhyo-requests\/([^/]+)$/);
    if (fuhyoMatch && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, "CLERK");
      const id = fuhyoMatch[1];
      const item = (await dbSelectOne("koseki_fuhyo.fuhyo_request", id))
        ?? (await readJson(fuhyoRequestsFile, [])).find((r) => r.id === id);
      if (!item) return reply(res, 404, { error: "fuhyo request not found" });
      if (req.method === "GET") return reply(res, 200, item);

      const body = await parseBody(req);
      const editable = ["applicantName", "requestType", "purpose", "copies"];
      const patch = {};
      for (const k of editable) if (body[k] !== undefined) patch[k] = body[k];
      if (patch.requestType && !FUHYO_TYPES.includes(patch.requestType)) return reply(res, 400, { error: `requestType must be one of: ${FUHYO_TYPES.join(", ")}` });
      if (patch.copies !== undefined) patch.fee = Number(patch.copies) * 450;
      patch.updatedAt = new Date().toISOString();
      patch.updatedBy = actor.id;
      const updated = await dbUpdate("koseki_fuhyo.fuhyo_request", id, patch);
      let result = updated;
      if (updated === null) {
        const items = await readJson(fuhyoRequestsFile, []);
        const idx = items.findIndex((x) => x.id === id);
        items[idx] = { ...items[idx], ...patch };
        await writeJson(fuhyoRequestsFile, items);
        result = items[idx];
      }
      await audit(req, "fuhyo-request.update", { id, fields: Object.keys(patch).filter((k) => k !== "updatedAt" && k !== "updatedBy" && k !== "fee") });
      return reply(res, 200, result);
    }

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


    // ── 戸籍の附票 証明手数料の算定（標準仕様書準拠）──
    if (pathname === "/api/fuhyo/fee" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeFuhyoFee(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "fuhyo.fee-calc", { certType: result.certType, total: result.total });
      return reply(res, 200, result);
    }

    // ════ 附票の改製・除票管理・写し・支援措置通知・住基ネット連携・イメージ（標準仕様書）════

    // ── 附票の登録（履歴上限なし＝満欄による自動改製は行わない）──
    if (pathname === "/api/fuhyo/sheets" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.kosekiId) return reply(res, 400, { error: "kosekiId required" });
      const sheets = await readJson(fuhyoSheetsFile, []);
      const sheet = {
        id: crypto.randomUUID(), kosekiId: String(b.kosekiId),
        honseki: b.honseki ?? "なし", hittousha: b.hittousha ?? "不明", // 『なし』『不明』記載に対応
        addressHistory: Array.isArray(b.addressHistory) ? b.addressHistory : [], // 上限回数を設けない
        henseiDate: b.henseiDate ?? new Date().toISOString().slice(0, 10),
        kaiseiDates: [], status: "現附票", createdBy: actor.id, createdAt: new Date().toISOString(),
      };
      sheets.unshift(sheet);
      await writeJson(fuhyoSheetsFile, sheets);
      await audit(req, "fuhyo-sheet.create", { id: sheet.id });
      return reply(res, 201, sheet);
    }
    // 改製（手動・戸籍再製連動・全員消除）。旧附票は除票化し150年保存
    {
      const kaiseiMatch = pathname.match(/^\/api\/fuhyo\/sheets\/([^/]+)\/kaisei$/);
      if (kaiseiMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const reasons = ["手動", "戸籍再製", "全員消除"];
        const reason = reasons.includes(b.reason) ? b.reason : "手動";
        const sheets = await readJson(fuhyoSheetsFile, []);
        const idx = sheets.findIndex((s) => s.id === kaiseiMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "sheet not found" });
        const today = new Date().toISOString().slice(0, 10);
        const old = sheets[idx];
        // 旧附票 → 除票（消除事由が生じた日から150年保存、経過後廃棄）
        const johyo = await readJson(fuhyoJohyoFile, []);
        const disposal = new Date(today);
        disposal.setFullYear(disposal.getFullYear() + 150);
        johyo.unshift({ ...old, status: "除票", removedAt: today, removeReason: reason,
          kaiseiShojoDate: today, retentionUntil: disposal.toISOString().slice(0, 10) });
        await writeJson(fuhyoJohyoFile, johyo);
        let next = null;
        if (reason !== "全員消除") {
          next = { ...old, id: crypto.randomUUID(), henseiDate: old.henseiDate,
            kaiseiDates: [...old.kaiseiDates, today], kaiseiKisaiDate: today,
            saiseiKisaiDate: reason === "戸籍再製" ? today : old.saiseiKisaiDate ?? "",
            status: "現附票", createdAt: new Date().toISOString() };
          sheets[idx] = next;
        } else {
          sheets.splice(idx, 1);
        }
        await writeJson(fuhyoSheetsFile, sheets);
        await audit(req, "fuhyo-sheet.kaisei", { id: kaiseiMatch[1], reason });
        return reply(res, 200, { reason, johyoCreated: true, retentionYears: 150, next });
      }
    }
    // 除票一覧と保存期間経過分の廃棄
    if (pathname === "/api/fuhyo/johyo" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, (await readJson(fuhyoJohyoFile, [])).slice(0, 300));
    }
    if (pathname === "/api/fuhyo/johyo/purge-expired" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const asOf = (await parseBody(req)).asOf ?? new Date().toISOString().slice(0, 10);
      const johyo = await readJson(fuhyoJohyoFile, []);
      const keep = johyo.filter((j) => j.retentionUntil > asOf);
      const purged = johyo.length - keep.length;
      await writeJson(fuhyoJohyoFile, keep);
      await audit(req, "fuhyo-johyo.purge", { purged, asOf });
      return reply(res, 200, { purged });
    }
    // 写し・除票写し（備考欄に編製/改製記載/改製消除/再製記載年月日を必ず記載）
    if (pathname === "/api/fuhyo/copy" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const id = searchParams.get("id") ?? "";
      const sheets = await readJson(fuhyoSheetsFile, []);
      const johyo = await readJson(fuhyoJohyoFile, []);
      const sheet = sheets.find((s) => s.id === id) ?? johyo.find((j) => j.id === id);
      if (!sheet) return reply(res, 404, { error: "sheet not found" });
      const remarks = [
        `編製年月日: ${sheet.henseiDate ?? ""}`,
        sheet.kaiseiKisaiDate ? `改製記載年月日: ${sheet.kaiseiKisaiDate}` : "",
        sheet.kaiseiShojoDate ? `改製消除年月日: ${sheet.kaiseiShojoDate}` : "",
        sheet.saiseiKisaiDate ? `再製記載年月日: ${sheet.saiseiKisaiDate}` : "",
      ].filter(Boolean);
      await audit(req, "fuhyo-copy.issue", { id, johyo: sheet.status === "除票" });
      return reply(res, 200, { id, kind: sheet.status === "除票" ? "除票の写し" : "附票の写し",
        honseki: sheet.honseki, hittousha: sheet.hittousha, addressHistory: sheet.addressHistory, remarks });
    }

    // ── 支援措置の開始通知・延長通知 ──
    if (pathname === "/api/fuhyo/support-notice" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["開始", "延長"];
      if (!kinds.includes(b.kind) || !b.applicantId) return reply(res, 400, { error: `kind(${kinds.join("/")}), applicantId required` });
      const notices = await readJson(fuhyoNoticesFile, []);
      const notice = { id: crypto.randomUUID(), kind: b.kind, applicantId: String(b.applicantId),
        title: `支援措置${b.kind}通知`, at: new Date().toISOString(), by: actor.id };
      notices.unshift(notice);
      await writeJson(fuhyoNoticesFile, notices);
      await audit(req, "fuhyo-support.notice", { kind: b.kind });
      return reply(res, 201, notice);
    }

    // ── 住基ネット（CSサーバ附票AP）: 受信電文の構成自治体振分・本人確認情報との整合性確認 ──
    if (pathname === "/api/fuhyo/csnet/dispatch" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.messages)) return reply(res, 400, { error: "messages[]{municipalityCode,...} required" });
      const byMunicipality = {};
      for (const m of b.messages) (byMunicipality[m.municipalityCode ?? "不明"] ??= []).push(m);
      const result = Object.entries(byMunicipality).map(([municipalityCode, messages]) => ({ municipalityCode, count: messages.length }));
      await audit(req, "fuhyo-csnet.dispatch", { municipalities: result.length, total: b.messages.length });
      return reply(res, 200, { dispatched: result });
    }
    if (pathname === "/api/fuhyo/csnet/verify" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!Array.isArray(b.csRecords)) return reply(res, 400, { error: "csRecords[]{kosekiId,honseki} required" });
      const sheets = await readJson(fuhyoSheetsFile, []);
      const mismatches = [];
      for (const cs of b.csRecords) {
        const local = sheets.find((s) => s.kosekiId === cs.kosekiId);
        if (!local) mismatches.push({ kosekiId: cs.kosekiId, issue: "本体側に存在しない" });
        else if ((cs.honseki ?? "") !== local.honseki) mismatches.push({ kosekiId: cs.kosekiId, issue: "本籍不一致", cs: cs.honseki, local: local.honseki });
      }
      await audit(req, "fuhyo-csnet.verify", { checked: b.csRecords.length, mismatches: mismatches.length });
      return reply(res, 200, { checked: b.csRecords.length, ok: mismatches.length === 0, mismatches });
    }

    // ── 除票イメージの管理（BMP保持/可逆変換のみ・400dpi・注記等のイメージ処理）──
    if (pathname === "/api/fuhyo/images" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const formats = ["BMP", "TIFF"]; // 可逆形式のみ
      if (!b.johyoId || !formats.includes(b.format)) return reply(res, 400, { error: `johyoId と format(${formats.join("/")}=可逆形式) が必須です` });
      const dpi = Number(b.dpi ?? 400);
      if (dpi < 400 && b.migrated !== true) return reply(res, 400, { error: "解像度は400dpi以上（移行前データのみ現状解像度を許容: migrated=true）" });
      const images = await readJson(fuhyoImagesFile, []);
      const image = { id: crypto.randomUUID(), johyoId: String(b.johyoId), format: b.format, dpi,
        annotations: [], registeredBy: actor.id, registeredAt: new Date().toISOString() };
      images.unshift(image);
      await writeJson(fuhyoImagesFile, images);
      await audit(req, "fuhyo-image.register", { johyoId: image.johyoId, format: image.format, dpi });
      return reply(res, 201, image);
    }
    {
      const annotateMatch = pathname.match(/^\/api\/fuhyo\/images\/([^/]+)\/annotate$/);
      if (annotateMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const kinds = ["文字追加", "線描画", "マーカー"];
        if (!kinds.includes(b.kind)) return reply(res, 400, { error: `kind(${kinds.join("/")}) required` });
        const images = await readJson(fuhyoImagesFile, []);
        const idx = images.findIndex((i) => i.id === annotateMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "image not found" });
        images[idx].annotations.push({ kind: b.kind, value: b.value ?? "", at: new Date().toISOString(), by: actor.id });
        await writeJson(fuhyoImagesFile, images);
        await audit(req, "fuhyo-image.annotate", { id: annotateMatch[1], kind: b.kind });
        return reply(res, 200, images[idx]);
      }
    }

    // ════ 国外転出表記・編集承認・みなし生年月日・振り仮名・住所辞書・スキャナ調整・レプリカDB（標準仕様書）════

    // ── 国外転出者の写し（『住所』→『国外転出先』、『住定日』→『転出予定日』に置換表記）──
    if (pathname === "/api/fuhyo/copy/overseas" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.destination || !b.departureDate) return reply(res, 400, { error: "destination(国外転出先), departureDate(転出予定日) required" });
      await audit(req, "fuhyo-copy.overseas", {});
      return reply(res, 200, {
        kind: "附票の写し（国外転出）",
        labels: { 住所: "国外転出先", 住定日: "転出予定日" },
        国外転出先: String(b.destination), 転出予定日: String(b.departureDate),
      });
    }

    // ── 文字情報の編集（追加・消除）→確認画面→承認のワークフロー ──
    if (pathname === "/api/fuhyo/edits" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const ops = ["追加", "消除", "編集"];
      if (!b.sheetId || !ops.includes(b.op) || !b.field) return reply(res, 400, { error: `sheetId, op(${ops.join("/")}), field required` });
      const edits = await readJson(fuhyoEditsFile, []);
      const edit = { id: crypto.randomUUID(), sheetId: String(b.sheetId), op: b.op, field: String(b.field),
        value: b.value ?? "", status: "承認待ち", requestedBy: actor.id, requestedAt: new Date().toISOString() };
      edits.unshift(edit);
      await writeJson(fuhyoEditsFile, edits);
      await audit(req, "fuhyo-edit.request", { sheetId: edit.sheetId, op: edit.op });
      return reply(res, 201, { ...edit, confirmView: `【確認】${edit.op}: ${edit.field} → ${edit.value}` });
    }
    {
      const approveMatch = pathname.match(/^\/api\/fuhyo\/edits\/([^/]+)\/(approve|reject)$/);
      if (approveMatch && req.method === "POST") {
        requireRole(actor, "ADMIN"); // 承認機能
        const edits = await readJson(fuhyoEditsFile, []);
        const idx = edits.findIndex((e) => e.id === approveMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "edit request not found" });
        edits[idx].status = approveMatch[2] === "approve" ? "承認済" : "却下";
        edits[idx].decidedBy = actor.id;
        edits[idx].decidedAt = new Date().toISOString();
        if (edits[idx].status === "承認済") {
          const sheets = await readJson(fuhyoSheetsFile, []);
          const sheet = sheets.find((s) => s.id === edits[idx].sheetId);
          if (sheet && edits[idx].op !== "消除") sheet[edits[idx].field] = edits[idx].value;
          else if (sheet) delete sheet[edits[idx].field];
          await writeJson(fuhyoSheetsFile, sheets);
        }
        await writeJson(fuhyoEditsFile, edits);
        await audit(req, "fuhyo-edit.decide", { id: approveMatch[1], status: edits[idx].status });
        return reply(res, 200, edits[idx]);
      }
    }

    // ── みなし生年月日の作成（不詳部分の補完: 年のみ→7月1日、年月のみ→当月15日）──
    if (pathname === "/api/fuhyo/minashi-birthdate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const partial = String(b.partial ?? "").trim();
      let minashi;
      if (/^\d{4}$/.test(partial)) minashi = `${partial}-07-01`;
      else if (/^\d{4}-\d{2}$/.test(partial)) minashi = `${partial}-15`;
      else if (/^\d{4}-\d{2}-\d{2}$/.test(partial)) minashi = partial;
      else return reply(res, 400, { error: "partial は YYYY / YYYY-MM / YYYY-MM-DD のいずれか" });
      await audit(req, "fuhyo.minashi-birthdate", { partial });
      return reply(res, 200, { partial, minashiBirthDate: minashi, isMinashi: partial !== minashi });
    }

    // ── 氏名振り仮名の検査（撥音「ン」・促音「ッ」を区別して保持。小書きを大書きに丸めない）──
    if (pathname === "/api/fuhyo/kana/validate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kana = String(b.kana ?? "");
      if (!kana) return reply(res, 400, { error: "kana required" });
      const valid = /^[ァ-ヶー]+$/.test(kana);
      const hasSokuon = kana.includes("ッ");
      const hasHatsuon = kana.includes("ン");
      return reply(res, 200, { kana, valid, hasSokuon, hasHatsuon,
        note: "促音（ッ）と大書き（ツ）、撥音（ン）を区別して保持します" });
    }

    // ── 住所辞書（都道府県コード＝基本データリスト準拠）──
    if (pathname === "/api/fuhyo/address-dict" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const code = searchParams.get("prefCode") ?? "";
      if (code) {
        const name = PREF_CODES[code];
        if (!name) return reply(res, 404, { error: "unknown prefCode（JIS X 0401 2桁）" });
        return reply(res, 200, { prefCode: code, name });
      }
      return reply(res, 200, { count: Object.keys(PREF_CODES).length, dictionary: PREF_CODES });
    }

    // ── スキャナ調整（濃度・回転・読取位置）──
    {
      const adjustMatch = pathname.match(/^\/api\/fuhyo\/images\/([^/]+)\/adjust$/);
      if (adjustMatch && req.method === "POST") {
        requireRole(actor, "CLERK");
        const b = await parseBody(req);
        const images = await readJson(fuhyoImagesFile, []);
        const idx = images.findIndex((i) => i.id === adjustMatch[1]);
        if (idx === -1) return reply(res, 404, { error: "image not found" });
        const rotation = [0, 90, 180, 270].includes(Number(b.rotation)) ? Number(b.rotation) : 0;
        images[idx].scanAdjust = {
          density: Math.min(Math.max(Number(b.density ?? 50), 0), 100),
          rotation,
          position: { x: Number(b.x ?? 0), y: Number(b.y ?? 0) },
          at: new Date().toISOString(), by: actor.id,
        };
        await writeJson(fuhyoImagesFile, images);
        await audit(req, "fuhyo-image.adjust", { id: adjustMatch[1], rotation });
        return reply(res, 200, images[idx]);
      }
    }

    // ── 他システム（戸籍情報システム等）でのDB構築時の最新データ保持・画面確認 ──
    if (pathname === "/api/fuhyo/replica/sync" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const sheets = await readJson(fuhyoSheetsFile, []);
      const replica = { lastSyncedAt: new Date().toISOString(), records: sheets.length, host: "戸籍情報システム（外部DB）", by: actor.id };
      await writeJson(fuhyoReplicaFile, replica);
      await audit(req, "fuhyo-replica.sync", { records: replica.records });
      return reply(res, 200, replica);
    }
    if (pathname === "/api/fuhyo/replica/status" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const replica = await readJson(fuhyoReplicaFile, {});
      return reply(res, 200, { ...replica, fresh: !!replica.lastSyncedAt,
        note: "外部DB構築時も最新データ保持状況を端末画面で確認できる" });
    }

    // ════ 写し記載制御・レイアウト・電子公印・支援措置延長・運用系（標準仕様書）════

    // ── 写しの印刷（和暦出力・施行日以前消除者の記載省略・特別請求での選択・全ページ印字・レイアウト・改ざん防止）──
    if (pathname === "/api/fuhyo/copy/print" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const persons = Array.isArray(b.persons) ? b.persons : [];
      if (persons.length === 0) return reply(res, 400, { error: "persons[]{name,birthDate,gender,jukiCode,removedDate?} required" });
      const acts = { ...ONLINE_ACT, ...(b.effectiveDates ?? {}) };
      const special = b.specialRequest === true; // 特別請求・必要申出→省略項目も記載
      const entries = persons.map((p) => {
        const removed = p.removedDate ?? "";
        const omitProfile = !!removed && removed <= acts.no9 && !special; // 附則9号施行日以前消除→性別・生年月日省略
        const omitJuki = !!removed && removed <= acts.no10 && !special; // 附則10号施行日以前消除→住民票コード省略
        return {
          name: `［氏名］ ${p.name ?? ""}`,
          birthDate: omitProfile ? "（記載省略）" : `［生年月日］ ${toWareki(p.birthDate)}`,
          gender: omitProfile ? "（記載省略）" : `［性別］ ${p.gender ?? ""}`,
          jukiCode: omitJuki ? "（記載省略）" : `［住民票コード］ ${p.jukiCode ?? ""}`,
          separator: "－－－－－－－－（対象者ごとに破線の罫線で区分）",
        };
      });
      const perPage = Math.max(1, Number(b.personsPerPage ?? 4));
      const totalPages = Math.ceil(entries.length / perPage); // 対象者（事象）単位でページに割り当て＝事象途中の改ページなし
      const issuedAt = new Date().toISOString().slice(0, 10);
      const pages = Array.from({ length: totalPages }, (_, i) => ({
        page: i + 1,
        persons: entries.slice(i * perPage, (i + 1) * perPage),
        footer: { 発行年月日: toWareki(issuedAt), 市区町村名: b.municipality ?? "○○市", 発行端末番号: b.terminalNo ?? "T-001",
          発行順番号: b.serialNo ?? 1, ページ番号: i + 1, 総ページ数: totalPages },
      }));
      const doc = { kind: "附票の写し（印刷）", wareki: true,
        layout: { itemLabel: "項目名は［ ］で囲み横書き左右中央揃え", pageBreak: "事象単位の途中で改ページしない" }, pages };
      const json = JSON.stringify(doc);
      const hash = crypto.createHash
        ? crypto.createHash("sha256").update(json).digest("hex")
        : Array.from(json).reduce((h, c) => ((h * 31 + c.charCodeAt(0)) >>> 0), 0).toString(16);
      await audit(req, "fuhyo-copy.print", { persons: persons.length, pages: totalPages, special });
      return reply(res, 200, { ...doc, tamperProof: { algorithm: crypto.createHash ? "SHA-256" : "checksum", hash, writeProtected: true } });
    }

    // ── 総住第136号レイアウトでの転記出力 ──
    if (pathname === "/api/fuhyo/copy/soju136" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const sheets = await readJson(fuhyoSheetsFile, []);
      const sheet = sheets.find((s) => s.id === b.id);
      if (!sheet) return reply(res, 404, { error: "sheet not found" });
      await audit(req, "fuhyo-copy.soju136", { id: sheet.id });
      return reply(res, 200, { form: "総住第136号", kind: "戸籍の附票の写し（転記出力）",
        transcribed: { 本籍: sheet.honseki, 筆頭者: sheet.hittousha, 住所履歴: sheet.addressHistory } });
    }

    // ── 電子公印の設定（職務代理者・氏名空欄『○○長 公印』・公印種類・固定文言・指定都市の県名省略）──
    if (pathname === "/api/fuhyo/seal" && (req.method === "GET" || req.method === "PUT")) {
      requireRole(actor, req.method === "PUT" ? "ADMIN" : "CLERK");
      const defaults = { officialTitle: "○○市長", name: "", isDeputy: false, sealEnabled: true,
        sealKind: "電子公印", fixedNote: "この印は黒色です", isSeireiShitei: false, prefName: "○○県" };
      let conf = { ...defaults, ...(await readJson(fuhyoSealFile, {})) };
      if (req.method === "PUT") {
        const b = await parseBody(req);
        const kinds = ["電子公印", "朱肉公印", "印影なし"];
        if (b.sealKind !== undefined && !kinds.includes(b.sealKind)) return reply(res, 400, { error: `sealKind(${kinds.join("/")})` });
        conf = { ...conf, ...b, updatedBy: actor.id, updatedAt: new Date().toISOString() };
        await writeJson(fuhyoSealFile, conf);
        await audit(req, "fuhyo-seal.update", { sealKind: conf.sealKind });
      }
      const title = conf.isDeputy ? `${conf.officialTitle}職務代理者` : conf.officialTitle;
      const issuerLine = [conf.isSeireiShitei ? "" : conf.prefName, title, conf.name || "公印"].filter(Boolean).join(" ");
      return reply(res, 200, { ...conf, issuerLine,
        fixedNoteApplied: conf.sealEnabled && conf.sealKind === "電子公印" ? conf.fixedNote : "" });
    }

    // ── 支援措置の延長処理（延長前終了日の翌日から起算して1年間）──
    if (pathname === "/api/fuhyo/support-measures/extend" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.applicantId || !/^\d{4}-\d{2}-\d{2}$/.test(String(b.currentEndDate ?? ""))) {
        return reply(res, 400, { error: "applicantId, currentEndDate(YYYY-MM-DD) required" });
      }
      const start = new Date(b.currentEndDate + "T00:00:00Z");
      start.setUTCDate(start.getUTCDate() + 1);
      const end = new Date(start);
      end.setUTCFullYear(end.getUTCFullYear() + 1);
      end.setUTCDate(end.getUTCDate() - 1);
      const measures = await readJson(fuhyoMeasuresFile, []);
      const measure = { id: crypto.randomUUID(), applicantId: String(b.applicantId), kind: "延長",
        newStartDate: start.toISOString().slice(0, 10), newEndDate: end.toISOString().slice(0, 10),
        extendedBy: actor.id, extendedAt: new Date().toISOString() };
      measures.unshift(measure);
      await writeJson(fuhyoMeasuresFile, measures);
      await audit(req, "fuhyo-support.extend", { applicantId: measure.applicantId });
      return reply(res, 201, measure);
    }

    // ── マニュアル（オンライン＝画面別ヘルプ／冊子＝印刷用全章）──
    if (pathname === "/api/fuhyo/manual" && req.method === "GET") {
      const MANUAL = {
        domain: { title: "附票管理", body: "附票の編製・改製・除票・写し交付の操作手順。" },
        reports: { title: "帳票", body: "写し・除票写し・総住第136号転記の出力手順。" },
        admin: { title: "管理", body: "公印設定・メンテナンス・契約終了時データ提供の運用手順。" },
        online: { title: "オンライン申請", body: "交付申請の受付・審査・交付の操作手順。" },
      };
      const screen = searchParams.get("screen") ?? "";
      if (screen) {
        const page = MANUAL[screen];
        if (!page) return reply(res, 404, { error: "unknown screen" });
        return reply(res, 200, { screen, ...page, kind: "オンラインマニュアル（操作画面からの画面別ヘルプ）" });
      }
      const format = searchParams.get("format") ?? "online";
      return reply(res, 200, { kind: format === "booklet" ? "冊子マニュアル（印刷用・全章）" : "オンラインマニュアル（目次）",
        chapters: Object.entries(MANUAL).map(([k, v]) => ({ screen: k, title: v.title })) });
    }

    // ── 契約期間終了時のデータ提供（その時点のデータ要件標準に従う）──
    if (pathname === "/api/fuhyo/export/standard" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const sheets = await readJson(fuhyoSheetsFile, []);
      const johyo = await readJson(fuhyoJohyoFile, []);
      await audit(req, "fuhyo.export-standard", { sheets: sheets.length, johyo: johyo.length });
      return reply(res, 200, { kind: "契約期間終了時のデータ提供", standard: "データ要件・連携要件標準仕様（提供時点の最新版）",
        formats: ["CSV", "JSON"], datasets: [
          { name: "附票", records: sheets.length },
          { name: "附票の除票", records: johyo.length },
        ] });
    }

    // ── 個別・一括の各種制御・メンテナンス ──
    if (pathname === "/api/fuhyo/maintenance" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const modes = ["個別", "一括"];
      if (!modes.includes(b.mode) || !b.target) return reply(res, 400, { error: `mode(${modes.join("/")}), target required` });
      const ids = b.mode === "一括" ? (Array.isArray(b.ids) ? b.ids : []) : [b.id].filter(Boolean);
      if (ids.length === 0) return reply(res, 400, { error: b.mode === "一括" ? "ids[] required" : "id required" });
      await audit(req, "fuhyo.maintenance", { mode: b.mode, target: String(b.target), count: ids.length });
      return reply(res, 200, { mode: b.mode, target: String(b.target), processed: ids.length, status: "完了" });
    }

    // ── シングルサインオン（統合認証基盤発行のヘッダを受け入れてセッション確立）──
    if (pathname === "/api/fuhyo/sso/session" && req.method === "POST") {
      const ssoUser = req.headers["x-sso-user"];
      if (!ssoUser) return reply(res, 401, { error: "x-sso-user header required（統合認証基盤発行）" });
      await audit(req, "fuhyo.sso-login", { user: String(ssoUser) });
      return reply(res, 200, { kind: "シングルサインオン", user: String(ssoUser),
        provider: "統合認証基盤（庁内SSO）", sessionId: crypto.randomUUID() });
    }

    // ── 戸籍附票システム改造仕様書への準拠（版数表示＋受信電文の仕様書検証）──
    if (pathname === "/api/fuhyo/csnet/spec" && req.method === "GET") {
      requireRole(actor, "CLERK");
      return reply(res, 200, { spec: "戸籍附票システム改造仕様書", version: "第3.0版",
        processes: ["受信電文の構成自治体振分", "本人確認情報との整合性確認", "符号要求送信の確認"] });
    }
    if (pathname === "/api/fuhyo/csnet/spec/validate" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const required = ["municipalityCode", "messageType", "kosekiId"];
      const messages = Array.isArray(b.messages) ? b.messages : [];
      const errors = messages.flatMap((m, i) => required.filter((k) => !m[k]).map((k) => ({ index: i, missing: k })));
      await audit(req, "fuhyo-csnet.spec-validate", { checked: messages.length, errors: errors.length });
      return reply(res, 200, { spec: "戸籍附票システム改造仕様書", checked: messages.length, ok: errors.length === 0, errors });
    }

  return false;
}
