/* todos.jsx — the To-Do tab: ONE place for everything awaiting action across
   every server you can see.

   It's a view over tape_recap_flags — the same store the bot writes to and the
   owner DM digest reads from, so the tab, the DM and the bot never disagree.
   Items come from three places:
     · parse quality  — unrecognised ticker, option opened with no resolvable expiry
     · expiry         — an expired position auto-closed at its last trim, closing
                        price wants confirming
     · recaps         — reconciliation mismatches, and recap updates awaiting your
                        approval (servers set to manual approve)

   Access: the owner, plus anyone the owner grants the `todos` cap. Every read and
   write is scoped server-side to the caller's servers.
*/

const TODO_KIND = {
  ticker_unknown:       { label: "Unrecognised ticker",  tone: "warn" },
  expiry_unresolved:    { label: "Missing expiry",       tone: "warn" },
  expiry_close_price:   { label: "Confirm closing price", tone: "warn" },
  position_no_basis:    { label: "No cost basis",         tone: "bad"  },
  exit_mismatch:        { label: "Recap exit mismatch",  tone: "bad"  },
  untracked:            { label: "Recap play untracked", tone: "bad"  },
  unknown_analyst:      { label: "Recap: unknown analyst", tone: "bad" },
  recap_update_pending: { label: "Recap update to approve", tone: "act" },
};
const kindMeta = (k) => TODO_KIND[k] || { label: k || "Task", tone: "warn" };

function TodoKindPill({ kind }) {
  const m = kindMeta(kind);
  return <span className={cx("todo-kind", "tk-" + m.tone)}>{m.label}</span>;
}

// The recap named an exit we never recorded. Only a person can say whether that
// was a partial or the whole position — the bot deliberately refuses to guess,
// because closing a still-running play is the one unrecoverable mistake. So it
// asks here, with the recap's own numbers shown as the evidence.
function ApproveRecap({ row, onDone }) {
  const [open, setOpen] = useState(false);
  const [price, setPrice] = useState(row.recap_exit != null ? String(row.recap_exit) : "");
  const [busy, setBusy] = useState(false);

  async function approve(action) {
    setBusy(true);
    try {
      const r = await tapeSend(apiBase() + "/api/todos/" + encodeURIComponent(row.id) + "/approve", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ action, price: price === "" ? null : Number(price) }),
      });
      const j = await r.json().catch(() => ({}));
      toast.success(`Recorded as a ${action} @ ${j.price ?? price}`);
      onDone(row);
    } catch (e) { toast.error("Couldn't approve — " + e.message); }
    setBusy(false);
  }

  if (!open) {
    return <button className="btn" title="Record this exit on the position" onClick={() => setOpen(true)}>
      {I("check", { size: 13 })} Approve
    </button>;
  }
  return (
    <div className="todo-approve">
      <div className="todo-approve-ev mono">
        {row.recap_entry != null && <span>entry {row.recap_entry}</span>}
        {row.recap_exit != null && <span>recap exit {row.recap_exit}</span>}
        {row.recap_pct != null && <span>{row.recap_pct > 0 ? "+" : ""}{row.recap_pct}%</span>}
      </div>
      <label className="todo-approve-px">
        Exit price
        <input className="input" value={price} onChange={e => setPrice(e.target.value)}
          inputMode="decimal" placeholder="price" style={{ width: 90 }} />
      </label>
      <div className="todo-approve-acts">
        <button className="btn" disabled={busy} onClick={() => approve("trim")}
          title="Part of the position was sold — the rest stays open">Trim</button>
        <button className="btn" disabled={busy} onClick={() => approve("close")}
          title="The whole position was exited">Close</button>
        <button className="btn" disabled={busy} onClick={() => setOpen(false)}>Cancel</button>
      </div>
    </div>
  );
}

function TodosTab({ servers, serverIds, isOwner, onFindInEvents, onOpenCountChange }) {
  const [status, setStatus] = useState("open");     // open | resolved | all
  const [kind, setKind] = useState("all");
  const [rows, setRows] = useState(null);           // null = loading
  const [err, setErr] = useState(null);
  const [busy, setBusy] = useState(false);

  const serverName = (gid) => (servers.find(s => s.guild_id === gid) || {}).name || "Unknown server";

  const load = useCallback(() => {
    setErr(null);
    let url = apiBase() + "/api/todos?status=" + encodeURIComponent(status);
    if (kind !== "all") url += "&kind=" + encodeURIComponent(kind);
    if (serverIds) url += "&server_ids=" + serverIds;
    tapeFetch(url)
      .then(async r => { const j = await r.json(); if (!r.ok) throw new Error(j.error || "HTTP " + r.status); return j; })
      .then(j => setRows(j.todos || []))
      .catch(e => { setErr(e.message); setRows([]); });
  }, [status, kind, serverIds]);
  useEffect(() => { setRows(null); load(); }, [load]);

  // Optimistic single update — put the row back if the write fails. Under "All"
  // the row still belongs in view, so recolour it in place rather than dropping
  // it; under a status filter it no longer matches, so it goes.
  async function setOne(row, next) {
    const prev = rows;
    setRows(rs => status === "all"
      ? (rs || []).map(r => (r.id === row.id ? { ...r, status: next } : r))
      : (rs || []).filter(r => r.id !== row.id));
    try {
      await tapeSend(apiBase() + "/api/todos/" + encodeURIComponent(row.id), {
        method: "PATCH", headers: { "content-type": "application/json" },
        body: JSON.stringify({ status: next }),
      });
      toast.success(next === "resolved" ? "Marked done" : next === "open" ? "Reopened" : "Dismissed");
      // Keep the nav badge in step: leaving 'open' clears one, reopening adds one.
      if (onOpenCountChange) {
        const was = row.status === "open", willBe = next === "open";
        if (was && !willBe) onOpenCountChange(-1);
        else if (!was && willBe) onOpenCountChange(1);
      }
    } catch (e) {
      setRows(prev);
      toast.error("Couldn't update — " + e.message);
    }
  }

  // Clear everything currently visible (respects the active filters + scope).
  async function clearVisible() {
    const ids = (rows || []).map(r => r.id);
    if (!ids.length) return;
    if (!(await confirmDialog({
      title: "Mark all done",
      message: "Mark all " + ids.length + " visible item" + (ids.length === 1 ? "" : "s") + " as done?",
      confirmLabel: "Mark all done",
    }))) return;
    const openCleared = (rows || []).filter(r => r.status === "open").length;
    setBusy(true);
    try {
      await tapeSend(apiBase() + "/api/todos/bulk", {
        method: "POST", headers: { "content-type": "application/json" },
        body: JSON.stringify({ ids, status: "resolved" }),
      });
      setRows([]);
      if (onOpenCountChange && openCleared) onOpenCountChange(-openCleared);
      toast.success("Cleared " + ids.length);
    } catch (e) { toast.error("Failed — " + e.message); }
    setBusy(false);
  }

  // Group by server so multi-server owners see what belongs to whom. A single
  // server in view renders flat — no point labelling every row with one name.
  const groups = useMemo(() => {
    const by = new Map();
    for (const r of rows || []) {
      const g = r.guild_id || "";
      if (!by.has(g)) by.set(g, []);
      by.get(g).push(r);
    }
    return [...by.entries()].sort((a, b) => serverName(a[0]).localeCompare(serverName(b[0])));
  }, [rows, servers]);

  const kindsPresent = useMemo(() => [...new Set((rows || []).map(r => r.kind))], [rows]);
  const showServerHeads = groups.length > 1;

  // The strap line follows the filter — "waiting on you" is simply untrue once
  // you're looking at the Done list.
  const n = (rows || []).length;
  const tally = rows === null ? "" : ` ${n} item${n === 1 ? "" : "s"}.`;
  const blurb = (status === "resolved"
    ? "Items you've already settled — kept for reference, nothing here needs action."
    : status === "all"
      ? "Every item, outstanding and settled."
      : "Everything waiting on you — parse issues, expiring positions and recap reconciliations."
  ) + tally;

  return (
    <div className="todos">
      <div className="usage-head">
        <p className="sub" style={{ margin: 0 }}>
          {blurb}
          {!isOwner && " Scoped to your server(s)."}
        </p>
        <div className="todo-controls">
          <div className="seg">
            {[["open", "Open"], ["resolved", "Done"], ["all", "All"]].map(([k, lbl]) => (
              <button key={k} className={cx("seg-btn", status === k && "on")} onClick={() => setStatus(k)}>{lbl}</button>
            ))}
          </div>
          <select className="rating-select" value={kind} onChange={e => setKind(e.target.value)}>
            <option value="all">All types</option>
            {Object.keys(TODO_KIND).filter(k => kind === k || kindsPresent.includes(k) || status !== "open")
              .map(k => <option key={k} value={k}>{TODO_KIND[k].label}</option>)}
          </select>
          {status === "open" && (rows || []).length > 0 && (
            <button className="btn" disabled={busy} onClick={clearVisible}>{I("check", { size: 13 })} Mark all done</button>
          )}
        </div>
      </div>

      {err && <div className="trash-note muted">Couldn't load — {err}</div>}
      {rows === null ? (
        <div className="trash-note muted">Loading…</div>
      ) : rows.length === 0 ? (
        <Empty msg={status === "open"
          ? "Nothing needs you right now. New parse issues and recap mismatches land here automatically."
          : "Nothing here."} />
      ) : (
        <div className="todo-wrap">
          {groups.map(([gid, list]) => (
            <div key={gid || "none"} className="todo-group">
              {showServerHeads && (
                <div className="todo-group-head">
                  <span>{serverName(gid)}</span>
                  <span className="muted mono">{list.length}</span>
                </div>
              )}
              {list.map(t => (
                <div
                  className={cx("todo-row", "tone-" + kindMeta(t.kind).tone, "td-" + (t.status || "open"))}
                  key={t.id}
                >
                  <div className="todo-row-main">
                    <div className="todo-line">
                      <TodoKindPill kind={t.kind} />
                      {t.status === "resolved" && <span className="todo-status ts-resolved">Done</span>}
                      {t.status === "dismissed" && <span className="todo-status ts-dismissed">Dismissed</span>}
                      {t.ticker && (
                        <span className="ticker">${String(t.ticker).toUpperCase()}</span>
                      )}
                      {t.contract && <span className="mono todo-contract">{t.contract}</span>}
                      {t.analyst_handle && <span className="muted">· {t.analyst_handle}</span>}
                    </div>
                    <div className="todo-detail">{t.detail || "—"}</div>
                    {/* The post that caused it — without this a parse to-do names a
                        ticker and nothing else, and you'd have to hunt through
                        Events to work out which position it even belongs to. */}
                    {t.source_raw && (
                      <div className="todo-src">
                        <span className="todo-src-ch mono">{t.source_channel ? "#" + t.source_channel : "message"}</span>
                        <span className="todo-src-txt">{t.source_raw}</span>
                      </div>
                    )}
                    <div className="todo-sub mono">
                      {t.created_at ? fmtAgo(new Date(t.created_at).getTime()) : ""}
                      {t.recap_date ? " · " + t.recap_date : ""}
                    </div>
                  </div>
                  <div className="todo-row-acts">
                    {t.status === "open" && t.kind === "recap_update_pending" && (
                      <ApproveRecap row={t} onDone={(r) => setRows(rs => status === "all"
                        ? (rs || []).map(x => (x.id === r.id ? { ...x, status: "resolved" } : x))
                        : (rs || []).filter(x => x.id !== r.id))} />
                    )}
                    {t.ticker && onFindInEvents && (
                      <button
                        className="btn"
                        title={"Open Events filtered to $" + String(t.ticker).toUpperCase() + " — fix it there with Override"}
                        onClick={() => onFindInEvents(String(t.ticker).toUpperCase())}
                      >
                        {I("events", { size: 13 })} Find
                      </button>
                    )}
                    {t.status === "open" ? (
                      <>
                        <button className="btn" title="Mark this done" onClick={() => setOne(t, "resolved")}>
                          {I("check", { size: 13 })} Done
                        </button>
                        <button className="btn" title="Dismiss — not something to act on" onClick={() => setOne(t, "dismissed")}>
                          Dismiss
                        </button>
                      </>
                    ) : (
                      <button className="btn" title="Put this back on the open list" onClick={() => setOne(t, "open")}>
                        Reopen
                      </button>
                    )}
                  </div>
                </div>
              ))}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
