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

// ── 印鑑登録 手数料の算定（標準仕様書: 交付種別×通数。手数料額は条例パラメータ）──
const INKAN_FEES = { "印鑑登録証明書": 300, "印鑑登録": 300, "印鑑登録証再交付": 200 };
const INKAN_POSTAL = 140;
function computeInkanFee({ certType, copies = 1, postal = false }, fees = INKAN_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 ? INKAN_POSTAL : 0;
  return { certType, unitFee: unit, copies: n, subtotal, postalFee, total: subtotal + postalFee };
}

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 sealsFile = join(dataRoot, "seals.json");


    // ── 印鑑登録 (Seal Registration) ─────────────────────────────────────

    if (pathname === "/api/seals" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const seals = (await dbSelect("inkan.seal")) ?? await readJson(sealsFile, []);
      const restrictions = await readJson(restrictionsFile, []);
      return reply(res, 200, seals.map((s) => maskRecord(actor, restrictions, s)));
    }

    if (pathname === "/api/seals" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.ownerName || !body.birthDate || !body.address) return reply(res, 400, { error: "ownerName, birthDate, address required" });
      const seals = (await dbSelect("inkan.seal")) ?? await readJson(sealsFile, []);
      const registrationNumber = "IS-" + Date.now().toString(36).toUpperCase();
      const seal = { id: crypto.randomUUID(), registrationNumber, ownerName: body.ownerName, birthDate: body.birthDate, address: body.address, sealDescription: body.sealDescription ?? "", status: "registered", registeredAt: new Date().toISOString(), createdBy: actor.id };
      const dbInserted = await dbInsert("inkan.seal", seal); if (dbInserted === null) { seals.push(seal); await writeJson(sealsFile, seals); }
      await audit(req, "seal.register", { id: seal.id, registrationNumber });
      return reply(res, 201, seal);
    }

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

    const sealCancelMatch = pathname.match(/^\/api\/seals\/([^/]+)\/cancel$/);
    if (sealCancelMatch && req.method === "POST") {
      requireRole(actor, "CLERK");
      const seal = (await dbSelectOne("inkan.seal", sealCancelMatch[1]))
        ?? (await readJson(sealsFile, [])).find((s) => s.id === sealCancelMatch[1]);
      if (!seal) return reply(res, 404, { error: "seal not found" });
      if (seal.status === "cancelled") return reply(res, 409, { error: "already cancelled" });
      seal.status = "cancelled";
      seal.cancelledAt = new Date().toISOString();
      seal.cancelledBy = actor.id;
      const upd = await dbUpdate("inkan.seal", seal.id, seal); if (upd === null) { const wl = await readJson(sealsFile, []); const wi = wl.findIndex((x) => x.id === seal.id); if (wi !== -1) { wl[wi] = seal; } await writeJson(sealsFile, wl); }
      await audit(req, "seal.cancel", { id: seal.id });
      return reply(res, 200, seal);
    }

    // ── 印鑑証明書 (Seal Certificates) ───────────────────────────────────

    if (pathname === "/api/certificates" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const certs = (await dbSelect("inkan.certificate")) ?? await readJson(certificatesFile, []);
      const { sealId } = Object.fromEntries(new URL(req.url, "http://x").searchParams);
      return reply(res, 200, sealId ? certs.filter((c) => c.sealId === sealId) : certs);
    }

    if (pathname === "/api/certificates" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      if (!body.sealId) return reply(res, 400, { error: "sealId required" });
      const seal = (await dbSelectOne("inkan.seal", body.sealId))
        ?? (await readJson(sealsFile, [])).find((s) => s.id === body.sealId);
      if (!seal) return reply(res, 404, { error: "seal not found" });
      if (seal.status !== "registered") return reply(res, 409, { error: "seal is not in registered status" });
      const certs = (await dbSelect("inkan.certificate")) ?? await readJson(certificatesFile, []);
      const certificateNumber = "IC-" + Date.now().toString(36).toUpperCase();
      const cert = { id: crypto.randomUUID(), certificateNumber, sealId: body.sealId, ownerName: seal.ownerName, issuedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 3 * 30 * 24 * 60 * 60 * 1000).toISOString(), purpose: body.purpose ?? "", issuedBy: actor.id };
      certs.push(cert);
      await writeJson(certificatesFile, certs);
      await audit(req, "certificate.issue", { id: cert.id, certificateNumber });
      return reply(res, 201, cert);
    }


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

    // ── コンビニ交付（juki水準: J-LIS連携 + 抑止連動）──
    if (pathname === "/api/certificates/conveni/status" && req.method === "GET") {
      requireRole(actor, "CLERK");
      const certs = (await dbSelect("inkan.certificate")) ?? await readJson(certificatesFile, []);
      const conv = certs.filter((c) => c.channel === "convenience");
      return reply(res, 200, {
        partner: "J-LIS / 自治体中間サーバ",
        linkState: "CONNECTED",
        serviceHours: "6:30-23:00",
        checkedAt: new Date().toISOString(),
        totals: { total: conv.length, issued: conv.filter((c) => c.status === "issued").length, refused: conv.filter((c) => c.status === "refused").length },
      });
    }

    if (pathname === "/api/certificates/conveni" && req.method === "POST") {
      // 印鑑登録証明書のコンビニ交付。支援措置・抑止対象は REFUSED。
      const body = await parseBody(req);
      if (!body.sealId) return reply(res, 400, { error: "sealId required" });
      const seal = (await dbSelectOne("inkan.seal", body.sealId)) ?? (await readJson(sealsFile, [])).find((s) => s.id === body.sealId);
      if (!seal) return reply(res, 404, { error: "seal not found" });
      const restrictions = await readJson(restrictionsFile, []);
      const restricted = restrictions.some((r) => r.status !== "inactive" && r.recordId === body.sealId);
      const certs = (await dbSelect("inkan.certificate")) ?? await readJson(certificatesFile, []);
      const cert = { id: crypto.randomUUID(), sealId: body.sealId, ownerName: seal.ownerName, channel: "convenience", storeCode: body.storeCode ?? "JLIS-STORE", requestedAt: new Date().toISOString() };
      if (restricted || seal.status !== "registered") {
        cert.status = "refused";
        cert.reason = restricted ? "支援措置・抑止対象のためコンビニ交付を停止しています。" : "登録状態でないため交付できません。";
      } else {
        cert.status = "issued";
        cert.certificateNumber = "IC-" + Date.now().toString(36).toUpperCase();
      }
      certs.push(cert);
      await writeJson(certificatesFile, certs);
      await audit(req, "certificate.conveni", { id: cert.id, sealId: body.sealId, status: cert.status });
      return reply(res, cert.status === "issued" ? 201 : 200, cert);
    }


    // ── 印鑑登録 手数料の算定（標準仕様書準拠）──
    if (pathname === "/api/inkan/fee" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const body = await parseBody(req);
      const result = computeInkanFee(body || {});
      if (result.error) return reply(res, 400, result);
      await audit(req, "inkan.fee-calc", { certType: result.certType, total: result.total });
      return reply(res, 200, result);
    }

    // ════ 印影イメージ・可視台帳・発行番号・指定都市・整合性・経年抹消・バーコード・運用系（標準仕様書）════

    const inkanImprintsFile = join(dataRoot, "inkan-imprints.json");
    const inkanCountersFile = join(dataRoot, "inkan-issue-counters.json");

    // ── 印影イメージの登録（BMP保持または可逆変換のみ・バイナリ構造保持）──
    if (pathname === "/api/inkan/imprints" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const formats = ["BMP", "TIFF"]; // 可逆形式のみ（JPEG等の非可逆は不可）
      if (!b.atenaId || !formats.includes(b.format)) return reply(res, 400, { error: `atenaId, format(${formats.join("/")}=可逆) required` });
      const imprints = await readJson(inkanImprintsFile, []);
      const item = { id: crypto.randomUUID(), atenaId: String(b.atenaId), format: b.format,
        binaryStructure: "BMP構造保持（TIFFはBMPへ可逆変換可）", registeredBy: actor.id, registeredAt: new Date().toISOString() };
      imprints.unshift(item);
      await writeJson(inkanImprintsFile, imprints);
      await audit(req, "inkan-imprint.register", { atenaId: item.atenaId, format: item.format });
      return reply(res, 201, item);
    }

    // ── 可視台帳（押印前）・押印用申請書の出力（項目名=中央揃え・項目内容=左揃え）──
    if (pathname === "/api/inkan/documents" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const kinds = ["可視台帳", "押印申請書"];
      if (!b.atenaId || !kinds.includes(b.kind)) return reply(res, 400, { error: `atenaId, kind(${kinds.join("/")}) required` });
      await audit(req, "inkan-documents.print", { atenaId: b.atenaId, kind: b.kind });
      return reply(res, 200, { kind: b.kind, atenaId: String(b.atenaId),
        imprintArea: b.kind === "押印申請書" ? "住民が印影を押下する欄あり" : "押印前（印影未貼付）",
        layout: { itemLabel: "横書き・左右上下中央揃え", itemValue: "横書き・左揃え・上下中央揃え" } });
    }

    // ── 証明書発行番号（発行場所単位の番号部＋複数部数は1部ずつ異なる番号）──
    if (pathname === "/api/inkan/certificates/issue-numbers" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const copies = Math.max(1, Number(b.copies) || 1);
      if (!b.locationCode) return reply(res, 400, { error: "locationCode（発行場所単位の番号）required" });
      const counters = await readJson(inkanCountersFile, {});
      const key = String(b.locationCode);
      let seq = counters[key] ?? 0;
      const numbers = Array.from({ length: copies }, () => `${key}-${String(++seq).padStart(6, "0")}`);
      counters[key] = seq;
      await writeJson(inkanCountersFile, counters);
      await audit(req, "inkan-cert.issue-numbers", { locationCode: key, copies });
      return reply(res, 200, { locationCode: key, copies, numbers, distinct: new Set(numbers).size === copies });
    }

    // ── 指定都市: 他区長の職名・公印の印字 ──
    if (pathname === "/api/inkan/certificates/ward-seal" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      if (!b.city || !b.ward) return reply(res, 400, { error: "city, ward required" });
      await audit(req, "inkan-cert.ward-seal", { ward: b.ward });
      return reply(res, 200, { issuerTitle: `${b.city}${b.ward}区長`, sealPrinted: true,
        note: "指定都市は交付区以外の他区長名・公印でも印字可能" });
    }

    // ── 指定都市: 区ごとのリスト出力 ──
    if (pathname === "/api/inkan/lists/by-ward" && 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[]{atenaId,ward} required" });
      const byWard = {};
      for (const r of rows) (byWard[r.ward ?? "不明"] ??= []).push(r);
      await audit(req, "inkan-lists.by-ward", { wards: Object.keys(byWard).length });
      return reply(res, 200, { wards: Object.entries(byWard).map(([ward, items]) => ({ ward, count: items.length, items })) });
    }

    // ── 異常終了後の住民記録システムとの紐付き整合性チェック ──
    if (pathname === "/api/inkan/juki/consistency-check" && req.method === "POST") {
      requireRole(actor, "ADMIN");
      const b = await parseBody(req);
      const inkanRecords = Array.isArray(b.inkanRecords) ? b.inkanRecords : [];
      const jukiRecords = Array.isArray(b.jukiRecords) ? b.jukiRecords : [];
      const jukiMap = new Map(jukiRecords.map((j) => [String(j.atenaId), j]));
      const issues = [];
      for (const r of inkanRecords) {
        const juki = jukiMap.get(String(r.atenaId));
        if (!juki) issues.push({ atenaId: r.atenaId, issue: "住民記録に存在しない登録" });
        else if (juki.status === "転出" && r.status === "registered") issues.push({ atenaId: r.atenaId, issue: "転出者の登録が継続" });
      }
      await audit(req, "inkan-juki.consistency", { checked: inkanRecords.length, issues: issues.length });
      return reply(res, 200, { trigger: b.trigger ?? "異常終了後の起動時", checked: inkanRecords.length,
        ok: issues.length === 0, issues });
    }

    // ── 除票経年抹消対象リストの出力 ──
    if (pathname === "/api/inkan/johyo/expired-list" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const records = Array.isArray(b.records) ? b.records : [];
      if (!records.length) return reply(res, 400, { error: "records[]{atenaId,removedAt} required" });
      const years = Number(b.retentionYears ?? 5);
      const asOf = b.asOf ?? new Date().toISOString().slice(0, 10);
      const targets = records.filter((r) => {
        if (!r.removedAt) return false;
        const limit = r.removedAt.replace(/^(\d{4})/, (y) => String(Number(y) + years));
        return limit <= asOf;
      }).map((r) => ({ atenaId: r.atenaId, removedAt: r.removedAt, status: "経年抹消対象" }));
      await audit(req, "inkan-johyo.expired-list", { targets: targets.length, years });
      return reply(res, 200, { asOf, retentionYears: years, count: targets.length, list: targets });
    }

    // ── カスタマバーコード（郵便番号＋住所表示番号から生成）──
    if (pathname === "/api/inkan/mail/customer-barcode" && req.method === "POST") {
      requireRole(actor, "CLERK");
      const b = await parseBody(req);
      const postal = String(b.postalCode ?? "").replace(/-/g, "");
      if (!/^\d{7}$/.test(postal)) return reply(res, 400, { error: "postalCode(7桁) required" });
      const addrDigits = String(b.address ?? "").replace(/[^0-9０-９-]/g, "").replace(/[０-９]/g, (c) => String("０１２３４５６７８９".indexOf(c)));
      const code = `${postal}${addrDigits}`.slice(0, 20);
      await audit(req, "inkan-mail.customer-barcode", {});
      return reply(res, 200, { customerBarcode: code, symbology: "郵便カスタマバーコード（郵便番号7桁＋住所表示番号）",
        printedOn: "宛名面" });
    }

    // ── マニュアル（オンライン＝画面別ヘルプ／冊子）──
    if (pathname === "/api/inkan/manual" && req.method === "GET") {
      const MANUAL = {
        domain: { title: "印鑑登録管理", body: "登録・廃止・証明書交付・コンビニ交付の操作手順。" },
        reports: { title: "帳票", body: "可視台帳・押印申請書・区別リストの出力手順。" },
        admin: { title: "管理", body: "整合性チェック・経年抹消・ログ保全の運用手順。" },
      };
      const screen = searchParams.get("screen") ?? "";
      if (screen) {
        const page = MANUAL[screen];
        if (!page) return reply(res, 404, { error: "unknown screen" });
        return reply(res, 200, { screen, ...page, kind: "オンラインマニュアル（画面別ヘルプ）" });
      }
      const format = searchParams.get("format") ?? "online";
      return reply(res, 200, { kind: format === "booklet" ? "冊子マニュアル（印刷用・全章）" : "オンラインマニュアル（目次）",
        chapters: Object.entries(MANUAL).map(([k, v]) => ({ screen: k, title: v.title })) });
    }

    // ── シングルサインオン ──
    if (pathname === "/api/inkan/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, "inkan.sso-login", { user: String(ssoUser) });
      return reply(res, 200, { kind: "シングルサインオン", user: String(ssoUser),
        provider: "統合認証基盤（庁内SSO）", sessionId: crypto.randomUUID() });
    }

    // ── ログの改ざん防止（追記専用＋SHA-256チェーンの検証）──
    if (pathname === "/api/inkan/logs/integrity" && req.method === "POST") {
      requireRole(actor, "AUDITOR");
      const b = await parseBody(req);
      const entries = Array.isArray(b.entries) ? b.entries : [];
      let chain = "GENESIS";
      const sha = (s) => crypto.createHash
        ? crypto.createHash("sha256").update(s).digest("hex")
        : Array.from(s).reduce((h, c) => ((h * 31 + c.charCodeAt(0)) >>> 0), 0).toString(16);
      for (const e of entries) chain = sha(chain + JSON.stringify(e));
      return reply(res, 200, { writeProtected: true, entries: entries.length, chainHash: chain,
        measures: ["追記専用NDJSON（上書き不可）", "SHA-256ハッシュチェーン", "書込権限の分離（AUDITORは読取のみ）"] });
    }

  return false;
}
