// CaseEdit — P0編集プリミティブ：インライン編集・工程消込／承認・ファイル添付・金額入力

const edTodayMd = () => { const d = new Date(); return `${d.getMonth()+1}/${d.getDate()}`; };

/* 単一行のインライン編集（クリック→入力→Enter確定／Esc取消） */
function EdText({ value, onSave, placeholder, mono, bold, size, color, multiline, width }) {
  const [ed, setEd] = React.useState(false);
  const [v, setV] = React.useState(value);
  const [hov, setHov] = React.useState(false);
  React.useEffect(()=>{ setV(value); }, [value]);
  const commit = () => { setEd(false); if(v !== value) onSave && onSave(v); };
  if(ed) {
    const P = { value:v, autoFocus:true, onChange:e=>setV(e.target.value), onBlur:commit,
      onKeyDown:e=>{ if(e.key==="Enter" && !multiline){ e.preventDefault(); commit(); } if(e.key==="Escape"){ setV(value); setEd(false); } },
      style:{ boxSizing:"border-box", width:width||"100%", minWidth:90, padding:"2px 6px", borderRadius:"var(--radius-sm)",
        border:"1px solid var(--brand)", boxShadow:"var(--ring-focus)", outline:"none", background:"var(--card)",
        fontFamily:"var(--font-sans)", fontSize:size||"var(--text-body-md)", fontWeight:bold?700:400, color:"var(--foreground)",
        minHeight:multiline?48:0, resize:multiline?"vertical":"none" } };
    return multiline ? <textarea {...P} /> : <input {...P} />;
  }
  return (
    <span onClick={()=>setEd(true)} onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)} title="クリックで編集"
      className={mono?"mono":""} style={{ cursor:"text", fontSize:size||"var(--text-body-md)", fontWeight:bold?700:undefined,
        color:color||(value?undefined:"var(--foreground-subtle)"), borderBottom:"1px dashed "+(hov?"var(--brand)":"transparent"),
        transition:"border-color var(--duration-fast) var(--easing)", display:"inline-flex", alignItems:"baseline", gap:4 }}>
      {value || placeholder || "—"}
      <span style={{ fontSize:"var(--text-caption)", color:hov?"var(--brand)":"var(--foreground-subtle)", flexShrink:0 }}>✎</span>
    </span>
  );
}

/* 金額の入力を読む(H3・9/25)。金額の欄(EdMoney)と保険の充当額(Payment.jsx)が同じ規則で読む。
   書き込み層(app/write/mstore-diff.js の uiMoneyCheck)も同じ規則で、ここを変えたらあちらも揃える。

   以前は「数字以外を先に全部消してから読む」作りで、打った値が黙って別の金額になっていた:
     -5000 → 5,000 / 12.5 → 125 / 全角の １２０００ → 空(= 金額未確定に戻る)
   消した後ろにある「負の数は入れられません」の検査には、正しい値が届いていなかった。

   受けるもの(値を変えずに読めるものだけ):
     ・半角・全角の数字(全角は半角に直す)
     ・3 桁ごとの区切り「,」(12,000)。区切りの位置がおかしいもの(12,5)は断る
     ・先頭の ¥ / ￥、末尾の 円、前後の空白
     ・空 … 金額を消す(null)。請求額なら「金額未確定」に戻る
   断るもの(保存せず、その場で理由を返す):
     ・マイナス(- － − ‐ ― ー △ ▲ で始まる) … 返金・値引きは行を負にせず明細を分けて表す(app/write/schema.js)
     ・小数点(. ．) … 1 円単位の整数で持つ(読み込みは小数を切り捨てるので、入れると画面と DB が食い違う)
     ・それ以外の文字
     ・100 億円以上(DB の列 numeric(12,2) に入らない)
   戻り値: { ok:true, value:number|null } または { ok:false, error:"…" } */
const ED_MONEY_MAX = 9999999999;
function edParseMoney(raw, label) {
  const name = label || "金額";
  const s0 = String(raw == null ? "" : raw)
    .replace(/[０-９]/g, ch => String.fromCharCode(ch.charCodeAt(0) - 0xFEE0))
    .replace(/，/g, ",").replace(/．/g, ".").replace(/　/g, " ")
    .trim();
  if (s0 === "") return { ok:true, value:null };
  const s = s0.replace(/^[¥￥]\s*/, "").replace(/\s*円$/, "");
  if (/^[-－−‐―ー△▲]/.test(s)) {
    return { ok:false, error:name+"にマイナスの金額は入れられません" };
  }
  if (/[.]/.test(s)) {
    return { ok:false, error:name+"は1円単位の整数で入力してください（小数点は使えません）" };
  }
  if (!/^\d+$/.test(s) && !/^\d{1,3}(,\d{3})+$/.test(s)) {
    return { ok:false, error:name+"は数字だけで入力してください（例: 12000）" };
  }
  const n = Number(s.replace(/,/g, ""));
  if (!Number.isSafeInteger(n) || n > ED_MONEY_MAX) {
    return { ok:false, error:name+"が大きすぎます（100億円未満で入力してください）" };
  }
  return { ok:true, value:n };
}

/* 金額のインライン編集（¥表示）。読めない入力は保存せず、欄の下に理由を出して入力を続けさせる
   (精算画面の入金期限の欄 PaymentDueEditor と同じ形 ─ H3)。Esc で元の値に戻して閉じる。
   onError を渡すと、断った理由を呼び出し側(精算画面のトースト)にも渡す。 */
function EdMoney({ value, onSave, label, onError }) {
  const [ed, setEd] = React.useState(false);
  const [v, setV] = React.useState(value==null?"":String(value));
  const [err, setErr] = React.useState("");
  const [hov, setHov] = React.useState(false);
  // Enter で閉じたあとに blur が続いても、二度保存しない(PaymentDueEditor と同じ)。
  const committed = React.useRef(false);
  React.useEffect(()=>{ setV(value==null?"":String(value)); setErr(""); }, [value]);
  const commit = () => {
    if(committed.current) return;
    const r = edParseMoney(v, label);
    if(!r.ok){ setErr(r.error); onError && onError(r.error); return; }
    committed.current = true;
    setErr(""); setEd(false);
    if(r.value!==value) onSave && onSave(r.value);
  };
  const cancel = () => { committed.current = true; setV(value==null?"":String(value)); setErr(""); setEd(false); };
  if(ed) return (
    <span style={{ display:"inline-flex", flexDirection:"column", alignItems:"flex-end", gap:3 }}>
      <input value={v} autoFocus aria-label={label||"金額"} aria-invalid={err?"true":"false"} inputMode="numeric"
        onChange={e=>{ setV(e.target.value); setErr(""); }} onBlur={commit}
        onKeyDown={e=>{ if(e.key==="Enter"){ e.preventDefault(); commit(); } if(e.key==="Escape"){ cancel(); } }}
        className="mono" style={{ boxSizing:"border-box", width:96, padding:"2px 6px", borderRadius:"var(--radius-sm)",
          border:"1px solid "+(err?"var(--destructive)":"var(--brand)"), boxShadow:"var(--ring-focus)", outline:"none", background:"var(--card)",
          fontFamily:"var(--font-sans)", fontSize:"var(--text-body-sm)", fontWeight:700, color:"var(--foreground)", textAlign:"right" }} />
      {err && <span role="alert" style={{ width:180, whiteSpace:"normal", textAlign:"left", fontWeight:400,
        fontSize:"var(--text-caption)", lineHeight:1.5, color:"var(--destructive)" }}>{err}</span>}
    </span>
  );
  return (
    <span onClick={()=>{ committed.current = false; setEd(true); }} onMouseEnter={()=>setHov(true)} onMouseLeave={()=>setHov(false)} title={(label||"金額")+"をクリックで編集"}
      className="mono" style={{ cursor:"text", fontWeight:700, color:value==null?"var(--foreground-subtle)":"var(--foreground)",
        borderBottom:"1px dashed "+(hov?"var(--brand)":"transparent") }}>{value==null?"未入力":yen(value)}<span style={{ fontSize:"var(--text-caption)", marginLeft:2, color:hov?"var(--brand)":"var(--foreground-subtle)" }}>✎</span></span>
  );
}

/* まだサーバに保存できない項目を「下書き」と分かる形にする。

   これまでは編集できてしまい、保存の失敗も出ず、次に別の端末で開くと
   消えていた。**保存したつもりで消えている**のが現場でいちばん危ない形なので、
   入力そのものは残したまま（現場はいま画面のメモとして使っている）、
   「この端末の画面にしか残らない」ことを見て分かるようにする。

   why には「なぜ保存できないか」を短く書く。理由を書けないものは
   そもそも編集させないほうがよい。 */
function EdDraft({ children, why }) {
  return (
    <span title={"この項目はまだサーバに保存されません（" + (why || "DBに置き場が無い") + "）。この端末の画面にだけ残ります。"}
      style={{ display:"inline-flex", alignItems:"baseline", gap:4, padding:"0 5px",
        borderRadius:"var(--radius-sm)", background:"var(--muted)",
        border:"1px dashed var(--border-strong)" }}>
      {children}
      <span style={{ fontSize:9, fontWeight:700, color:"var(--warning)", flexShrink:0, letterSpacing:"0.04em" }}>下書き</span>
    </span>
  );
}

/* 工程の消込・承認（レールの点タップ→ここで確定） */
function StepModal({ ctx, onClose, onDone, onUndo, onReject, onWait, onResume, onDoc, onPlan, onReceive, onUnskip, onSkip, onAssign, staff }) {
  if(!ctx) return null;
  const { step, trackLabel, prevName, docType, receiveLabel } = ctx;
  const isApproval = /承認/.test(step.n);
  const isDone = step.s==="done";
  const isSkip = step.s==="skip";
  const isWaiting = step.s==="waiting";
  const [date, setDate] = React.useState(/^\d{1,2}\/\d{1,2}$/.test(step.date||"") ? step.date : edTodayMd());
  const [plan, setPlan] = React.useState(step.plan||"");
  const [memo, setMemo] = React.useState("");
  // K18-08: 新しく見送りにする。理由は必須(サーバ 0097 が空を 400 で断るので、画面でも
  // 空のままは押せない)。完了済みの工程には出さない(先に「完了を戻す」─ 0097【設計判断2】)。
  const [skipOpen, setSkipOpen] = React.useState(false);
  const [skipReason, setSkipReason] = React.useState("");
  const canSkip = !!onSkip && !isDone && !isSkip;
  // 案件詳細-16: 相手待ちの理由・いつから。理由は自由入力・任意。いつからは
  // 未着手なら今日を初期値にする(0065【設計判断1】でサーバがsinceを省略時に
  // 今日で補うのに揃える)。既に待ち中ならDBの値をそのまま初期値にする。
  const [waitReason, setWaitReason] = React.useState(step.waitingReason||"");
  const [waitSince, setWaitSince] = React.useState(step.waitingSince || (isWaiting ? "" : edTodayMd()));
  const fileRef = React.useRef(null);
  const users = staff || [];
  // 決-10: 警告は needsAssignee だけ。assigneeUserId==null で自前判定しない
  // (移行673件が全部空で、初日に画面じゅうが「未アサイン」になる狼少年を避ける)。
  const needsAssignee = !!step.needsAssignee;
  const tone = isSkip ? "neutral" : isDone ? "success" : step.s==="waiting" ? "warn" : "brand";
  const statusLabel = isSkip ? "見送り" : isDone ? "完了" : step.s==="waiting" ? "相手待ち" : step.s==="now" ? "進行中" : "未着手";
  return (
    <ScModal title={step.n} sub={trackLabel} onClose={onClose}
      footer={<React.Fragment>
        <AfButton onClick={onClose}>閉じる</AfButton>
        {isSkip
          ? (onUnskip && <AfButton variant="primary" onClick={()=>onUnskip(memo)}>見送りを取り消す</AfButton>)
          : <React.Fragment>
              {docType && !isDone && <AfButton onClick={()=>onDoc&&onDoc(docType)}>{docType}を作成 ›</AfButton>}
              {isDone
                ? <AfButton onClick={()=>onUndo&&onUndo(memo)}>完了を戻す</AfButton>
                : <React.Fragment>
                    {isApproval && <AfButton onClick={()=>onReject&&onReject(memo)}>差し戻し</AfButton>}
                    {!isApproval && (isWaiting
                      ? (onResume && <AfButton onClick={()=>onResume()}>相手待ちを解除する</AfButton>)
                      : <AfButton onClick={()=>onWait&&onWait(waitReason||null, waitSince||null)}>相手待ちにする</AfButton>)}
                    {canSkip && <AfButton onClick={()=>setSkipOpen(v=>!v)} aria-expanded={skipOpen}>見送る</AfButton>}
                    <AfButton variant="primary" onClick={()=>onDone&&onDone(date, memo)}>{isApproval?"承認して完了":"完了にする"}</AfButton>
                  </React.Fragment>}
            </React.Fragment>}
      </React.Fragment>}>
      <div style={{ display:"flex", alignItems:"center", gap:8, flexWrap:"wrap" }}>
        <ScChip tone={tone} dot>{statusLabel}</ScChip>
        {step.date && !isSkip && <span className="mono" style={{ fontSize:"var(--text-body-sm)", color:"var(--muted-foreground)" }}>{step.date}</span>}
        {isApproval && !isSkip && <ScChip tone="brand">承認工程</ScChip>}
        {needsAssignee && <ScChip tone="warn" dot>未アサイン</ScChip>}
      </div>
      {isSkip && (
        <AfAlert variant="info" title="見送り中">
          {step.skipReason
            ? <span>理由: {step.skipReason}。取り消してもこの理由は残ります。</span>
            : <span>見送り理由の記録はありません。取り消しても、あとから理由は消えません（もともと空のままです）。</span>}
        </AfAlert>
      )}
      {/* K18-08: 見送りの理由(必須)。「見送る」を押すと開く小さな入力。見送った工程は
          次の工程へ進み、あとから「見送りを取り消す」で戻せる(理由は残る・決-9)。 */}
      {canSkip && skipOpen && (
        <AfCard pad={12} style={{ background:"var(--accent)" }}>
          <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>見送りの理由（必須）</div>
          <AfInput aria-label="見送りの理由" value={skipReason} onChange={e=>setSkipReason(e.target.value)}
            placeholder="保険を使わないことになった・この工程は不要 など" style={{ width:"100%" }} />
          <div style={{ display:"flex", alignItems:"center", gap:8, flexWrap:"wrap", marginTop:8 }}>
            <AfButton size="sm" variant="primary" disabled={!skipReason.trim()} onClick={()=>onSkip(skipReason.trim(), memo)}>見送りにする</AfButton>
            <AfButton size="sm" onClick={()=>setSkipOpen(false)}>やめる</AfButton>
            <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>この工程はやらないものとして次の工程へ進みます。あとから取り消せます（理由は残ります）。</span>
          </div>
        </AfCard>
      )}
      {/* 決-10: 工程ごとの担当。案件の本担当(lead)とは別。必須にしない。 */}
      {onAssign && (
        <div>
          <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>工程の担当（この工程をやる人。案件の本担当とは別）</div>
          <select aria-label="工程の担当" value={step.assigneeUserId || ""}
            onChange={e=>onAssign(e.target.value || null)}
            style={{ width:"100%", height:28, padding:"0 8px", borderRadius:"var(--radius-md)",
              borderWidth:1, borderStyle:"solid", borderColor:"var(--input)",
              fontSize:"var(--text-body-md)", fontFamily:"var(--font-sans)",
              color:"var(--foreground)", background:"var(--card)" }}>
            <option value="">未アサイン（あとで決める）</option>
            {users.map(u => <option key={u.id} value={u.id}>{u.name}</option>)}
          </select>
          {needsAssignee && <div style={{ marginTop:6 }}><AfAlert variant="notice" title="未アサインです">この工程にはまだ担当が付いていません。必須ではありませんが、付けると誰がやるか分かります。</AfAlert></div>}
          {!users.length && (
            <div style={{ fontSize:"var(--text-body-sm)", color:"var(--muted-foreground)", marginTop:4 }}>
              担当者の名簿を読み込めていません。
            </div>
          )}
        </div>
      )}
      {!isDone && !isSkip && (
        <React.Fragment>
          {/* K18-49: 日付は年月日の選択(FormParts.jsx MrDatePicker)。池田さん版に合わせて 2 列に並べ、
              横の補足文は外した。値は今までどおり店の表記(M/D・年がずれるときだけ YYYY/M/D)で受け渡す。 */}
          <div className="mr-stack-1" style={{ display:"grid", gridTemplateColumns:"1fr 1fr", gap:12 }}>
            <div>
              <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>予定日</div>
              <MrDatePicker label="予定日" value={plan} onChange={setPlan} width="100%" />
            </div>
            <div>
              <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>確定日</div>
              <MrDatePicker label="確定日" value={date} onChange={setDate} required width="100%" />
            </div>
          </div>
          {onPlan && <div><AfButton size="sm" onClick={()=>onPlan(plan)}>予定日だけ保存</AfButton></div>}
          {/* 案件詳細-16: 相手待ちの理由・いつから。理由は自由入力・任意
              (未入力でも「相手待ちにする」は押せる ─ 0065【設計判断1】)。 */}
          {onWait && (
            <div>
              <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>相手待ちの理由（任意）</div>
              <AfInput value={waitReason} onChange={e=>setWaitReason(e.target.value)} placeholder="保険会社の回答待ち・部材待ち など" style={{ width:"100%" }} />
              <div style={{ display:"flex", gap:12, alignItems:"end", marginTop:8, flexWrap:"wrap" }}>
                <div>
                  <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>いつから</div>
                  <MrDatePicker label="いつから" value={waitSince} onChange={setWaitSince} />
                </div>
                {isWaiting && <AfButton size="sm" onClick={()=>onWait(waitReason||null, waitSince||null)}>相手待ちの内容を保存</AfButton>}
              </div>
            </div>
          )}
          {receiveLabel && onReceive && (
            <AfCard pad={12} style={{ background:"var(--accent)" }}>
              <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>{receiveLabel}を受領して完了</div>
              <div style={{ display:"flex", alignItems:"center", gap:8, flexWrap:"wrap" }}>
                <AfButton size="sm" onClick={()=>fileRef.current&&fileRef.current.click()}>ファイルを選ぶ</AfButton>
                <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>添付すると案件のファイルに残り、この工程が同時に完了します</span>
              </div>
              <input ref={fileRef} type="file" multiple style={{ display:"none" }}
                onChange={ev=>{ const list=[...ev.target.files]; if(list.length) onReceive(list, date, memo); }} />
            </AfCard>
          )}
        </React.Fragment>
      )}
      <div>
        <div style={{ fontSize:"var(--text-caption)", fontWeight:600, letterSpacing:"0.06em", color:"var(--muted-foreground)", marginBottom:5 }}>メモ（任意）</div>
        <textarea value={memo} onChange={e=>setMemo(e.target.value)} placeholder={isSkip?"取り消しのメモ":isApproval?"承認・差し戻しの理由":"やり取りの結果"}
          style={{ width:"100%", boxSizing:"border-box", minHeight:52, padding:8, borderRadius:"var(--radius-md)", border:"1px solid var(--input)",
            fontSize:"var(--text-body-md)", fontFamily:"var(--font-sans)", resize:"vertical", color:"var(--foreground)", background:"var(--card)" }} />
      </div>
      {isApproval && !isDone && !isSkip && <AfAlert variant="info" title="差し戻し">前工程「{prevName||"見積"}」を進行中に戻します。</AfAlert>}
    </ScModal>
  );
}

/* ファイル添付（ドロップ／選択・写真はプレビュー可） */
function FileDrop({ onFiles }) {
  const [over, setOver] = React.useState(false);
  const ref = React.useRef(null);
  const take = list => { const arr = [...list]; if(arr.length && onFiles) onFiles(arr); };
  return (
    <div onDragOver={e=>{ e.preventDefault(); setOver(true); }} onDragLeave={()=>setOver(false)}
      onDrop={e=>{ e.preventDefault(); setOver(false); take(e.dataTransfer.files); }}
      onClick={()=>ref.current && ref.current.click()}
      style={{ cursor:"pointer", padding:"11px 12px", marginBottom:10, borderRadius:"var(--radius-md)",
        border:"1px dashed "+(over?"var(--brand)":"var(--border-strong)"), background:over?"var(--brand-muted)":"var(--accent)",
        display:"flex", alignItems:"center", gap:9, transition:"all var(--duration-fast) var(--easing)" }}>
      <span style={{ fontSize:"var(--text-body-md)", fontWeight:600, color:over?"var(--brand)":"var(--foreground)" }}>＋ ファイルを追加</span>
      <span style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>ここにドロップ、またはクリックして選択（写真・PDF）</span>
      <input ref={ref} type="file" multiple accept="image/*,application/pdf" onChange={e=>{ take(e.target.files); e.target.value=""; }} style={{ display:"none" }} />
    </div>
  );
}

function FileViewer({ file, onClose }) {
  if(!file) return null;
  // 決-8: 帳票本文(docBody)はシステムで作らない。FileViewer は「外で作って添付した
  // ファイル」を見るためのもの。⑭: url は getAttachmentUrl(署名URL・60分)で取る。
  // url が無いときは本文を出さず、開けないと正直に出す(05 §5.5)。
  const img = file.url && file.mime && String(file.mime).startsWith("image");
  const pdf = file.url && ((file.mime && /pdf/i.test(file.mime)) || /\.pdf$/i.test(file.name||""));
  const printIt = () => {
    if(file.url){ const w = window.open(file.url, "_blank", "noopener"); if(w) w.focus(); }
  };
  return (
    <ScModal width="var(--modal-w-lg)" title={file.name} sub={(file.type||"その他")+(file.sub?"・"+file.sub:"")+"　"+file.date} onClose={onClose}
      footer={<React.Fragment>
        <AfButton onClick={onClose}>閉じる</AfButton>
        {file.url && <AfButton onClick={printIt}>別タブで開く／印刷</AfButton>}
      </React.Fragment>}>
      {img
        ? <div style={{ background:"var(--muted)", border:"1px solid var(--border)", borderRadius:"var(--radius-md)", padding:8, textAlign:"center" }}>
            <img src={file.url} alt={file.name} style={{ maxWidth:"100%", maxHeight:420, display:"block", margin:"0 auto", borderRadius:"var(--radius-sm)" }} />
          </div>
        : pdf
          ? <div style={{ padding:"36px 12px", fontSize:"var(--text-body-md)", color:"var(--muted-foreground)", textAlign:"center" }}>PDF は別タブで開きます　<a href={file.url} target="_blank" rel="noreferrer">{file.name}</a></div>
        : file.url
          ? <div style={{ padding:"36px 12px", fontSize:"var(--text-body-md)", color:"var(--muted-foreground)", textAlign:"center" }}>この形式はプレビューできません　<a href={file.url} target="_blank" rel="noreferrer">{file.name}</a></div>
          : <div style={{ background:"var(--muted)", border:"1px dashed var(--border-strong)", borderRadius:"var(--radius-md)",
              padding:"40px 12px", textAlign:"center", fontSize:"var(--text-body-md)", color:"var(--muted-foreground)" }}>
              このファイルのプレビュー用URLがありません
            </div>}
      <div style={{ fontSize:"var(--text-caption)", color:"var(--foreground-subtle)" }}>紐づけ：{file.link||"案件全体"}{file.count?`　${file.count}枚`:""}</div>
    </ScModal>
  );
}

Object.assign(window, { EdText, EdMoney, edParseMoney, EdDraft, StepModal, FileDrop, FileViewer });
