/* global React */
// discussions.jsx — Discussions module per PRODUCT.md.
// Two backends, same shape:
//   - real:  /api/discussions + /api/auth/* (when window.CCT_USE_REAL_API)
//   - mock:  localStorage-backed (offline fallback)
// Shape: { id, ticker, quarter, handle, karma, body, score, ts, replies: [{...}] }
// Exposes window.Discussions, window.discussionsStore.

const { useState, useEffect, useRef } = React;

// -------- API helper (cookies for session) --------
const API_BASE = window.CCT_API_BASE || "";
async function apiFetch(path, opts = {}) {
  const res = await fetch(`${API_BASE}${path}`, {
    credentials: "include",
    headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
    ...opts,
  });
  if (!res.ok) {
    const t = await res.text().catch(() => "");
    const err = new Error(`api ${res.status} ${path} ${t}`); err.status = res.status; throw err;
  }
  return res.json();
}

// -------- localStorage store (mock + offline fallback) --------
// Keys bumped to v4. Earlier versions (v1/v2/v3) are swept on every load.
// Stored sets that contain smoke-test handles or the placeholder shiv take
// are treated as poisoned and re-seeded from CCT_DISCUSSIONS_SEED.
const LS_KEY_TAKES = "cct.discussions.v4";
const LS_KEY_USER  = "cct.user.v1";
const LS_KEY_VOTES = "cct.discussions.votes.v4";

const SMOKE_HANDLE_RE = /smoke|^smokeweb|smoke1777|^web|^shiv$/i;
const SMOKE_BODY_RE   = /smoke-take|smoke|verifying live|shiv karma/i;

function isSmokeTake(t) {
  return SMOKE_HANDLE_RE.test(t.handle || "") || SMOKE_BODY_RE.test(t.body || "");
}

function loadTakes() {
  try {
    [
      "cct.discussions.v1", "cct.discussions.v2", "cct.discussions.v3",
      "cct.discussions.votes.v1", "cct.discussions.votes.v2", "cct.discussions.votes.v3",
    ].forEach(k => {
      if (localStorage.getItem(k)) localStorage.removeItem(k);
    });
  } catch {}
  try {
    const raw = localStorage.getItem(LS_KEY_TAKES);
    if (raw) {
      const parsed = JSON.parse(raw);
      if (Array.isArray(parsed) && parsed.length > 0) {
        if (!parsed.some(isSmokeTake)) return parsed;
      }
    }
  } catch {}
  return JSON.parse(JSON.stringify(window.CCT_DISCUSSIONS_SEED || []));
}
function saveTakes(takes) { localStorage.setItem(LS_KEY_TAKES, JSON.stringify(takes)); }
function loadUser() {
  try {
    const raw = localStorage.getItem(LS_KEY_USER);
    if (raw) return JSON.parse(raw);
  } catch {}
  return null;
}
function saveUser(u) { localStorage.setItem(LS_KEY_USER, JSON.stringify(u)); }
function loadVotes() {
  try { return JSON.parse(localStorage.getItem(LS_KEY_VOTES) || "{}"); } catch { return {}; }
}
function saveVotes(v) { localStorage.setItem(LS_KEY_VOTES, JSON.stringify(v)); }

const localStore = {
  async list({ ticker, quarter }) {
    const all = loadTakes();
    const filtered = all.filter(t => t.ticker === ticker && (!quarter || t.quarter === quarter));
    return withoutSmoke(filtered);
  },
  async add({ ticker, quarter, body, user }) {
    const take = {
      id: "u_" + Math.random().toString(36).slice(2, 10),
      ticker, quarter,
      handle: user.handle, karma: user.karma || 0,
      body, score: 1, ts: new Date().toISOString(), replies: []
    };
    const all = loadTakes(); all.unshift(take); saveTakes(all);
    return take;
  },
  async reply({ takeId, body, user }) {
    const reply = {
      id: "r_" + Math.random().toString(36).slice(2, 10),
      handle: user.handle, karma: user.karma || 0,
      body, score: 1, ts: new Date().toISOString()
    };
    const all = loadTakes();
    const t = all.find(x => x.id === takeId);
    if (!t) throw new Error("take not found");
    t.replies = t.replies || []; t.replies.push(reply);
    saveTakes(all);
    return reply;
  },
  async vote({ id, dir }) {
    const all = loadTakes();
    let target = null;
    for (const t of all) {
      if (t.id === id) { target = t; break; }
      const r = (t.replies || []).find(rr => rr.id === id);
      if (r) { target = r; break; }
    }
    if (!target) throw new Error("not found");
    // Approximate: store edges per-id in LS_KEY_VOTES — caller already does that.
    target.score = (target.score || 0) + dir;
    saveTakes(all);
    return target.score;
  }
};

// Filter the seed by ticker/quarter — used as a defensive fallback if the API
// is unreachable (e.g. backend not running, JSDOM has no fetch).
function seedFor({ ticker, quarter }) {
  const seed = window.CCT_DISCUSSIONS_SEED || [];
  return seed.filter(t => t.ticker === ticker && (!quarter || t.quarter === quarter));
}

// Strip stale smoke-test takes from any take list before the UI sees them.
// The DB sweeper deletes them at startup, but defence-in-depth — if a stale
// row survives (race during deploy, replica lag, manual smoke run), the user
// shouldn't see it.
function withoutSmoke(takes) {
  return (takes || []).filter(t => !isSmokeTake(t)).map(t => ({
    ...t,
    replies: (t.replies || []).filter(r => !isSmokeTake(r)),
  }));
}

// -------- real-API store --------
const apiStore = {
  async list({ ticker, quarter }) {
    try {
      const r = await apiFetch(`/api/discussions?ticker=${encodeURIComponent(ticker)}&quarter=${encodeURIComponent(quarter || "")}`);
      return withoutSmoke(r.takes || []);
    } catch (e) {
      // Network/auth blip: serve curated seed so the page never reads as dead.
      // Posting still requires a working API + cookie session.
      return withoutSmoke(seedFor({ ticker, quarter }));
    }
  },
  async add({ ticker, quarter, body }) {
    const r = await apiFetch("/api/discussions", { method: "POST", body: JSON.stringify({ ticker, quarter, body }) });
    return r.take;
  },
  async reply({ takeId, body }) {
    const r = await apiFetch(`/api/discussions/${encodeURIComponent(takeId)}/reply`, { method: "POST", body: JSON.stringify({ body }) });
    return r.reply;
  },
  async vote({ id, dir }) {
    const r = await apiFetch(`/api/discussions/${encodeURIComponent(id)}/vote`, { method: "POST", body: JSON.stringify({ dir }) });
    return r.score;
  }
};

const discussionsStore = window.CCT_USE_REAL_API ? apiStore : localStore;
window.discussionsStore = discussionsStore;

// -------- auth (real API or mock-from-LS) --------
function useAuth() {
  const [user, setUser] = useState(window.CCT_USE_REAL_API ? null : loadUser());
  const [bootstrapped, setBootstrapped] = useState(!window.CCT_USE_REAL_API);

  // On mount, hydrate from /api/auth/me when real API is on.
  useEffect(() => {
    if (!window.CCT_USE_REAL_API) return;
    let cancelled = false;
    (async () => {
      try {
        const r = await apiFetch("/api/auth/me");
        if (!cancelled) setUser(r.user || null);
      } catch {} finally { if (!cancelled) setBootstrapped(true); }
    })();
    return () => { cancelled = true; };
  }, []);

  async function signIn(email) {
    if (window.CCT_USE_REAL_API) {
      const r = await apiFetch("/api/auth/signin", { method: "POST", body: JSON.stringify({ email }) });
      setUser(r.user);
      return r.user;
    }
    const handle = email.split("@")[0].replace(/[^a-z0-9_]/gi, "_").slice(0, 20).toLowerCase() || "investor";
    const u = { email, handle, karma: 0 };
    saveUser(u); setUser(u); return u;
  }

  async function signOut() {
    if (window.CCT_USE_REAL_API) {
      try { await apiFetch("/api/auth/signout", { method: "POST" }); } catch {}
    } else {
      localStorage.removeItem(LS_KEY_USER);
    }
    setUser(null);
  }

  function bumpKarma(d) {
    // With the real API karma is computed server-side — no-op. Local mode
    // updates locally so the chip refreshes immediately.
    if (window.CCT_USE_REAL_API || !user) return;
    const u = { ...user, karma: (user.karma || 0) + d };
    saveUser(u); setUser(u);
  }

  return { user, signIn, signOut, bumpKarma, bootstrapped };
}

// -------- helpers --------
function timeAgo(iso) {
  const d = new Date(iso);
  const s = Math.max(1, Math.floor((Date.now() - d.getTime()) / 1000));
  if (s < 60) return s + "s";
  const m = Math.floor(s / 60); if (m < 60) return m + "m";
  const h = Math.floor(m / 60); if (h < 24) return h + "h";
  const day = Math.floor(h / 24); if (day < 30) return day + "d";
  return d.toISOString().slice(0, 10);
}

function VoteCol({ score, voted, onVote }) {
  return (
    <div className="vote-col">
      <button
        className={"vote " + (voted === 1 ? "active up" : "")}
        aria-label="upvote"
        onClick={() => onVote(voted === 1 ? 0 : 1)}
      >▲</button>
      <div className="score">{score}</div>
      <button
        className={"vote " + (voted === -1 ? "active down" : "")}
        aria-label="downvote"
        onClick={() => onVote(voted === -1 ? 0 : -1)}
      >▼</button>
    </div>
  );
}

// -------- main component --------
function Discussions({ ticker, quarter }) {
  const [takes, setTakes] = useState([]);
  const [sort, setSort] = useState("score"); // "score" | "new"
  const [body, setBody] = useState("");
  const [replyOpen, setReplyOpen] = useState(null); // takeId or null
  const [replyBody, setReplyBody] = useState("");
  const [votes, setVotes] = useState(loadVotes());
  const auth = useAuth();
  const [signInEmail, setSignInEmail] = useState("");
  const [error, setError] = useState(null);

  async function refresh() {
    try {
      const list = await discussionsStore.list({ ticker, quarter });
      setTakes(list);
    } catch (e) {
      setError(e.message || "failed to load discussions");
    }
  }

  // Load on mount + whenever ticker/quarter changes
  useEffect(() => { refresh(); }, [ticker, quarter]);

  async function vote(takeId, replyId, dir) {
    if (!auth.user) return alert("Sign in to vote.");
    const key = replyId ? `${takeId}:${replyId}` : takeId;
    const prev = votes[key] || 0;
    if (dir === prev) return;
    try {
      // Real API: dir replaces prev (server keeps unique vote per user).
      // Local: we send the delta so existing scores update correctly.
      const id = replyId || takeId;
      if (window.CCT_USE_REAL_API) {
        await discussionsStore.vote({ id, dir });
      } else {
        await discussionsStore.vote({ id, dir: dir - prev });
      }
      const nextVotes = { ...votes, [key]: dir };
      saveVotes(nextVotes);
      setVotes(nextVotes);
      refresh();
    } catch (e) {
      setError(e.message || "vote failed");
    }
  }

  async function submit(e) {
    e.preventDefault();
    const t = body.trim();
    if (!t || !auth.user || t.length > 280) return;
    try {
      await discussionsStore.add({ ticker, quarter, body: t, user: auth.user });
      auth.bumpKarma(1);
      setBody("");
      refresh();
    } catch (e) {
      setError(e.message || "failed to post take");
    }
  }

  async function submitReply(takeId, e) {
    e.preventDefault();
    const t = replyBody.trim();
    if (!t || !auth.user || t.length > 280) return;
    try {
      await discussionsStore.reply({ takeId, body: t, user: auth.user });
      auth.bumpKarma(1);
      setReplyBody(""); setReplyOpen(null);
      refresh();
    } catch (e) {
      setError(e.message || "failed to post reply");
    }
  }

  const sorted = [...takes].sort((a, b) =>
    sort === "score" ? b.score - a.score : new Date(b.ts) - new Date(a.ts)
  );

  return (
    <div className="discussions">
      <div className="disc-head">
        <div>
          <div className="eyebrow">Discussions · {quarter}</div>
          <h3 className="h-2" style={{ marginTop: 4 }}>What other investors are seeing.</h3>
          <p className="dim" style={{ marginTop: 6, fontSize: "var(--fs-sm)" }}>
            280 chars · upvote / downvote · one level of reply. Sharpen your take.
          </p>
        </div>
        <div className="disc-sort">
          <button className={sort === "score" ? "active" : ""} onClick={() => setSort("score")}>Top</button>
          <button className={sort === "new" ? "active" : ""} onClick={() => setSort("new")}>New</button>
        </div>
      </div>

      {/* Compose */}
      {auth.user ? (
        <form className="disc-compose" onSubmit={submit}>
          <div className="who-line">
            <span className="handle">{auth.user.handle}</span>
            <span className="karma mono">karma {auth.user.karma}</span>
            <button type="button" className="signout" onClick={auth.signOut}>sign out</button>
          </div>
          <textarea
            value={body}
            onChange={e => setBody(e.target.value.slice(0, 280))}
            placeholder="Drop a sharp observation. 280 chars."
            maxLength={280}
            rows={3}
          />
          <div className="row">
            <span className="dim mono" style={{ fontSize: "var(--fs-mono-xs)" }}>{280 - body.length} left</span>
            <button className="btn primary" type="submit" disabled={!body.trim()}>Post take →</button>
          </div>
        </form>
      ) : (
        <form
          className="disc-compose signin"
          onSubmit={async e => {
            e.preventDefault();
            if (!signInEmail.includes("@")) return;
            try { await auth.signIn(signInEmail.trim().toLowerCase()); refresh(); }
            catch (err) { setError(err.message || "sign-in failed"); }
          }}
        >
          <div className="eyebrow">Sign in to post · email-only{window.CCT_USE_REAL_API ? "" : " · mock auth"}</div>
          <div className="row" style={{ gap: 8, marginTop: 8 }}>
            <input
              type="email"
              placeholder="you@firm.com"
              value={signInEmail}
              onChange={e => setSignInEmail(e.target.value)}
              required
            />
            <button className="btn primary" type="submit">Continue →</button>
          </div>
          <div className="dim mono" style={{ fontSize: "var(--fs-mono-xs)", marginTop: 6 }}>
            No password. Handle generated from your email. {window.CCT_USE_REAL_API ? "Persisted server-side." : "Stored locally."}
          </div>
        </form>
      )}

      {error && (
        <div className="empty" style={{ marginTop: 12, color: "var(--signal-disappeared)" }}>
          <div className="glyph">[ ! ]</div>
          <div>{error}</div>
        </div>
      )}

      {/* List */}
      <div className="disc-list">
        {sorted.length === 0 && (
          <div className="empty" style={{ marginTop: 16 }}>
            <div className="glyph">[ — ]</div>
            <div>No takes on {ticker} for {quarter} yet. Be first.</div>
          </div>
        )}
        {sorted.map(t => {
          const v = votes[t.id] || 0;
          return (
            <article className="take" key={t.id}>
              <VoteCol score={t.score} voted={v} onVote={d => vote(t.id, null, d)} />
              <div className="take-body">
                <div className="meta">
                  <span className="handle">{t.handle}</span>
                  {/^(p|c|k|po)\d/.test(t.id || "") && (
                    <span className="curated-tag mono">CURATED</span>
                  )}
                  <span className="karma mono">karma {t.karma}</span>
                  <span className="ts mono">{timeAgo(t.ts)}</span>
                </div>
                <div className="body">{t.body}</div>
                <div className="actions">
                  <button onClick={() => setReplyOpen(replyOpen === t.id ? null : t.id)}>
                    {replyOpen === t.id ? "cancel" : "reply"}
                  </button>
                  <span className="dim mono">{(t.replies || []).length} {(t.replies || []).length === 1 ? "reply" : "replies"}</span>
                </div>
                {replyOpen === t.id && auth.user && (
                  <form className="reply-compose" onSubmit={e => submitReply(t.id, e)}>
                    <textarea
                      value={replyBody}
                      onChange={e => setReplyBody(e.target.value.slice(0, 280))}
                      placeholder={`Reply to ${t.handle}…`}
                      rows={2}
                      maxLength={280}
                    />
                    <div className="row">
                      <span className="dim mono" style={{ fontSize: "var(--fs-mono-xs)" }}>{280 - replyBody.length} left</span>
                      <button className="btn primary" type="submit" disabled={!replyBody.trim()}>Post reply →</button>
                    </div>
                  </form>
                )}
                {replyOpen === t.id && !auth.user && (
                  <div className="dim" style={{ fontSize: "var(--fs-sm)", marginTop: 6 }}>
                    Sign in above to reply.
                  </div>
                )}
                {(t.replies || []).map(r => {
                  const rv = votes[`${t.id}:${r.id}`] || 0;
                  return (
                    <div className="reply" key={r.id}>
                      <VoteCol score={r.score} voted={rv} onVote={d => vote(t.id, r.id, d)} />
                      <div className="take-body">
                        <div className="meta">
                          <span className="handle">{r.handle}</span>
                          <span className="karma mono">karma {r.karma}</span>
                          <span className="ts mono">{timeAgo(r.ts)}</span>
                        </div>
                        <div className="body">{r.body}</div>
                      </div>
                    </div>
                  );
                })}
              </div>
            </article>
          );
        })}
      </div>
    </div>
  );
}

Object.assign(window, { Discussions });
