// TrackContactPicker — トラックの先方担当を画面から付け替える(K-28・0117 track.contact)。
// ==================================================================
// 置き場の知識はサーバが持つ(0117【設計判断1】): 画面は「このトラックの先方担当(業者側/保険会社側)をこの人にする」
// だけを track.contact で送る。業者側は track_relations、保険会社側は insurance_claims.insurer_contact_id に入る。
//
// なぜ差分(mstore-diff.js)を通さないか:
//   先方担当は案件の state に入っていない(店のメモリ MStore.trackContacts にだけ控える・K18-20)。案件の state に
//   足すと localStorage の控えに写り、案件単位の関係者(parties)と取り違えやすい(docs/05 §7 罠 #38)。なので
//   下請手配(mstore-adapter.js logDispatch)と同じく、書き込みキューへ op を直接 1 つ積む。キューは案件ごとに直列で、
//   版(expectedVersion)はキューが知っている最新を使う(差分の保存と追い越さない)。
//
// 画面の流れ:
//   ① 店の控えを先に差し替える(押した直後から新しい担当が見える・TODO 詳細の宛先の初期値も変わる)
//   ② track.contact を積み、キューが空くまで待つ
//   ③ サーバに残らなかった(拒否・翻訳不能)なら控えを元に戻し、理由を返す。競合(409)は書き込み層が案件を読み直して
//      赤帯を出すので、控えも戻す(もう一度選び直してもらう)。
//
// 案件の関係者(c.parties)には足さない・読まない(罠 #38)。担当者は取引先の担当者(org_contacts)から選ぶ:
//   候補は rpc_list_org_contacts(0117)。会社(業者 = 今の担当の所属か選んだ業者 / 保険会社)の担当者を名前順に出し、
//   名前で探すと自テナント全体から探す(担当者の会社がトラックの業者と食い違う 52 件 ─ docs/18 E-17 ─ を直せるように)。

const TCP_SIDE_LABEL = { "業者": "業者側", "保険会社": "保険会社側" };
const tcpText = v => (typeof v === "string" ? v.trim() : "");

/* 店の控えが変わったことを、先方担当を出している部品へ知らせる(店の update() とは別。控えは案件の state ではない)。 */
const tcpListeners = new Set();
function tcpNotify() { tcpListeners.forEach(f => { try { f(); } catch (_) {} }); }
function useTrackContactsTick() {
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    const f = () => setTick(n => n + 1);
    tcpListeners.add(f);
    return () => { tcpListeners.delete(f); };
  }, []);
}

/* 書き込み層の件数の控え(Screens.jsx の scWriteSnap と同じ形)。 */
function tcpWriteSnap(W) {
  if (!W || typeof W.status !== "function") return null;
  const s = W.status() || {};
  return { failed: (s.failed || []).length, unsupported: (s.unsupported || []).length,
           conflicts: Number(s.conflicts) || 0, blocked: Number(s.blocked) || 0 };
}
/* 控え(before)からの変化で成否を読む(Screens.jsx の scAwaitWrite と同じ読み方)。 */
async function tcpAwaitWrite(W, before, timeout) {
  if (!W || !before) return { ok: true };
  const idle = typeof W.idle === "function" ? Promise.resolve(W.idle()) : Promise.resolve();
  const timedOut = await Promise.race([idle.then(() => false), new Promise(r => setTimeout(() => r(true), timeout))]);
  const s = (typeof W.status === "function" && W.status()) || {};
  const after = tcpWriteSnap(W) || before;
  const last = arr => (Array.isArray(arr) && arr.length) ? arr[arr.length - 1] : null;
  if (after.failed > before.failed) {
    const f = last(s.failed);
    return { ok: false, lost: true, reason: "サーバに拒否されました" + (f && f.message ? "（" + f.message + "）" : "") + "。先方担当は変わっていません。" };
  }
  if (after.conflicts > before.conflicts) {
    return { ok: false, lost: true, reason: "他の人が先に更新していたため、この案件を読み直しました。もう一度選び直してください。" };
  }
  if (after.blocked > 0) {
    return { ok: false, lost: false, reason: "認証の期限が切れています。ログインし直すと、この付け替えは自動で送られます。" };
  }
  if (timedOut || (Number(s.pending) || 0) > 0) {
    return { ok: false, lost: false, reason: "保存がまだ終わっていません（送信待ち）。届くまでお待ちください。" };
  }
  return { ok: true };
}

/** 先方担当を付け替える(contact が null なら外す)。戻り値 { ok, lost?, reason? }。
 *  contact は rpc_list_org_contacts の行({ id, name, title, phone, orgId, orgName })。 */
async function saveTrackContact({ caseId, trackDbId, side, contact, timeout = 30000 }) {
  const S = typeof window !== "undefined" ? window.MStore : null;
  const W = typeof window !== "undefined" ? window.MarurouWrite : null;
  const c = S && typeof S.case === "function" ? S.case(caseId) : null;
  if (!c || !c.dbId) return { ok: false, lost: true, reason: "この案件はまだサーバに登録されていないため、先方担当を保存できません。" };
  if (!trackDbId) return { ok: false, lost: true, reason: "このトラックはまだサーバに登録されていないため、先方担当を保存できません。" };
  if (side !== "業者" && side !== "保険会社") return { ok: false, lost: true, reason: "先方担当の側(業者側/保険会社側)が分かりません。" };
  if (!W || !W.queue || typeof W.queue.enqueue !== "function") {
    return { ok: false, lost: true, reason: "保存の経路がありません（書き込み層が読み込まれていません）。" };
  }
  const row = contact ? {
    trackId: trackDbId, side, relationId: null, contactDbId: contact.id, orgDbId: contact.orgId || null,
    name: tcpText(contact.name), orgName: contact.orgName || null, title: contact.title || null, phone: contact.phone || null,
  } : null;
  // ① 控えを先に差し替える(前の行を覚えておき、残らなかったら戻す)
  const prev = typeof S.setTrackContact === "function" ? S.setTrackContact(caseId, trackDbId, side, row) : undefined;
  tcpNotify();
  // ② op を 1 つ積む(mstore-adapter.js logDispatch と同じ積み方。版はキューが知っている最新を優先)
  const before = tcpWriteSnap(W);
  const known = typeof W.queue.versionOf === "function" ? W.queue.versionOf(c.dbId) : null;
  W.queue.enqueue({
    caseId: c.dbId, uiId: caseId, expectedVersion: known || c.version || null,
    ops: [{ op: "track.contact", trackId: trackDbId, side, contactId: contact ? contact.id : null }],
  });
  const res = await tcpAwaitWrite(W, before, timeout);
  // ③ 残らなかったら控えを戻す
  if (!res.ok && res.lost && prev !== undefined && typeof S.setTrackContact === "function") {
    S.setTrackContact(caseId, trackDbId, side, prev);
    tcpNotify();
  }
  return res;
}

/** 候補の一覧(rpc_list_org_contacts)。会社が分からず検索語も無ければ読まない(null を返す)。 */
function useOrgContactCandidates(orgDbId, query) {
  const [st, setSt] = React.useState({ status: "idle", rows: [], truncated: false, error: "" });
  React.useEffect(() => {
    const q = tcpText(query);
    if (!orgDbId && !q) { setSt({ status: "idle", rows: [], truncated: false, error: "" }); return undefined; }
    const cloud = typeof window !== "undefined" ? window.MarurouCloud : null;
    if (!cloud || typeof cloud.listOrgContacts !== "function") {
      setSt({ status: "error", rows: [], truncated: false, error: "担当者の一覧を読み込めません（接続の設定がありません）。" });
      return undefined;
    }
    let alive = true;
    setSt(s => ({ ...s, status: "loading", error: "" }));
    const timer = setTimeout(() => {
      cloud.listOrgContacts({ orgId: orgDbId || null, query: q || null, limit: 50 }).then(r => {
        if (alive) setSt({ status: "ready", rows: (r && r.rows) || [], truncated: !!(r && r.truncated), error: "" });
      }).catch(e => {
        if (alive) setSt({ status: "error", rows: [], truncated: false, error: (e && e.message) || "担当者の一覧を読み込めませんでした" });
      });
    }, q ? 250 : 0);
    return () => { alive = false; clearTimeout(timer); };
  }, [orgDbId, query]);
  return st;
}

/** 付け替えのモーダル。current は今の担当(店の控えの行)か null。 */
function TrackContactPicker({ caseId, trackDbId, side, orgDbId, orgName, current, onClose, onSaved }) {
  const [query, setQuery] = React.useState("");
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState("");
  const org = orgDbId || (current && current.orgDbId) || null;
  const cand = useOrgContactCandidates(org, query);
  const sideLabel = TCP_SIDE_LABEL[side] || side;
  const pick = async (contact) => {
    if (saving) return;
    setSaving(true); setErr("");
    const res = await saveTrackContact({ caseId, trackDbId, side, contact });
    setSaving(false);
    if (res.ok) { onSaved && onSaved(contact); onClose && onClose(); return; }
    setErr(res.reason || "保存できませんでした。");
  };
  const curId = current && current.contactDbId;
  const orgTitle = tcpText(orgName) || (current && tcpText(current.orgName)) || "";
  return (
    <ScModal title={"先方担当（" + sideLabel + "）を付け替える"} sub={orgTitle ? orgTitle + " の担当者から選びます。名前で探すと他の会社の担当者も選べます。" : "名前で担当者を探してください。"}
      onClose={saving ? () => {} : onClose}
      footer={<React.Fragment>
        {current && <AfButton variant="danger" disabled={saving} onClick={() => pick(null)} data-tcp-clear="">担当を外す</AfButton>}
        <AfButton onClick={onClose} disabled={saving}>閉じる</AfButton>
      </React.Fragment>}>
      <div data-track-contact-picker={side} style={{ display: "flex", flexDirection: "column", gap: 10 }}>
        {current && <div style={{ fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)" }}>
          いまの担当: <b style={{ color: "var(--foreground)" }}>{tcpText(current.name)}</b>{tcpText(current.orgName) ? "（" + tcpText(current.orgName) + "）" : ""}
        </div>}
        <AfInput aria-label="担当者を名前で探す" value={query} onChange={e => setQuery(e.target.value)}
          placeholder="名前・フリガナで探す" style={{ width: "100%" }} />
        {err && <div role="alert" style={{ padding: "8px 10px", borderRadius: "var(--radius-md)", background: "var(--destructive-muted)", color: "var(--destructive)", border: "1px solid var(--destructive-border)", fontSize: "var(--text-body-sm)" }}>{err}</div>}
        <div style={{ display: "flex", flexDirection: "column", gap: 4, maxHeight: 320, overflowY: "auto" }}>
          {cand.status === "idle" && <div style={{ fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)" }}>会社が分からないため、名前で探してください。</div>}
          {cand.status === "loading" && <div role="status" style={{ fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)" }}>担当者を読み込み中…</div>}
          {cand.status === "error" && <div role="alert" style={{ fontSize: "var(--text-body-sm)", color: "var(--destructive)" }}>{cand.error}</div>}
          {cand.status === "ready" && !cand.rows.length && <div style={{ fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)" }}>該当する担当者がいません。</div>}
          {cand.status === "ready" && cand.rows.map(r => {
            const isCur = curId && r.id === curId;
            const sub = [r.title, r.orgName].map(tcpText).filter(Boolean).join("・");
            return (
              <div key={r.id} data-tcp-row={r.id} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 8px", borderRadius: "var(--radius-md)", border: "1px solid var(--border)", background: isCur ? "var(--muted)" : "var(--card)" }}>
                <span style={{ display: "flex", flexDirection: "column", minWidth: 0, flex: 1 }}>
                  <b style={{ color: "var(--foreground)", overflowWrap: "anywhere" }}>{tcpText(r.name)}</b>
                  {sub && <span style={{ fontSize: "var(--text-caption)", color: "var(--muted-foreground)", overflowWrap: "anywhere" }}>{sub}</span>}
                </span>
                {org && r.sameOrg === false && <AfChip tone="warning">別の会社</AfChip>}
                {isCur
                  ? <AfChip>いまの担当</AfChip>
                  : <AfButton size="sm" variant="primary" disabled={saving} onClick={() => pick(r)}>この人にする</AfButton>}
              </div>
            );
          })}
          {cand.status === "ready" && cand.truncated && <div style={{ fontSize: "var(--text-caption)", color: "var(--foreground-subtle)" }}>多いので先頭の 50 人だけ出しています。名前で絞ってください。</div>}
        </div>
        {saving && <div role="status" style={{ fontSize: "var(--text-body-sm)", color: "var(--muted-foreground)" }}>保存しています…</div>}
      </div>
    </ScModal>
  );
}

/** トラック詳細の「先方担当」欄: 今の担当(TrackContactList)と「付け替え」ボタン。
 *  ready(案件の個別取得が済んだ)まで出さない。担当が無いときは「未設定」と「設定」ボタン。
 *  side はトラックの種類で決まる(保険 → 保険会社側、ほか → 業者側)。orgDbId は候補の会社(分かれば)。 */
function TrackContactField({ caseId, trackDbId, kind, orgDbId, orgName, ready, onToast, Row }) {
  useTrackContactsTick();
  const [open, setOpen] = React.useState(false);
  if (!ready || !trackDbId || typeof window.trackContactsOf !== "function") return null;
  const list = window.trackContactsOf(caseId, trackDbId);
  if (list === undefined) return null;
  const side = kind === "ins" ? "保険会社" : "業者";
  const current = typeof window.trackContactPrimary === "function" ? window.trackContactPrimary(list, kind) : null;
  const canEdit = !!(typeof window !== "undefined" && window.MarurouWrite);
  if (!list.length && !canEdit) return null;
  const body = (
    <span data-track-contact-field={side} style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap", minWidth: 0 }}>
      {list.length ? <TrackContactList contacts={list} /> : <span style={{ color: "var(--foreground-subtle)" }}>{TCP_SIDE_LABEL[side]} 未設定</span>}
      {canEdit && <AfButton size="sm" onClick={() => setOpen(true)} aria-label={"先方担当（" + TCP_SIDE_LABEL[side] + "）を" + (current ? "付け替える" : "設定する")}>
        {current ? "付け替え" : "設定"}</AfButton>}
    </span>
  );
  const modal = open ? <TrackContactPicker caseId={caseId} trackDbId={trackDbId} side={side} orgDbId={orgDbId} orgName={orgName}
    current={current} onClose={() => setOpen(false)}
    onSaved={c => onToast && onToast(c ? "先方担当を " + tcpText(c.name) + " にしました" : "先方担当を外しました")} /> : null;
  // Row は呼び出し側の行の部品(案件詳細の DDef・進行詳細の Line)。進行詳細の Line は描画のたびに作り直される
  // 関数なので、モーダル(入力中の検索語を持つ)は Row の外に置く(中に置くと描き直しで消える)。
  return (
    <React.Fragment>
      {Row ? <Row label="先方担当">{body}</Row> : body}
      {modal}
    </React.Fragment>
  );
}

Object.assign(window, { saveTrackContact, TrackContactPicker, TrackContactField, useTrackContactsTick });
