import { useCallback, useEffect, useState } from 'react'
import { Loading, ErrorState } from '../../../components/PageState.jsx'
import { api } from '../../../api/client.js'

// ── Admin · Spawn atlas ─────────────────────────────────────────────────────
//
// The atlas re-derives itself from the shard's ServUO tree on every boot, so
// this panel exists for the three things a restart cannot do:
//
//   • point it at a different tree,
//   • apply a map change without restarting, and
//   • answer a refresh that was parsed but deliberately NOT applied because it
//     would remove a facet.
//
// That last one is the reason the panel is worth building. Losing a facet looks
// exactly like a half-copied or mid-update tree, and boot cannot tell them
// apart — so it stages the decision for a human instead of guessing. Until
// someone decides here, the site keeps serving the atlas it already had.

// A refresh reports its outcome rather than throwing (the boot path must never
// be stopped by a bad tree), so these are answers, not errors — the panel says
// what happened in the shard's terms instead of showing a failure box.
const OUTCOME = {
  imported: (r) =>
    `Imported — ${r.counts?.points?.toLocaleString() ?? '?'} spawners, ${r.counts?.creatures?.toLocaleString() ?? '?'} creatures.`,
  unchanged: (r) =>
    r.reason === 'refresh previously rejected'
      ? 'Unchanged — this exact tree was already reviewed and declined.'
      : 'Unchanged — the tree matches what is already loaded.',
  needsReview: () => 'Staged for review: this refresh would remove a facet, so it was not applied.',
  unavailable: (r) => `The tree could not be read: ${r.reason || 'unknown reason'}`,
  skipped: () => 'No ServUO path is configured, so there is nothing to import.',
  failed: (r) => `Refresh failed: ${r.reason || 'unknown reason'}`,
  rejected: () => 'Declined. It will not be offered again until the tree changes.',
}

const describe = (result) => (OUTCOME[result?.status] || (() => `Result: ${result?.status}`))(result)

function Row({ label, children }) {
  return (
    <div
      className="sans"
      style={{
        display: 'flex',
        alignItems: 'baseline',
        justifyContent: 'space-between',
        gap: 16,
        padding: '7px 0',
        borderBottom: '1px solid var(--line)',
        fontSize: '0.86rem',
      }}
    >
      <span className="dim">{label}</span>
      <span style={{ color: 'var(--head)', textAlign: 'right', wordBreak: 'break-all' }}>{children}</span>
    </div>
  )
}

function PendingReview({ pending, busy, onApprove, onReject }) {
  const declined = pending.status === 'rejected'
  return (
    <section
      style={{
        border: `1px solid ${declined ? 'var(--line)' : '#c58f4a'}`,
        borderRadius: 10,
        padding: 16,
        background: declined ? 'transparent' : 'rgba(197,143,74,0.08)',
      }}
    >
      <h3 className="display" style={{ margin: 0, fontSize: '1rem', color: 'var(--head)' }}>
        {declined ? 'A refresh was declined' : 'A refresh is waiting for you'}
      </h3>
      <p className="sans" style={{ margin: '6px 0 12px', fontSize: '0.86rem', color: 'var(--muted)', lineHeight: 1.6 }}>
        {declined ? (
          <>
            This tree was reviewed and declined, so it is not offered again until the files change.
            Approving now applies it anyway.
          </>
        ) : (
          <>
            The tree parses cleanly but would <strong>remove {pending.removedFacets?.length || 0} facet
            </strong>
            {(pending.removedFacets?.length || 0) === 1 ? '' : 's'} the site is currently serving. That
            is what a half-copied or mid-update tree looks like as well as a real map change, so it was
            not applied. Approving re-parses the tree as it is right now — if you have since fixed the
            mount, what lands is the corrected import.
          </>
        )}
      </p>
      <Row label="Would remove">{(pending.removedFacets || []).join(', ') || '—'}</Row>
      <Row label="Would add">{(pending.addedFacets || []).join(', ') || '—'}</Row>
      <Row label="Detected">{pending.detectedAt ? new Date(pending.detectedAt).toLocaleString() : '—'}</Row>
      <div style={{ display: 'flex', gap: 10, marginTop: 14, flexWrap: 'wrap' }}>
        <button type="button" className="btn btn-primary btn-sq" disabled={busy} onClick={onApprove}>
          Approve and import
        </button>
        {!declined && (
          <button type="button" className="btn btn-sq" disabled={busy} onClick={onReject}>
            Keep the current atlas
          </button>
        )}
      </div>
    </section>
  )
}

export default function SpawnAtlas() {
  const [status, setStatus] = useState(null)
  const [path, setPath] = useState('')
  const [force, setForce] = useState(false)
  const [loading, setLoading] = useState(true)
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState('')
  const [msg, setMsg] = useState('')

  const load = useCallback(async () => {
    setLoading(true)
    setError('')
    try {
      const data = await api.admin.atlas.status()
      setStatus(data)
      setPath(data.path || '')
    } catch (err) {
      setError(err.message || 'Could not load atlas status.')
    } finally {
      setLoading(false)
    }
  }, [])

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

  // Every mutating action shares this: run it, report what it said, then reload
  // status so the panel reflects the world rather than what we assumed happened.
  async function run(action, fn) {
    setBusy(true)
    setMsg('')
    setError('')
    try {
      const result = await fn()
      setMsg(describe(result))
      const fresh = await api.admin.atlas.status()
      setStatus(fresh)
      setPath(fresh.path || '')
    } catch (err) {
      setError(err.message || `Could not ${action}.`)
    } finally {
      setBusy(false)
    }
  }

  async function savePath() {
    setBusy(true)
    setMsg('')
    setError('')
    try {
      const fresh = await api.admin.atlas.setPath(path.trim())
      setStatus(fresh)
      setPath(fresh.path || '')
      setMsg(
        fresh.path === ''
          ? 'Path cleared. The atlas will be skipped on the next boot; what is loaded keeps serving.'
          : fresh.treeReadable
            ? 'Saved. The tree is readable — import when you are ready.'
            : 'Saved, but the tree could not be read from here. Check the mount and permissions.',
      )
    } catch (err) {
      setError(err.message || 'Could not save the path.')
    } finally {
      setBusy(false)
    }
  }

  if (loading) return <Loading />
  if (error && !status) return <ErrorState message={error} />

  const counts = status?.counts || null

  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
      <header>
        <h2 className="display" style={{ margin: 0, fontSize: '1.3rem', color: 'var(--head)' }}>
          Spawn atlas
        </h2>
        <p className="sans" style={{ margin: '6px 0 0', color: 'var(--muted)', fontSize: '0.88rem', lineHeight: 1.6, maxWidth: 760 }}>
          The bestiary and spawn map on the public site, parsed from the shard’s own ServUO files.
          It refreshes itself on every server start; everything here is for the times you don’t want
          to wait for one. Nothing on this page touches the sidecar — the atlas is shard content, not
          shard state, and stays complete while the shard is down.
        </p>
      </header>

      {status?.pending && (
        <PendingReview
          pending={status.pending}
          busy={busy}
          onApprove={() => run('approve the refresh', () => api.admin.atlas.approve())}
          onReject={() => run('decline the refresh', () => api.admin.atlas.reject())}
        />
      )}

      <section style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }}>
        <h3 className="display" style={{ margin: '0 0 10px', fontSize: '1rem', color: 'var(--head)' }}>
          What is loaded
        </h3>
        <Row label="Imported">
          {status?.importedAt ? new Date(status.importedAt).toLocaleString() : 'Never'}
        </Row>
        <Row label="Facets">{status?.facets?.length ? status.facets.join(', ') : '—'}</Row>
        {counts && (
          <>
            <Row label="Spawners">{counts.points?.toLocaleString() ?? '—'}</Row>
            <Row label="Creatures">{counts.creatures?.toLocaleString() ?? '—'}</Row>
            <Row label="Regions / landmarks">
              {`${counts.regions?.toLocaleString() ?? '—'} / ${counts.landmarks?.toLocaleString() ?? '—'}`}
            </Row>
            <Row label="Champion altars">{counts.champions?.toLocaleString() ?? '—'}</Row>
          </>
        )}
        <Row label="Tree readable">
          {!status?.configured ? 'No path set' : status.treeReadable ? 'Yes' : 'No'}
        </Row>
        <Row label="Tree changed since import">
          {status?.drift == null ? '—' : status.drift ? 'Yes — an import would pick it up' : 'No'}
        </Row>
      </section>

      <section style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }}>
        <h3 className="display" style={{ margin: '0 0 4px', fontSize: '1rem', color: 'var(--head)' }}>
          ServUO tree
        </h3>
        <p className="sans" style={{ margin: '0 0 12px', fontSize: '0.84rem', color: 'var(--muted)', lineHeight: 1.6 }}>
          Where the website reads the shard’s spawn files from — the same host, a bind mount or a
          shared volume. This setting wins over the <code>SERVUO_PATH</code> deploy default, so the
          mount can move without a redeploy. Leave it blank to turn the atlas off.
        </p>
        <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap', alignItems: 'center' }}>
          <input
            className="input"
            value={path}
            onChange={(e) => setPath(e.target.value)}
            placeholder="/srv/servuo"
            style={{ flex: '1 1 320px', minWidth: 0 }}
          />
          <button type="button" className="btn btn-sq" disabled={busy} onClick={savePath}>
            Save path
          </button>
        </div>
      </section>

      <section style={{ border: '1px solid var(--line)', borderRadius: 10, padding: 16 }}>
        <h3 className="display" style={{ margin: '0 0 4px', fontSize: '1rem', color: 'var(--head)' }}>
          Re-import
        </h3>
        <p className="sans" style={{ margin: '0 0 12px', fontSize: '0.84rem', color: 'var(--muted)', lineHeight: 1.6 }}>
          Applies a map change without restarting. An unchanged tree costs nothing — the source files
          are hashed first and skipped when they match. A refresh that would remove a facet still
          comes back here for approval rather than being applied.
        </p>
        <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap', alignItems: 'center' }}>
          <button
            type="button"
            className="btn btn-primary btn-sq"
            disabled={busy || !status?.configured}
            onClick={() => run('import the atlas', () => api.admin.atlas.import(force))}
          >
            {busy ? 'Working…' : 'Import now'}
          </button>
          <label className="sans" style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: '0.85rem', cursor: 'pointer' }}>
            <input type="checkbox" checked={force} onChange={(e) => setForce(e.target.checked)} />
            Re-import even if the tree is unchanged
          </label>
        </div>
      </section>

      {(msg || error) && (
        <div style={{ display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' }}>
          {msg && <span className="sans" style={{ color: '#7fd0a4', fontSize: '0.85rem' }}>{msg}</span>}
          {error && <span className="sans" style={{ color: '#d98b84', fontSize: '0.85rem' }}>{error}</span>}
        </div>
      )}
    </div>
  )
}
