// PRView.jsx — the §10.3 PR review page.
//
// Three-column shape per §8.1: the catalog on the left is the App
// chrome (so it sits outside this component, same as RFCView). This
// component owns the center (diff toggleable between unified and
// split) and the right (compressed conversation + inline review
// surface). A header strip carries title, editable description,
// status, the merge button, and aggregate counts.
//
// The per-user seen-cursor (§10.3) accents new hunks and new
// conversation messages on the next visit. The cursor advances on
// view; reviewers do not mark anything as read.

import { useCallback, useEffect, useMemo, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import {
  advancePRSeen,
  editPRDescription,
  getPR,
  mergePR,
  postPRReview,
  startResolutionBranch,
  withdrawPR,
} from '../api'
import { EVENTS, track } from '../lib/analytics'
import { entryPath, entryPrPath, useProjectId } from '../lib/entryPaths'
import LinkedText from './LinkedText'

export default function PRView({ viewer }) {
  const { slug, prNumber: prNumberParam } = useParams()
  const navigate = useNavigate()
  const pid = useProjectId()
  const prNumber = Number(prNumberParam)

  const [pr, setPR] = useState(null)
  const [error, setError] = useState(null)
  const [diffMode, setDiffMode] = useState('unified') // 'unified' | 'split'
  const [editingHeader, setEditingHeader] = useState(false)
  const [draftTitle, setDraftTitle] = useState('')
  const [draftDescription, setDraftDescription] = useState('')
  const [acting, setActing] = useState(null) // 'merge' | 'withdraw' | 'resolution'

  // Review-comment composer state.
  const [reviewDraft, setReviewDraft] = useState(null) // { quote, anchorPayload }
  const [reviewText, setReviewText] = useState('')

  const refresh = useCallback(() => {
    return getPR(slug, prNumber)
      .then(setPR)
      .catch(err => setError(err.message))
  }, [slug, prNumber])

  useEffect(() => { refresh() }, [refresh])

  // §10.3: advance the seen cursor on view. We snapshot the most
  // recent commit sha and the highest message id we render, then
  // POST. Fire-and-forget; the next read returns the advanced cursor.
  useEffect(() => {
    if (!pr) return
    const headSha = pr.merge_commit_sha || pr.head_sha
    const allMessages = Object.values(pr.messages_by_thread || {}).flat()
    const lastMessageId = allMessages.reduce((m, x) => Math.max(m, x.id || 0), 0)
    if (!headSha && !lastMessageId) return
    if (viewer == null) return
    advancePRSeen(slug, prNumber, {
      lastSeenCommitSha: headSha,
      lastSeenMessageId: lastMessageId || null,
    }).catch(() => {})
  }, [pr?.head_sha, pr?.merge_commit_sha, prNumber, slug, viewer])

  // ── header actions ─────────────────────────────────────────────
  const onMerge = useCallback(async () => {
    if (!confirm(`Merge PR #${prNumber}? §10.5 produces a no-fast-forward merge commit on main.`)) return
    setActing('merge')
    setError(null)
    try {
      await mergePR(slug, prNumber)
      await refresh()
    } catch (e) {
      setError(e.message)
    } finally {
      setActing(null)
    }
  }, [slug, prNumber, refresh])

  const onWithdraw = useCallback(async () => {
    if (!confirm(`Withdraw PR #${prNumber}? The branch is not deleted. §10.8.`)) return
    setActing('withdraw')
    setError(null)
    try {
      await withdrawPR(slug, prNumber)
      await refresh()
    } catch (e) {
      setError(e.message)
    } finally {
      setActing(null)
    }
  }, [slug, prNumber, refresh])

  const onResolutionBranch = useCallback(async () => {
    if (!confirm('Start a resolution branch off main? §10.9 cuts a fresh branch, replays the diff, and opens a new PR.')) return
    setActing('resolution')
    setError(null)
    try {
      const { resolution_branch } = await startResolutionBranch(slug, prNumber)
      navigate(`${entryPath(pid, slug)}?branch=${encodeURIComponent(resolution_branch)}`)
    } catch (e) {
      setError(e.message)
    } finally {
      setActing(null)
    }
  }, [slug, prNumber, navigate, pid])

  const startHeaderEdit = useCallback(() => {
    setDraftTitle(pr?.title || '')
    setDraftDescription(pr?.description || '')
    setEditingHeader(true)
  }, [pr])

  const saveHeader = useCallback(async () => {
    try {
      await editPRDescription(slug, prNumber, {
        title: draftTitle.trim(),
        description: draftDescription.trim(),
      })
      setEditingHeader(false)
      await refresh()
    } catch (e) {
      setError(e.message)
    }
  }, [slug, prNumber, draftTitle, draftDescription, refresh])

  // ── review composer ────────────────────────────────────────────
  const onSubmitReview = useCallback(async () => {
    if (!reviewText.trim()) return
    try {
      await postPRReview(slug, prNumber, {
        text: reviewText.trim(),
        anchorPayload: reviewDraft?.anchorPayload || {},
        quote: reviewDraft?.quote || null,
      })
      // v0.15.0 — analytics: fire on §10.4 review-comment success.
      // surface=pr distinguishes this from RFC discussion comments.
      // No body text or quote material in the event.
      track(EVENTS.COMMENT_POSTED, { rfc_slug: slug, pr_number: prNumber, surface: 'pr' })
      setReviewText('')
      setReviewDraft(null)
      await refresh()
    } catch (e) {
      setError(e.message)
    }
  }, [slug, prNumber, reviewText, reviewDraft, refresh])

  // §10.6: split messages by thread kind for the visual distinction
  // §10.4 requires. Within each kind, sort by id for chronological
  // order. Declared above the early returns below so the hook count
  // stays stable between the loading render (pr == null) and the
  // loaded render (Rules of Hooks — React #310 otherwise).
  const threadsByKind = useMemo(() => {
    const out = { chat: [], review: [], flag: [] }
    for (const t of pr?.threads || []) {
      out[t.thread_kind]?.push(t)
    }
    return out
  }, [pr?.threads])

  // ── render ─────────────────────────────────────────────────────
  if (error) return <article className="entry-view"><p>Error: {error}</p></article>
  if (!pr) return <article className="entry-view">Loading PR…</article>

  const merged = pr.state === 'merged'
  const withdrawn = pr.state === 'withdrawn'
  const closed = pr.state === 'closed'
  const open = pr.state === 'open'
  const supersededBy = pr.superseded_by_pr_number
  const supersedes = pr.supersedes_pr_number

  const seenSha = pr.seen?.last_seen_commit_sha
  const seenMsgId = pr.seen?.last_seen_message_id || 0

  // Compute new-since-cursor accents. A commit is "new" when the PR
  // moved past the seen sha; for v1 we accent the diff whole-cloth
  // when seenSha differs from current head/merge sha (a future slice
  // can render per-hunk accents — the cursor is in place).
  const diffHasNewCommits = seenSha && seenSha !== (pr.merge_commit_sha || pr.head_sha)

  return (
    <div className="rfc-view pr-view">
      {/* Header strip */}
      <div className="pr-header">
        <div className="pr-header-left">
          <div className="pr-breadcrumb">
            <a href={entryPath(pid, slug)}>{pr.rfc_id || pr.slug}</a>
            <span className="breadcrumb-sep">›</span>
            <a href={`${entryPath(pid, slug)}?branch=${encodeURIComponent(pr.head_branch)}`}>{pr.head_branch}</a>
            <span className="breadcrumb-sep">›</span>
            <strong>PR #{pr.pr_number}</strong>
          </div>
          {editingHeader ? (
            <div className="pr-header-edit">
              <input
                type="text"
                className="modal-input"
                value={draftTitle}
                onChange={e => setDraftTitle(e.target.value)}
                maxLength={240}
              />
              <textarea
                className="modal-textarea"
                value={draftDescription}
                onChange={e => setDraftDescription(e.target.value)}
                rows={4}
                maxLength={8000}
              />
              <div className="modal-actions">
                <button className="btn-secondary" onClick={() => setEditingHeader(false)}>Cancel</button>
                <button className="btn-primary" onClick={saveHeader}>Save</button>
              </div>
            </div>
          ) : (
            <>
              <h1 className="pr-title">{pr.title}</h1>
              {pr.description && (
                <p className="pr-description">
                  <LinkedText segments={pr.description_segments} text={pr.description} viewer={viewer} canCreate={viewer?.permission_state === 'granted'} />
                </p>
              )}
              {/* #26: the optional ground-truth use case for this change,
                  captured when the PR was opened. Muted "left blank"
                  treatment when none was supplied. */}
              <div className="pr-use-case" style={{ margin: '6px 0', fontSize: 13 }}>
                <span style={{ fontWeight: 700, color: '#888', textTransform: 'uppercase', letterSpacing: '0.05em', fontSize: 11 }}>
                  Intended use case:
                </span>{' '}
                {pr.proposed_use_case
                  ? <span style={{ whiteSpace: 'pre-wrap' }}>{pr.proposed_use_case}</span>
                  : <span style={{ color: '#999', fontStyle: 'italic' }}>left blank</span>}
              </div>
              {pr.capabilities?.can_edit_text && (
                <button className="btn-link" onClick={startHeaderEdit}>Edit title & description</button>
              )}
            </>
          )}
        </div>
        <div className="pr-header-right">
          <StateBanner state={pr.state} mergedAt={pr.merged_at} closedAt={pr.closed_at} />
          {(supersedes || supersededBy) && (
            <div className="pr-supersedes">
              {supersedes && <>Supersedes <a href={entryPrPath(pid, slug, supersedes)}>#{supersedes}</a></>}
              {supersededBy && <>Closed by <a href={entryPrPath(pid, slug, supersededBy)}>#{supersededBy}</a></>}
            </div>
          )}
          <div className="pr-counts">
            <span className="pr-count-flags" title="Open flags on the source branch (§8.13)">
              {pr.counts.open_flags} flag{pr.counts.open_flags === 1 ? '' : 's'}
            </span>
            <span className="pr-count-reviews">
              {pr.counts.open_review_threads} review thread{pr.counts.open_review_threads === 1 ? '' : 's'}
            </span>
            <span className="pr-count-chats">
              {pr.counts.open_chat_threads} open chat{pr.counts.open_chat_threads === 1 ? '' : 's'}
            </span>
          </div>
          {open && pr.mergeable === false && (
            <div className="pr-conflict-banner">
              <p>
                <strong>Merge conflict with main.</strong> Per §10.9, start a
                resolution branch off main's tip to replay your work.
              </p>
              <button
                className="btn-primary"
                onClick={onResolutionBranch}
                disabled={acting === 'resolution'}
              >
                {acting === 'resolution' ? 'Starting…' : 'Start resolution branch'}
              </button>
            </div>
          )}
          <div className="pr-actions">
            {pr.capabilities?.can_merge && pr.mergeable !== false && (
              <button
                className="btn-primary"
                onClick={onMerge}
                disabled={acting === 'merge'}
              >
                {acting === 'merge' ? 'Merging…' : 'Merge'}
              </button>
            )}
            {pr.capabilities?.can_withdraw && (
              <button
                className="btn-secondary"
                onClick={onWithdraw}
                disabled={acting === 'withdraw'}
              >
                {acting === 'withdraw' ? 'Withdrawing…' : 'Withdraw'}
              </button>
            )}
          </div>
          {!viewer && (
            <a className="btn-link" href="/auth/login">Sign in to review</a>
          )}
        </div>
      </div>

      {/* Two columns under the header */}
      <div className="rfc-body pr-body">
        <div className="editor-area">
          <div className="diff-mode-toolbar">
            <button
              className={`btn-link ${diffMode === 'unified' ? 'active' : ''}`}
              onClick={() => setDiffMode('unified')}
            >Unified</button>
            <button
              className={`btn-link ${diffMode === 'split' ? 'active' : ''}`}
              onClick={() => setDiffMode('split')}
            >Split</button>
            {diffHasNewCommits && (
              <span className="pr-diff-accent" title="New commits since your last visit">
                New since your last visit
              </span>
            )}
          </div>
          <DiffPane
            mainBody={pr.main_body || ''}
            branchBody={pr.branch_body || ''}
            mode={diffMode}
            onReviewRange={(quote, anchorPayload) => {
              if (!pr.capabilities?.can_post_review) return
              setReviewDraft({ quote, anchorPayload })
              setReviewText('')
            }}
          />
        </div>

        <div className="right-panel">
          <PRConversation
            threads={pr.threads || []}
            messagesByThread={pr.messages_by_thread || {}}
            threadsByKind={threadsByKind}
            seenMsgId={seenMsgId}
          />
          {reviewDraft && pr.capabilities?.can_post_review && (
            <div className="pr-review-composer">
              <div className="pr-review-quote">
                <strong>Reviewing:</strong>
                <pre>{reviewDraft.quote || '(no selection)'}</pre>
              </div>
              <textarea
                className="modal-textarea"
                value={reviewText}
                onChange={e => setReviewText(e.target.value)}
                placeholder="Leave a review comment on this range — §10.4."
                rows={4}
              />
              <div className="modal-actions">
                <button className="btn-secondary" onClick={() => { setReviewDraft(null); setReviewText('') }}>Cancel</button>
                <button className="btn-primary" onClick={onSubmitReview} disabled={!reviewText.trim()}>Post review</button>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  )
}

function StateBanner({ state, mergedAt, closedAt }) {
  if (state === 'merged') return <div className="pr-state-banner merged">Merged {fmtDate(mergedAt)}</div>
  if (state === 'withdrawn') return <div className="pr-state-banner withdrawn">Withdrawn {fmtDate(closedAt)}</div>
  if (state === 'closed') return <div className="pr-state-banner closed">Closed {fmtDate(closedAt)}</div>
  return <div className="pr-state-banner open">Open</div>
}

function fmtDate(iso) {
  if (!iso) return ''
  try { return new Date(iso).toLocaleDateString() } catch { return iso }
}

// ─────────────────────────────────────────────────────────────────
// Conversation surface — chat + review interleaved by chronology.
// §8.5: compressed by default. We expand all for v1; the toggle and
// the "Show full conversation" affordance are tracked as a §19.2
// follow-on if usage shows it matters.
// ─────────────────────────────────────────────────────────────────

function PRConversation({ threads, messagesByThread, threadsByKind, seenMsgId }) {
  // Flatten every message with its thread context, sort by id.
  const items = []
  for (const t of threads) {
    const msgs = messagesByThread[t.id] || []
    for (const m of msgs) {
      items.push({ ...m, _thread: t })
    }
    if (t.thread_kind === 'flag' && msgs.length === 0) {
      items.push({
        id: `flag-${t.id}`,
        role: 'system',
        text: t.label || '',
        _thread: t,
        created_at: t.created_at,
      })
    }
  }
  items.sort((a, b) => {
    const ai = typeof a.id === 'number' ? a.id : 0
    const bi = typeof b.id === 'number' ? b.id : 0
    return ai - bi
  })

  return (
    <div className="chat-panel pr-conversation">
      <div className="pr-conv-disclosure">
        {threadsByKind.review.length > 0 && (
          <div className="thread-disclosure">
            <strong>{threadsByKind.review.length}</strong> review thread{threadsByKind.review.length === 1 ? '' : 's'}
          </div>
        )}
        {threadsByKind.flag.length > 0 && (
          <div className="thread-disclosure">
            <strong>{threadsByKind.flag.length}</strong> flag{threadsByKind.flag.length === 1 ? '' : 's'}
          </div>
        )}
      </div>
      <ul className="chat-feed">
        {items.map(m => {
          const t = m._thread
          const isReview = t?.thread_kind === 'review'
          const isFlag = t?.thread_kind === 'flag'
          const isNew = typeof m.id === 'number' && m.id > seenMsgId
          const cls = [
            'chat-msg',
            `chat-msg-${m.role}`,
            isReview ? 'chat-msg-review' : '',
            isFlag ? 'chat-msg-flag' : '',
            isNew ? 'chat-msg-new' : '',
          ].filter(Boolean).join(' ')
          return (
            <li key={m.id} className={cls} data-message-id={m.id}>
              <div className="chat-msg-header">
                {isReview && <span className="chat-msg-badge review">Review</span>}
                {isFlag && <span className="chat-msg-badge flag">Flag</span>}
                <span className="chat-msg-author">
                  {m.role === 'assistant' ? `Claude (${m.model_id || 'ai'})` : (m.author_display || m.author_login || (m.role === 'system' ? 'system' : ''))}
                </span>
                {isNew && <span className="chat-msg-new-pip" title="New since your last visit">●</span>}
              </div>
              {m.quote && <pre className="chat-msg-quote">{m.quote}</pre>}
              <div className="chat-msg-body">
                <LinkedText segments={m.text_segments} text={m.text} viewer={viewer} canCreate={viewer?.permission_state === 'granted'} />
              </div>
            </li>
          )
        })}
      </ul>
    </div>
  )
}

// ─────────────────────────────────────────────────────────────────
// Diff pane — minimal line-by-line unified/split renderer over the
// branch's RFC.md vs main's RFC.md. The §19.2 candidate "DiffView
// reconstruction from changes history" is the long-form follow-on;
// for Slice 3 we serve the file-level diff readers expect.
// ─────────────────────────────────────────────────────────────────

function DiffPane({ mainBody, branchBody, mode, onReviewRange }) {
  const lines = useMemo(() => computeLineDiff(mainBody, branchBody), [mainBody, branchBody])

  if (mode === 'split') {
    return (
      <div className="diff-pane diff-split">
        <div className="diff-col">
          <div className="diff-col-header">main</div>
          {lines.map((l, i) => (
            <DiffLine key={`m${i}`} side="left" type={l.type} text={l.left} onReviewRange={onReviewRange} />
          ))}
        </div>
        <div className="diff-col">
          <div className="diff-col-header">{`branch`}</div>
          {lines.map((l, i) => (
            <DiffLine key={`b${i}`} side="right" type={l.type} text={l.right} onReviewRange={onReviewRange} />
          ))}
        </div>
      </div>
    )
  }

  return (
    <div className="diff-pane diff-unified">
      {lines.map((l, i) => (
        <UnifiedRow key={i} line={l} onReviewRange={onReviewRange} />
      ))}
    </div>
  )
}

function UnifiedRow({ line, onReviewRange }) {
  if (line.type === 'equal') {
    return <div className="diff-row diff-equal" onMouseUp={makeReviewHandler(line.left || line.right, onReviewRange)}>
      <span className="diff-marker"> </span><span className="diff-text">{line.left}</span>
    </div>
  }
  return (
    <>
      {line.left != null && (
        <div className="diff-row diff-del" onMouseUp={makeReviewHandler(line.left, onReviewRange)}>
          <span className="diff-marker">−</span><span className="diff-text">{line.left}</span>
        </div>
      )}
      {line.right != null && (
        <div className="diff-row diff-add" onMouseUp={makeReviewHandler(line.right, onReviewRange)}>
          <span className="diff-marker">+</span><span className="diff-text">{line.right}</span>
        </div>
      )}
    </>
  )
}

function DiffLine({ type, text, onReviewRange }) {
  const cls = `diff-row diff-${type === 'equal' ? 'equal' : (text == null ? 'empty' : type)}`
  return (
    <div className={cls} onMouseUp={makeReviewHandler(text || '', onReviewRange)}>
      <span className="diff-text">{text || ''}</span>
    </div>
  )
}

function makeReviewHandler(rowText, onReviewRange) {
  return () => {
    const sel = window.getSelection?.()
    if (!sel || sel.isCollapsed) return
    const quote = sel.toString()
    if (!quote || !quote.trim()) return
    onReviewRange?.(quote, { row_text: rowText, quote })
  }
}

// Crude longest-common-subsequence-ish line diff. Sufficient for the
// single-file diff we render; switching to a proper diff library is a
// §19.2 candidate ("markdown round-trip fidelity" earned attention
// first). The output is a list of {type, left, right} rows where
// type is one of 'equal' | 'del' | 'add'.
function computeLineDiff(a, b) {
  const aLines = (a || '').split('\n')
  const bLines = (b || '').split('\n')
  const n = aLines.length, m = bLines.length
  const dp = Array(n + 1).fill(null).map(() => Array(m + 1).fill(0))
  for (let i = n - 1; i >= 0; i--) {
    for (let j = m - 1; j >= 0; j--) {
      if (aLines[i] === bLines[j]) dp[i][j] = dp[i + 1][j + 1] + 1
      else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
    }
  }
  const out = []
  let i = 0, j = 0
  while (i < n && j < m) {
    if (aLines[i] === bLines[j]) {
      out.push({ type: 'equal', left: aLines[i], right: bLines[j] })
      i++; j++
    } else if (dp[i + 1][j] >= dp[i][j + 1]) {
      out.push({ type: 'del', left: aLines[i], right: null })
      i++
    } else {
      out.push({ type: 'add', left: null, right: bLines[j] })
      j++
    }
  }
  while (i < n) { out.push({ type: 'del', left: aLines[i++], right: null }) }
  while (j < m) { out.push({ type: 'add', left: null, right: bLines[j++] }) }
  return out
}
