// story-modal.jsx — User Story create + drawer.
//
// Two surfaces here:
//
//   NewStoryModal — minimalist create form. Just enough to define a
//                   story (title, description, project, epic, reviewer).
//                   Status starts at "backlog"; transitions to
//                   in_progress / review automatically when child task
//                   statuses change (driven by the backend
//                   recomputeStoryStatus helper).
//
//   StoryDrawer   — opens when a user clicks a story chip on a task or
//                   navigates from the upcoming "Stories" view. Shows
//                   the story header, reviewer chip, child task list,
//                   and the reviewer's Approve / Request Changes
//                   buttons (only visible when the story is in
//                   "review" AND the current user is in the reviewers
//                   list).
//
// We deliberately reuse the existing .modal* / .drawer-style CSS so
// the look matches the rest of the app — no new design language for a
// peripheral feature.

function NewStoryModal({ open, onClose, onCreated, defaultProjectId, people = [] }) {
  const [title, setTitle]       = React.useState("");
  const [desc, setDesc]         = React.useState("");
  const [projectId, setProjectId] = React.useState(defaultProjectId || (PROJECTS[0] && PROJECTS[0].id) || "");
  const [epicId, setEpicId]     = React.useState("");
  const [reviewerId, setReviewerId] = React.useState("");
  const [busy, setBusy]         = React.useState(false);
  const [err, setErr]           = React.useState("");

  React.useEffect(() => {
    if (!open) return;
    setTitle(""); setDesc("");
    const initialProj = defaultProjectId || (PROJECTS[0] && PROJECTS[0].id) || "";
    setProjectId(initialProj);
    setEpicId("");
    // Pre-fill reviewer from the project's default_reviewer_id when
    // set. The coordinator can override per-story but typing the
    // tester's name 20 times a day is what we're fixing.
    const proj = (PROJECTS || []).find(p => p.id === initialProj);
    setReviewerId((proj && proj.defaultReviewerId) || "");
    setBusy(false); setErr("");
  }, [open, defaultProjectId]);

  // When the user changes the project picker mid-flow, re-pull the
  // default reviewer for the newly-chosen project (but only if they
  // haven't manually picked one yet — preserving their explicit choice).
  React.useEffect(() => {
    if (!open) return;
    const proj = (PROJECTS || []).find(p => p.id === projectId);
    const fallback = (proj && proj.defaultReviewerId) || "";
    // Heuristic: if the current reviewerId matches *any* project's
    // default, treat it as auto-set and replace with the new one.
    // Manually-picked reviewers (those that don't match any project
    // default) stick around — the user clearly meant them.
    const wasAutoSet = (PROJECTS || []).some(p => p.defaultReviewerId === reviewerId && reviewerId);
    if (!reviewerId || wasAutoSet) setReviewerId(fallback);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [projectId]);

  if (!open) return null;

  const epicOptions = (Array.isArray(EPICS) ? EPICS : [])
    .filter(e => (e.project_id || e.projectId) === projectId);

  // Reviewer pool — anyone active in the workspace. We don't restrict
  // to project members because reviewers (e.g. tech leads) often
  // span multiple projects and may not be explicit project members.
  // Defensive id-dedup: if a stale localStorage bootstrap or a future
  // backend regression slips duplicate person rows into PEOPLE, the
  // <select> below would crash with "two children with the same key".
  // Last-wins per id is fine — the rows are otherwise identical.
  const reviewerPool = (() => {
    const src = (people || PEOPLE || []).filter(p => p && p.status !== "deactivated");
    const seen = new Set();
    const out = [];
    for (const p of src) {
      if (!p.id || seen.has(p.id)) continue;
      seen.add(p.id);
      out.push(p);
    }
    return out;
  })();

  async function submit() {
    if (!title.trim() || !projectId || busy) return;
    setBusy(true); setErr("");
    try {
      const payload = {
        project_id: projectId,
        epic_id: epicId || null,
        title: title.trim(),
        description: desc || null,
        reviewers: reviewerId ? [reviewerId] : [],
      };
      const created = await api.stories.create(payload);
      // Push into the in-memory USER_STORIES so the picker in
      // new-task-modal sees it immediately without a full reload.
      try {
        if (Array.isArray(window.USER_STORIES)) {
          window.USER_STORIES.unshift({
            id:          created.id,
            projectId:   created.project_id,
            epicId:      created.epic_id || null,
            title:       created.title,
            description: created.description || "",
            status:      created.status || "backlog",
            ownerId:     created.owner_id || null,
            createdBy:   created.created_by || null,
            reviewers:   created.reviewers || [],
            createdAt:   created.created_at,
            updatedAt:   created.updated_at,
          });
        }
      } catch {}
      onCreated && onCreated(created);
      onClose && onClose();
    } catch (e) {
      setErr((e && e.message) || "Could not create story");
      setBusy(false);
    }
  }

  return (
    <Modal open={true} onClose={busy ? undefined : onClose}
           title="New user story"
           subtitle="Group related tasks for end-of-feature review"
           width={520}
           footer={
             <>
               <button className="btn-ghost" onClick={onClose} disabled={busy}>Cancel</button>
               <button className="btn-primary" onClick={submit}
                       disabled={busy || !title.trim() || !projectId}>
                 {busy ? "Creating…" : "Create story"}
               </button>
             </>
           }>
      <div style={{ display: "grid", gap: 14 }}>
        <div className="ms-row">
          <label className="ms-label">Title</label>
          <input className="ms-input" value={title} autoFocus
                 onChange={e => setTitle(e.target.value)}
                 placeholder="As a student, I want to retry a failed payment…"/>
        </div>
        <div className="ms-row">
          <label className="ms-label">
            Description <span className="ms-optional">(optional)</span>
          </label>
          <textarea className="ms-input" rows={3}
                    style={{ height: "auto", minHeight: 60, padding: "8px 10px" }}
                    value={desc}
                    onChange={e => setDesc(e.target.value)}
                    placeholder="What's the goal? Acceptance criteria worth pinning here."/>
        </div>
        <div className="ms-row-split">
          <div className="ms-row">
            <label className="ms-label">Project</label>
            <select className="ms-input" value={projectId}
                    onChange={e => { setProjectId(e.target.value); setEpicId(""); }}>
              {(PROJECTS || []).map(p => (
                <option key={p.id} value={p.id}>{p.name}</option>
              ))}
            </select>
          </div>
          <div className="ms-row">
            <label className="ms-label">
              Epic <span className="ms-optional">(optional)</span>
            </label>
            <select className="ms-input" value={epicId}
                    onChange={e => setEpicId(e.target.value)}>
              <option value="">No epic</option>
              {epicOptions.map(ep => (
                <option key={ep.id} value={ep.id}>{ep.title}</option>
              ))}
            </select>
          </div>
        </div>
        <div className="ms-row">
          <label className="ms-label">
            Reviewer <span className="ms-optional">(can be set later)</span>
          </label>
          <select className="ms-input" value={reviewerId}
                  onChange={e => setReviewerId(e.target.value)}>
            <option value="">— No reviewer yet</option>
            {reviewerPool.map(p => (
              <option key={p.id} value={p.id}>
                {p.name}{p.title && p.title !== "—" ? ` · ${p.title}` : ""}
              </option>
            ))}
          </select>
        </div>
        {err && <div style={{ color: "var(--prio-critical, #c93636)", fontSize: 12 }}>{err}</div>}
      </div>
    </Modal>
  );
}

// ── StoryDrawer ─────────────────────────────────────────────────────
function StoryDrawer({ storyId, onClose, currentUserId, people = [], onTaskOpen }) {
  const [story, setStory]     = React.useState(null);
  const [tasks, setTasks]     = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [busy, setBusy]       = React.useState(false);
  const [err, setErr]         = React.useState("");
  const [changesNote, setChangesNote] = React.useState("");
  const [showChangesForm, setShowChangesForm] = React.useState(false);
  // Edit state — when active, the title + description become editable.
  const [editing, setEditing]               = React.useState(false);
  const [editTitle, setEditTitle]           = React.useState("");
  const [editDescription, setEditDesc]      = React.useState("");

  // Reload the story + child tasks. Called on mount and after any
  // mutating action (approve / request changes / reviewer change).
  const reload = React.useCallback(async () => {
    if (!storyId) return;
    setLoading(true); setErr("");
    try {
      const r = await api.stories.get(storyId);
      setStory(r); setTasks(Array.isArray(r.tasks) ? r.tasks : []);
    } catch (e) {
      setErr((e && e.message) || "Could not load story");
    } finally {
      setLoading(false);
    }
  }, [storyId]);
  React.useEffect(() => { reload(); }, [reload]);

  // Listen for SSE story updates so the drawer stays live while open.
  React.useEffect(() => {
    function onUpdate(e) {
      const id = e && e.detail && e.detail.id;
      if (id === storyId) reload();
    }
    window.addEventListener("flowboard:rt:stories.updated", onUpdate);
    return () => window.removeEventListener("flowboard:rt:stories.updated", onUpdate);
  }, [storyId, reload]);

  if (!storyId) return null;

  function nameOf(uid) {
    if (!uid) return "Unassigned";
    const p = (people || PEOPLE || []).find(x => x && x.id === uid);
    return (p && p.name) || uid;
  }

  const stat = STORY_STATUSES.find(s => s.id === (story && story.status))
            || STORY_STATUSES[0];
  const reviewers = (story && story.reviewers) || [];
  const isReviewer = !!currentUserId && reviewers.includes(currentUserId);
  const inReview   = story && story.status === "review";
  // "Needs review" — reviewer actions stay visible across BOTH the
  // initial review pass AND the changes-requested follow-up. Without
  // this, once the reviewer pressed "Request changes" the buttons
  // disappeared from this drawer (status flipped to changes_req) and
  // they had no way to approve from here after the dev re-completed
  // the work. Mirrors the StoryRow's needsReview gate in table.jsx.
  const needsReview = !!story && isReviewer && (story.status === "review" || story.status === "changes_req");
  // Request-changes is only meaningful while the story is asking for
  // review — once it's already in changes_req there's nothing left to
  // revert. Approve stays enabled in both states so the reviewer can
  // sign off as soon as the dev marks the reopened tasks done.
  const canRequestChanges = needsReview && story.status === "review";

  async function approve() {
    if (busy) return;
    setBusy(true); setErr("");
    try {
      await api.stories.approve(storyId);
      await reload();
    } catch (e) {
      setErr((e && e.message) || "Could not approve");
    } finally { setBusy(false); }
  }
  async function requestChanges() {
    if (busy) return;
    if (!changesNote.trim()) {
      setErr("Add a quick note explaining what to change.");
      return;
    }
    setBusy(true); setErr("");
    try {
      await api.stories.requestChanges(storyId, changesNote.trim());
      setChangesNote(""); setShowChangesForm(false);
      await reload();
    } catch (e) {
      setErr((e && e.message) || "Could not request changes");
    } finally { setBusy(false); }
  }

  // ── Edit / Delete ────────────────────────────────────────────
  function startEdit() {
    if (!story) return;
    setEditTitle(story.title || "");
    setEditDesc(story.description || "");
    setEditing(true);
    setErr("");
  }
  async function saveEdit() {
    if (busy) return;
    if (!editTitle.trim()) { setErr("Title is required."); return; }
    setBusy(true); setErr("");
    try {
      await api.stories.patch(storyId, {
        title: editTitle.trim(),
        description: editDescription || null,
      });
      setEditing(false);
      await reload();
    } catch (e) {
      setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Could not save");
    } finally { setBusy(false); }
  }
  async function remove() {
    if (busy) return;
    const ok = window.confirm(
      "Delete this story?\n\n" +
      "Linked tasks will keep existing — they just lose their story tag.\n" +
      "This action can't be undone."
    );
    if (!ok) return;
    setBusy(true); setErr("");
    try {
      await api.stories.remove(storyId);
      // Patch the live cache so the table updates immediately.
      try {
        const all = window.USER_STORIES;
        if (Array.isArray(all)) {
          const idx = all.findIndex(s => s && s.id === storyId);
          if (idx !== -1) all.splice(idx, 1);
        }
        window.dispatchEvent(new CustomEvent("flowboard:rt:stories.deleted", {
          detail: { id: storyId, local: true },
        }));
      } catch {}
      onClose && onClose();
    } catch (e) {
      setErr((e && e.body && (e.body.message || e.body.error)) || (e && e.message) || "Could not delete");
      setBusy(false);
    }
  }

  const counts = (story && story.task_counts) || { total: tasks.length, done: tasks.filter(t => t.status === "done").length };

  return ReactDOM.createPortal(
    <div className="story-drawer-backdrop" onMouseDown={onClose}>
      <aside className="story-drawer" onMouseDown={e => e.stopPropagation()}>
        <header className="story-drawer-head">
          <div className="story-drawer-title-row">
            <span className={`pill ${stat.cls}`}>{stat.label}</span>
            {editing ? (
              <input
                type="text"
                className="story-drawer-title-input"
                value={editTitle}
                onChange={(e) => setEditTitle(e.target.value)}
                placeholder="Story title"
                autoFocus
                style={{
                  flex: 1, minWidth: 0,
                  fontSize: 18, fontWeight: 700,
                  padding: "6px 10px",
                  border: "1px solid var(--border)", borderRadius: 6,
                  color: "var(--ink-strong)",
                }}/>
            ) : (
              <h2 className="story-drawer-title">
                {loading ? "Loading…" : (story && story.title) || "Story"}
              </h2>
            )}
            {story && !editing && (
              <button className="btn" onClick={startEdit} title="Edit story"
                      style={{ flexShrink: 0, fontSize: 12 }}>
                Edit
              </button>
            )}
            {story && !editing && (
              <button className="btn btn-danger" onClick={remove} title="Delete story"
                      disabled={busy}
                      style={{ flexShrink: 0, fontSize: 12 }}>
                Delete
              </button>
            )}
            <button className="story-drawer-close" onClick={onClose} aria-label="Close">×</button>
          </div>
          {story && (
            <div className="story-drawer-meta">
              <span><b>{counts.done}</b> / {counts.total} tasks done</span>
              <span>·</span>
              <span>
                Reviewer:&nbsp;
                {reviewers.length === 0
                  ? <em style={{ color: "var(--prio-critical, #c93636)" }}>none assigned</em>
                  : reviewers.map(nameOf).join(", ")}
              </span>
            </div>
          )}
        </header>

        {/* Description — either read-only or an inline editor when
            Edit mode is on. The Save / Cancel buttons live next to the
            textarea so the user can commit without scrolling. */}
        {editing ? (
          <div className="story-drawer-section">
            <div className="story-drawer-section-h">Description</div>
            <textarea
              className="ms-input"
              rows={4}
              value={editDescription}
              onChange={(e) => setEditDesc(e.target.value)}
              placeholder="Acceptance criteria, context, links…"
              style={{ width: "100%", minHeight: 80, padding: "8px 10px",
                       border: "1px solid var(--border)", borderRadius: 6,
                       fontFamily: "inherit", fontSize: 13 }}/>
            <div className="story-drawer-actions-row" style={{ marginTop: 10 }}>
              <button className="btn-ghost" onClick={() => setEditing(false)} disabled={busy}>
                Cancel
              </button>
              <button className="btn btn-primary" onClick={saveEdit} disabled={busy}>
                {busy ? "Saving…" : "Save changes"}
              </button>
            </div>
          </div>
        ) : (story && story.description && (
          <div className="story-drawer-section">
            <div className="story-drawer-section-h">Description</div>
            <div className="story-drawer-desc">{story.description}</div>
          </div>
        ))}

        <div className="story-drawer-section">
          <div className="story-drawer-section-h">
            Tasks
            {counts.total > 0 && (
              <span className="story-drawer-progress">
                <span className="story-drawer-progress-bar"
                      style={{ width: Math.round((counts.done / counts.total) * 100) + "%" }}/>
              </span>
            )}
          </div>
          {tasks.length === 0
            ? <div className="story-drawer-empty">No tasks linked yet. Pick this story when creating a task.</div>
            : (
              <ul className="story-drawer-tasks">
                {tasks.map(t => {
                  const ts = STATUSES.find(s => s.id === t.status) || STATUSES[0];
                  return (
                    <li key={t.id}
                        className={"story-drawer-task " + (t.status === "done" ? "is-done" : "")}
                        onClick={() => onTaskOpen && onTaskOpen(t.id)}
                        role="button" tabIndex={0}
                        onKeyDown={e => { if (e.key === "Enter" && onTaskOpen) onTaskOpen(t.id); }}>
                      <span className={`pill ${ts.cls}`}>{ts.label}</span>
                      <span className="story-drawer-task-name">{t.name}</span>
                      <span className="story-drawer-task-meta">
                        {t.due_date ? t.due_date : ""}
                      </span>
                    </li>
                  );
                })}
              </ul>
            )}
        </div>

        {/* Reviewer actions — visible whenever the story wants this
            user's attention (status=review OR status=changes_req with
            the current user listed as a reviewer). Approve always
            available; Request changes only meaningful while still in
            'review'. */}
        {needsReview && (
          <div className="story-drawer-actions">
            {showChangesForm ? (
              <div className="story-drawer-changes-form">
                <textarea
                  rows={3}
                  className="ms-input"
                  style={{ height: "auto", minHeight: 60, padding: "8px 10px" }}
                  placeholder="What needs changing? Devs see this when their tasks revert."
                  value={changesNote}
                  onChange={e => { setChangesNote(e.target.value); if (err) setErr(""); }}/>
                <div className="story-drawer-actions-row">
                  <button className="btn-ghost" onClick={() => { setShowChangesForm(false); setChangesNote(""); setErr(""); }} disabled={busy}>Cancel</button>
                  <button className="btn btn-danger" onClick={requestChanges}
                          disabled={busy || !changesNote.trim()}>
                    {busy ? "Sending…" : "Request changes & revert tasks"}
                  </button>
                </div>
              </div>
            ) : (
              <div className="story-drawer-actions-row">
                <button className="btn"
                        onClick={() => { setShowChangesForm(true); setErr(""); }}
                        disabled={busy || !canRequestChanges}
                        title={canRequestChanges
                          ? "Reverts every Done child task and notifies owners."
                          : "Already in 'Changes Requested' — wait for the dev to mark the reopened tasks done, then Approve."}>
                  Request changes
                </button>
                <button className="btn btn-primary" onClick={approve} disabled={busy}>
                  {busy ? "Approving…" : "Approve & mark done"}
                </button>
              </div>
            )}
          </div>
        )}

        {/* Status hint when the story wants review but the viewer
            can't act (not a listed reviewer). Covers both 'review'
            and 'changes_req' for symmetry with the gating above. */}
        {!!story && (story.status === "review" || story.status === "changes_req") && !isReviewer && reviewers.length > 0 && (
          <div className="story-drawer-section" style={{ color: "var(--ink-muted)", fontSize: 12 }}>
            Waiting on review from {reviewers.map(nameOf).join(", ")}.
          </div>
        )}

        {/* Status hint when in review but no reviewer is set yet. */}
        {inReview && reviewers.length === 0 && (
          <div className="story-drawer-section" style={{ color: "var(--prio-critical, #c93636)", fontSize: 12 }}>
            All tasks are done but no reviewer is assigned. Edit this story to add one.
          </div>
        )}

        {err && <div className="story-drawer-err">{err}</div>}
      </aside>
    </div>,
    document.body
  );
}

// ── ReviewsView ─────────────────────────────────────────────────
// Sidebar destination for "Stories awaiting your review". Pure
// derivation from window.USER_STORIES — no extra API call. Lists
// stories where the current user is in the reviewers array AND
// status is `review` (= ready for sign-off) or `changes_req`
// (= sent back to dev; you'll need to look at it again once they
// re-finish). Click a row → opens StoryDrawer in place.
function ReviewsView({ currentUserId, people, onOpenStory }) {
  const [version, setVersion] = React.useState(0);
  // Refresh whenever any story changes (created/updated/deleted),
  // so the badge + list stay live without polling. The realtime.js
  // module already subscribes to stories.* SSE events — we just
  // listen to the DOM events it dispatches.
  React.useEffect(() => {
    function bump() { setVersion(v => v + 1); }
    window.addEventListener("flowboard:rt:stories.created", bump);
    window.addEventListener("flowboard:rt:stories.updated", bump);
    window.addEventListener("flowboard:rt:stories.deleted", bump);
    return () => {
      window.removeEventListener("flowboard:rt:stories.created", bump);
      window.removeEventListener("flowboard:rt:stories.updated", bump);
      window.removeEventListener("flowboard:rt:stories.deleted", bump);
    };
  }, []);

  const stories = React.useMemo(() => {
    const all = (typeof USER_STORIES !== "undefined" && Array.isArray(USER_STORIES))
      ? USER_STORIES : [];
    return all.filter(s =>
      Array.isArray(s.reviewers) &&
      s.reviewers.includes(currentUserId) &&
      (s.status === "review" || s.status === "changes_req")
    );
  }, [currentUserId, version]);

  // Group: "review" first (action needed now), then "changes_req"
  // (waiting for dev). Sort within each group by updatedAt desc.
  const review      = stories.filter(s => s.status === "review")
                             .sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""));
  const changesReq  = stories.filter(s => s.status === "changes_req")
                             .sort((a, b) => (b.updatedAt || "").localeCompare(a.updatedAt || ""));

  function projectName(pid) {
    const p = (typeof PROJECTS !== "undefined" ? PROJECTS : []).find(x => x.id === pid);
    return p ? p.name : "—";
  }
  function projectColor(pid) {
    const p = (typeof PROJECTS !== "undefined" ? PROJECTS : []).find(x => x.id === pid);
    return p ? p.color : "#7c7c7c";
  }

  if (stories.length === 0) {
    return (
      <div className="reviews-page">
        <div className="reviews-header">
          <h1>Reviews</h1>
          <div className="reviews-sub">Stories waiting on your sign-off.</div>
        </div>
        <div className="reviews-empty">
          <div className="reviews-empty-emoji" aria-hidden="true">✅</div>
          <div className="reviews-empty-title">All caught up</div>
          <div className="reviews-empty-sub">
            Nothing's waiting on your review right now. The list will
            light up when a developer finishes the last task in any
            story you're a reviewer on.
          </div>
        </div>
      </div>
    );
  }

  function Section({ title, items, badge }) {
    if (!items.length) return null;
    return (
      <section className="reviews-section">
        <div className={"reviews-section-h " + (badge || "")}>
          <span>{title}</span>
          <span className="reviews-section-count">{items.length}</span>
        </div>
        <ul className="reviews-list">
          {items.map(s => (
            <li key={s.id} className="reviews-row"
                onClick={() => onOpenStory && onOpenStory(s.id)}
                role="button" tabIndex={0}
                onKeyDown={e => { if (e.key === "Enter" && onOpenStory) onOpenStory(s.id); }}>
              <span className="reviews-row-dot" style={{ background: projectColor(s.projectId) }}/>
              <div className="reviews-row-main">
                <div className="reviews-row-title">{s.title}</div>
                <div className="reviews-row-sub">
                  {projectName(s.projectId)}
                  {s.updatedAt && (
                    <> · updated {new Date(s.updatedAt).toLocaleString()}</>
                  )}
                </div>
              </div>
              <span className={"reviews-row-status pill " + (
                s.status === "review" ? "pill-review" : "pill-blocked"
              )}>
                {s.status === "review" ? "In Review" : "Changes Requested"}
              </span>
            </li>
          ))}
        </ul>
      </section>
    );
  }

  return (
    <div className="reviews-page">
      <div className="reviews-header">
        <h1>Reviews</h1>
        <div className="reviews-sub">
          {review.length} ready for sign-off
          {changesReq.length > 0 && <> · {changesReq.length} sent back</>}
        </div>
      </div>
      <Section title="Ready for your review" items={review} badge="is-review"/>
      <Section title="Sent back to dev (waiting)" items={changesReq} badge="is-changes"/>
    </div>
  );
}

// Helper for the sidebar badge — count of stories awaiting the
// signed-in user's review. Exposed on window so shell.jsx can
// import it without circular imports.
function countReviewsAwaiting(userId) {
  if (!userId) return 0;
  const all = (typeof USER_STORIES !== "undefined" && Array.isArray(USER_STORIES))
    ? USER_STORIES : [];
  return all.filter(s =>
    s && Array.isArray(s.reviewers) &&
    s.reviewers.includes(userId) &&
    s.status === "review"
  ).length;
}

Object.assign(window, { NewStoryModal, StoryDrawer, ReviewsView, countReviewsAwaiting });
