// GraduateDialog.jsx — the §13.2 Graduate dialog and the §13.3 flip.
//
// Meta-only topology (SPEC §1): graduation is an in-place state flip on
// the meta entry — no per-RFC repo is created and the body is kept. The
// dialog renders two editable fields (integer ID + initial owners) with
// debounced server-side validation per §13.2. On confirm it opens the
// §13.3 SSE stream and renders the two named steps (open the flip PR,
// merge it). There is no rollback step and no repo-name field.

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
  graduateCheck,
  openGraduationProgress,
  startGraduation,
} from '../api'

const CHECK_DEBOUNCE_MS = 250

const STEP_KEY_ORDER = ['open_pr', 'merge_pr']

export default function GraduateDialog({ slug, entry, onClose, onCompleted }) {
  // Suggest defaults from the catalog.
  const suggestedId = useMemo(() => suggestNextRfcId(entry?.allKnownIds || []), [entry])
  const [rfcId, setRfcId] = useState(suggestedId)
  const [owners, setOwners] = useState(entry?.owners?.length ? entry.owners : [])
  const [newOwner, setNewOwner] = useState('')

  const [checkResult, setCheckResult] = useState(null)
  const [phase, setPhase] = useState('idle')  // idle | running | done | failed | error
  const [streamState, setStreamState] = useState(null)
  const [submitError, setSubmitError] = useState(null)

  const esRef = useRef(null)

  useEffect(() => {
    const t = setTimeout(() => {
      graduateCheck(slug, { id: rfcId })
        .then(setCheckResult)
        .catch(() => {})
    }, CHECK_DEBOUNCE_MS)
    return () => clearTimeout(t)
  }, [slug, rfcId])

  useEffect(() => () => { esRef.current?.close() }, [])

  const idError = checkResult?.id?.error || null
  const ownersOk = owners.length > 0
  const ownersError = ownersOk ? null : 'Add at least one initial owner'

  // First-blocker tooltip text per §13.2.
  const firstBlocker = idError || ownersError
  const canSubmit = !firstBlocker && phase === 'idle' && checkResult?.id?.ok && ownersOk

  const handleAddOwner = useCallback(() => {
    const v = newOwner.trim().toLowerCase()
    if (!v || owners.includes(v)) return
    setOwners(prev => [...prev, v])
    setNewOwner('')
  }, [newOwner, owners])

  const handleRemoveOwner = useCallback((login) => {
    setOwners(prev => prev.filter(o => o !== login))
  }, [])

  const handleConfirm = useCallback(async () => {
    setSubmitError(null)
    setPhase('running')
    try {
      await startGraduation(slug, { rfcId, owners })
    } catch (err) {
      setPhase('idle')
      setSubmitError(err.message)
      return
    }
    esRef.current = openGraduationProgress(slug, {
      onUpdate: (payload) => {
        setStreamState(payload)
        if (payload?.finished) {
          if (payload.succeeded) {
            setPhase('done')
            // Short hold per §13.3, then dismiss.
            setTimeout(() => onCompleted?.(payload), 1500)
          } else {
            setPhase('failed')
          }
        }
      },
      onError: () => {
        setSubmitError('Lost connection to the graduation stream — refresh to see current state.')
        setPhase('error')
      },
    })
  }, [slug, rfcId, owners, onCompleted])

  // ----- Render -----

  const showStack = phase !== 'idle' && (streamState?.steps?.length || 0) > 0

  return (
    <div className="modal-overlay" onClick={(e) => { if (e.target === e.currentTarget && phase === 'idle') onClose?.() }}>
      <div className="modal modal-wide">
        <div className="modal-header">
          <h2>Graduate `{slug}` to active</h2>
          {phase === 'idle' && <button className="modal-close" onClick={onClose}>×</button>}
        </div>

        {!showStack && (
          <div className="modal-body">
            <p className="modal-intro">
              §13: graduate the super-draft to active. This is an in-place
              state flip — the entry keeps its body and stays in the meta
              repo; only the frontmatter changes (state, the optional
              integer ID, and the graduation stamps). No new repository is
              created.
            </p>

            <div className="form-row">
              <label>Integer ID <span className="field-optional">(optional)</span></label>
              <input
                type="text"
                value={rfcId}
                onChange={(e) => setRfcId(e.target.value.trim())}
                placeholder="RFC-NNNN — or leave blank"
                disabled={phase !== 'idle'}
              />
              <p className="field-help">
                Pre-filled as the next free integer; editable to reserve gaps,
                or <strong>clear it to graduate without a number</strong> — the
                slug stays the canonical identifier.
              </p>
              {idError && <p className="field-error">{idError}</p>}
            </div>

            <div className="form-row">
              <label>Initial owners</label>
              <div className="owner-list">
                {owners.length === 0 && <span className="owner-empty">No owners yet — add at least one.</span>}
                {owners.map(o => (
                  <span key={o} className="owner-chip">
                    {o}
                    <button
                      type="button"
                      className="owner-chip-x"
                      onClick={() => handleRemoveOwner(o)}
                      disabled={phase !== 'idle'}
                      aria-label={`Remove ${o}`}
                    >×</button>
                  </span>
                ))}
              </div>
              <div className="owner-picker">
                <input
                  type="text"
                  value={newOwner}
                  onChange={(e) => setNewOwner(e.target.value)}
                  placeholder="Gitea login"
                  disabled={phase !== 'idle'}
                  onKeyDown={(e) => { if (e.key === 'Enter') handleAddOwner() }}
                />
                <button
                  type="button"
                  className="btn-secondary"
                  onClick={handleAddOwner}
                  disabled={phase !== 'idle' || !newOwner.trim()}
                >Add</button>
              </div>
              {ownersError && <p className="field-error">{ownersError}</p>}
            </div>
          </div>
        )}

        {showStack && (
          <div className="modal-body">
            <StepStack steps={streamState?.steps || []} />
            {phase === 'failed' && (
              <div className="what-happened">
                <h3>What happened</h3>
                <p>
                  The graduation could not complete. Because it is a single
                  in-place flip, nothing was left half-applied — `{slug}` stays
                  a super-draft. Error: <code>{streamState?.error || 'unknown'}</code>.
                </p>
                <p>
                  Read the failure detail next to the red step above, resolve
                  the underlying cause (a concurrent PR landing on{' '}
                  `rfcs/{slug}.md`, a network flake), and try again.
                </p>
              </div>
            )}
            {phase === 'done' && (
              <div className="graduation-complete">
                <h3>Graduation complete</h3>
                <p>
                  {streamState?.rfc_id
                    ? <>`{slug}` is now active as <strong>{streamState.rfc_id}</strong>.</>
                    : <>`{slug}` is now active (no number — identified by its slug).</>}
                  {' '}The catalog and the RFC view reflect the new state.
                </p>
              </div>
            )}
          </div>
        )}

        <div className="modal-actions">
          {phase === 'idle' && (
            <>
              <button className="btn-secondary" onClick={onClose}>Cancel</button>
              <button
                className="btn-primary"
                onClick={handleConfirm}
                disabled={!canSubmit}
                title={canSubmit ? '' : firstBlocker || ''}
              >
                Graduate
              </button>
            </>
          )}
          {phase === 'running' && (
            <span className="modal-progress-note">Graduating…</span>
          )}
          {(phase === 'failed' || phase === 'error') && (
            <button className="btn-secondary" onClick={onClose}>Close</button>
          )}
          {phase === 'done' && (
            <button className="btn-primary" onClick={() => onCompleted?.(streamState)}>
              View the RFC
            </button>
          )}
        </div>
        {submitError && phase !== 'failed' && (
          <div className="modal-error">Error: {submitError}</div>
        )}
      </div>
    </div>
  )
}


function StepStack({ steps }) {
  return (
    <div className="step-stack">
      {steps.map(s => <StepRow key={s.key} step={s} />)}
    </div>
  )
}


function StepRow({ step }) {
  return (
    <div className={`step-row step-${step.status}`}>
      <span className={`step-marker step-marker-${step.status}`} />
      <div className="step-text">
        <div className="step-label">{step.label}</div>
        {step.detail && <div className="step-detail">{step.detail}</div>}
      </div>
      <div className={`step-status-pill pill-${step.status}`}>{labelFor(step.status)}</div>
    </div>
  )
}


function labelFor(status) {
  switch (status) {
    case 'pending': return 'pending'
    case 'running': return 'running'
    case 'done': return 'done'
    case 'failed': return 'failed'
    case 'not-reached': return 'not reached'
    default: return status
  }
}


function suggestNextRfcId(existing) {
  const used = new Set()
  for (const id of existing) {
    const m = /^RFC-(\d+)$/.exec(id || '')
    if (m) used.add(Number(m[1]))
  }
  const next = used.size === 0 ? 1 : (Math.max(...used) + 1)
  return `RFC-${String(next).padStart(4, '0')}`
}
