import { useCallback, useEffect, useState } from 'react'
import { Loading, ErrorState } from '../../../components/PageState.jsx'
import { dateTime } from '../../../lib/format.js'
import { statusOf, actionsFor, declarationNoteFor, needsRestart, parseHosts } from '../../../lib/moduleAdmin.js'
import { api } from '../../../api/client.js'

// Installed modules: install from a release URL, enable, disable, uninstall,
// purge, and restart the server so the changes take effect.
//
// Phase 4, slice 2 of docs/website/MODULE_SYSTEM.md §2.7.2. Everything that
// decides what a row SAYS and which buttons it offers lives in
// lib/moduleAdmin.js, which is plain JS and has tests; this file renders it.
//
// Two things about this screen are unlike the rest of the admin panel and are
// deliberate:
//
//   1. **Restart is a banner, not a per-row button.** A restart is a property of
//      the server, not of a module. Offering it on five rows would suggest
//      otherwise, and an operator who installed three modules should restart
//      once.
//   2. **Disable is the only action that takes effect immediately.** Everything
//      else is "true after the next boot", because the loader reads the volume
//      at require time (§1.12). The buttons say which they are.

const TONE = {
  ok: '#7fd0a4',
  warn: 'var(--accent)',
  bad: '#d98b84',
  idle: 'var(--muted)',
}

const DANGER = { color: '#d98b84', borderColor: '#5b2020' }

function Pill({ tone, children }) {
  return (
    <span
      className="badge"
      style={{ color: TONE[tone] || 'var(--muted)', borderColor: 'var(--line)', background: 'var(--panel-flat)' }}
    >
      {children}
    </span>
  )
}

// ── Install ────────────────────────────────────────────────────────────────

function InstallForm({ sourceHosts, onInstalled }) {
  const [url, setUrl] = useState('')
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState('')
  const [result, setResult] = useState(null)

  async function submit(e) {
    e.preventDefault()
    setError('')
    setResult(null)
    if (!url.trim()) return setError('Paste the URL of a release install manifest.')
    setBusy(true)
    try {
      const res = await api.admin.installModule(url.trim())
      setResult(res)
      setUrl('')
      await onInstalled()
    } catch (err) {
      // The server's message is written to be read by whoever pasted the URL —
      // which host was refused, which hash did not match, what the archive
      // contained. Replacing it with something friendlier would throw away the
      // only part that helps.
      setError(err.message || 'Could not install that module.')
    } finally {
      setBusy(false)
    }
  }

  return (
    <div className="panel" style={{ padding: 22, marginBottom: 22 }}>
      <div className="field-label" style={{ marginBottom: 10 }}>Install a module</div>
      <form onSubmit={submit} style={{ display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap' }}>
        <label style={{ flex: '1 1 380px' }}>
          <span className="field-label">Release install-manifest URL</span>
          <input
            type="url"
            value={url}
            onChange={(e) => setUrl(e.target.value)}
            className="input"
            placeholder="https://gitea.example.com/org/Module-uo/releases/download/v0.3.0/module-uo-0.3.0.json"
          />
        </label>
        <button type="submit" disabled={busy} className="btn btn-primary btn-sq">
          {busy ? 'Installing…' : 'Install'}
        </button>
      </form>

      <p className="sans" style={{ margin: '12px 0 0', fontSize: '0.8rem', color: 'var(--muted)' }}>
        The bundle is downloaded, checked against the <code>sha256</code> its release published, and
        unpacked onto the modules volume. It starts serving after a restart.{' '}
        {sourceHosts.length === 0
          ? 'No source hosts are allowed yet — add one below before installing.'
          : `Allowed hosts: ${sourceHosts.join(', ')}.`}
      </p>

      {error && <p className="sans" style={{ margin: '12px 0 0', color: TONE.bad, fontSize: '0.85rem' }}>{error}</p>}
      {result && (
        <p className="sans" style={{ margin: '12px 0 0', color: TONE.ok, fontSize: '0.85rem' }}>
          {result.replaced ? 'Upgraded' : 'Installed'} {result.module?.name} v{result.module?.version}. Restart to load it.
        </p>
      )}
    </div>
  )
}

// ── The restart banner ─────────────────────────────────────────────────────

function RestartBanner({ onDone }) {
  const [busy, setBusy] = useState(false)
  const [sent, setSent] = useState(false)

  async function restart() {
    // Said plainly, because it is true and because the failure mode is bad: a
    // deployment with no supervisor does not come back on its own.
    const ok = window.confirm(
      'Restart the server now?\n\n'
        + 'The site will be briefly unavailable. It comes back on its own only if something is '
        + 'supervising the process — the shipped Docker Compose file does. If you are running '
        + '`npm start` by hand, you will have to start it again yourself.',
    )
    if (!ok) return
    setBusy(true)
    try {
      await api.admin.restartServer()
      setSent(true)
      // Nothing is coming back on this connection: the process is exiting. Give
      // the supervisor a moment and then reload, which is what the operator was
      // about to do anyway.
      setTimeout(() => { if (onDone) onDone() }, 6000)
    } catch {
      // A failed request here is expected as often as not — the process can win
      // the race and drop the socket before the response lands.
      setSent(true)
      setTimeout(() => { if (onDone) onDone() }, 6000)
    } finally {
      setBusy(false)
    }
  }

  return (
    <div className="panel" style={{ padding: 18, marginBottom: 22, borderColor: 'var(--accent)' }}>
      <div style={{ display: 'flex', gap: 14, alignItems: 'center', flexWrap: 'wrap' }}>
        <div style={{ flex: '1 1 320px' }}>
          <div className="field-label" style={{ marginBottom: 4 }}>Restart needed</div>
          <p className="sans" style={{ margin: 0, fontSize: '0.84rem', color: 'var(--muted)' }}>
            {sent
              ? 'Restarting. This page will reload once the server is back.'
              : 'Modules are read from disk when the server starts, so an install, an uninstall or a re-enable only takes effect after a restart.'}
          </p>
        </div>
        <button type="button" className="btn btn-primary btn-sq" disabled={busy || sent} onClick={restart}>
          {sent ? 'Restarting…' : 'Restart the server'}
        </button>
      </div>
    </div>
  )
}

// ── The source allowlist ───────────────────────────────────────────────────

function SourceHosts({ hosts, onSaved }) {
  const [value, setValue] = useState(hosts.join(', '))
  const [busy, setBusy] = useState(false)
  const [error, setError] = useState('')
  const [saved, setSaved] = useState(false)

  useEffect(() => { setValue(hosts.join(', ')) }, [hosts])

  async function save(e) {
    e.preventDefault()
    setError('')
    setSaved(false)
    setBusy(true)
    try {
      await api.admin.setModuleSources(value)
      setSaved(true)
      await onSaved()
    } catch (err) {
      setError(err.message || 'Could not save the allowlist.')
    } finally {
      setBusy(false)
    }
  }

  const parsed = parseHosts(value)

  return (
    <div className="panel" style={{ padding: 22, marginTop: 22 }}>
      <div className="field-label" style={{ marginBottom: 10 }}>Where modules may be installed from</div>
      <form onSubmit={save} style={{ display: 'flex', gap: 12, alignItems: 'flex-end', flexWrap: 'wrap' }}>
        <label style={{ flex: '1 1 380px' }}>
          <span className="field-label">Allowed hosts</span>
          <input
            type="text"
            value={value}
            onChange={(e) => setValue(e.target.value)}
            className="input"
            placeholder="gitea.example.com, releases.example.org"
          />
        </label>
        <button type="submit" disabled={busy} className="btn btn-sq">{busy ? 'Saving…' : 'Save'}</button>
      </form>

      <p className="sans" style={{ margin: '12px 0 0', fontSize: '0.8rem', color: 'var(--muted)' }}>
        Installing a module runs its code inside this server, so only hosts listed here may be
        installed from — over HTTPS, and re-checked on every redirect. An empty list blocks all
        installs.{' '}
        {parsed.length > 0 && <>Will be saved as: <code>{parsed.join(', ')}</code>.</>}
      </p>

      {error && <p className="sans" style={{ margin: '10px 0 0', color: TONE.bad, fontSize: '0.85rem' }}>{error}</p>}
      {saved && !error && <p className="sans" style={{ margin: '10px 0 0', color: TONE.ok, fontSize: '0.85rem' }}>Saved.</p>}
    </div>
  )
}

// ── One module ─────────────────────────────────────────────────────────────

function ModuleRow({ m, onChanged, onError }) {
  const [busy, setBusy] = useState('')
  const status = statusOf(m)
  const actions = actionsFor(m)
  const note = declarationNoteFor(m)

  async function run(name, fn) {
    setBusy(name)
    try {
      await fn()
      await onChanged()
    } catch (err) {
      onError(err.message || `Could not ${name} ${m.id}.`)
    } finally {
      setBusy('')
    }
  }

  const disable = () => run('disable', () => api.admin.disableModule(m.id))
  const enable = () => run('enable', () => api.admin.enableModule(m.id))

  function uninstall() {
    // The purge choice is made HERE and only here, because purge.sql lives
    // inside the directory the uninstall is about to delete — there is no
    // "purge it later" (§2.7.2 decision 5). Two prompts rather than one, so
    // "delete the data too" is never something you agree to by reflex.
    if (!window.confirm(`Uninstall ${m.name}?\n\nIts files are removed. Its data is kept unless you ask otherwise next.`)) return
    let purge = false
    if (m.canPurge) {
      purge = window.confirm(
        `Also permanently delete ${m.name}'s data?\n\n`
          + 'This drops its tables and cannot be undone. This is the only moment it can be offered — '
          + 'the script that does it is part of the files being removed.\n\n'
          + 'OK deletes the data. Cancel keeps it.',
      )
    }
    return run('uninstall', () => api.admin.uninstallModule(m.id, { purge }))
  }

  function purge() {
    if (!window.confirm(`Permanently delete ${m.name}'s data?\n\nThis drops its tables and cannot be undone.`)) return
    return run('purge', () => api.admin.purgeModule(m.id))
  }

  const forget = () => run('forget', () => api.admin.uninstallModule(m.id))

  return (
    <tr>
      <td className="adm-td" style={{ color: 'var(--text)' }}>
        <div style={{ fontWeight: 600 }}>{m.name}</div>
        <div className="dim" style={{ fontSize: '0.76rem' }}>
          {/* A declared module that has never installed has no version to show —
              only the one MODULES asks for, which the status column carries. */}
          {m.id}{m.version ? ` · v${m.version}` : ''}
        </div>
        {m.capabilities?.length > 0 && (
          <div className="dim" style={{ fontSize: '0.72rem', marginTop: 2 }}>{m.capabilities.join(' · ')}</div>
        )}
      </td>

      <td className="adm-td">
        <Pill tone={status.tone}>{status.label}</Pill>
        <div className="dim" style={{ fontSize: '0.74rem', marginTop: 4, maxWidth: 380 }}>{status.detail}</div>
        {/* The environment's declaration, on its own line: a module can be
            running fine while its declared upgrade is failing, and the status
            above can only be one of those two things. */}
        {note && (
          <div
            style={{
              fontSize: '0.74rem',
              marginTop: 4,
              maxWidth: 380,
              color: note.tone === 'warn' ? TONE.warn : 'var(--muted)',
            }}
          >
            {note.text}
          </div>
        )}
      </td>

      <td className="adm-td dim" style={{ fontSize: '0.74rem' }}>
        {/* Provenance. Null for a directory placed on the volume by hand, which
            stays a supported install — so it is shown as that, not as missing.
            A declared module can also reach a boot with no provenance: the
            no-op path never fetches, so it has no sha256 to record and no
            reason to write a row. Saying "by hand" there would be the one
            wrong answer. */}
        {m.source ? (
          <>
            <div style={{ wordBreak: 'break-all', maxWidth: 260 }}>{m.source}</div>
            {m.sha256 && <div style={{ marginTop: 2 }}>sha256 {m.sha256.slice(0, 12)}…</div>}
          </>
        ) : (
          <span>{m.declared ? 'From the declared module set' : 'Placed on the volume by hand'}</span>
        )}
        {m.installedAt && <div style={{ marginTop: 2 }}>{dateTime(m.installedAt)}</div>}
      </td>

      <td className="adm-td" style={{ textAlign: 'right', whiteSpace: 'nowrap' }}>
        <div style={{ display: 'inline-flex', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
          {actions.disable.shown && (
            <button type="button" className="pill" style={{ fontSize: '0.72rem' }} disabled={Boolean(busy)} onClick={disable}>
              {busy === 'disable' ? 'Stopping…' : 'Disable'}
            </button>
          )}
          {actions.enable.shown && (
            <button type="button" className="pill" style={{ fontSize: '0.72rem' }} disabled={Boolean(busy)} onClick={enable}>
              {busy === 'enable' ? 'Enabling…' : 'Enable'}
            </button>
          )}
          {actions.purge.shown && (
            <button
              type="button"
              className="pill"
              style={{ fontSize: '0.72rem', ...DANGER, opacity: actions.purge.enabled ? 1 : 0.45 }}
              disabled={Boolean(busy) || !actions.purge.enabled}
              title={actions.purge.enabled ? undefined : actions.purge.reason}
              onClick={purge}
            >
              {busy === 'purge' ? 'Purging…' : 'Purge data'}
            </button>
          )}
          {actions.uninstall.shown && (
            <button type="button" className="pill" style={{ fontSize: '0.72rem', ...DANGER }} disabled={Boolean(busy)} onClick={uninstall}>
              {busy === 'uninstall' ? 'Removing…' : 'Uninstall'}
            </button>
          )}
          {actions.forget.shown && (
            <button type="button" className="pill" style={{ fontSize: '0.72rem' }} disabled={Boolean(busy)} onClick={forget}>
              {busy === 'forget' ? 'Clearing…' : 'Clear the row'}
            </button>
          )}
        </div>
      </td>
    </tr>
  )
}

// ── The screen ─────────────────────────────────────────────────────────────

export default function ModulesAdmin() {
  const [data, setData] = useState(null)
  const [error, setError] = useState('')
  const [actionError, setActionError] = useState('')

  const load = useCallback(async () => {
    setError('')
    try {
      setData(await api.admin.listModules())
    } catch {
      setError('Could not load installed modules.')
    }
  }, [])
  useEffect(() => { load() }, [load])

  if (error) return <ErrorState message={error} />
  if (!data) return <Loading />

  const modules = data.modules || []
  const sourceHosts = data.sourceHosts || []

  return (
    <section>
      {needsRestart(modules) && <RestartBanner onDone={() => window.location.reload()} />}

      <InstallForm sourceHosts={sourceHosts} onInstalled={load} />

      {actionError && (
        <p className="sans" style={{ margin: '0 0 14px', color: TONE.bad, fontSize: '0.85rem' }}>{actionError}</p>
      )}

      <div className="panel-flat">
        <table className="adm-table">
          <thead>
            <tr>
              <th className="adm-th">Module</th>
              <th className="adm-th">Status</th>
              <th className="adm-th">Installed from</th>
              <th className="adm-th" />
            </tr>
          </thead>
          <tbody>
            {modules.length === 0 && (
              <tr>
                <td className="adm-td" colSpan={4} style={{ color: 'var(--muted)' }}>
                  No modules installed. Paste a release install-manifest URL above to add one.
                </td>
              </tr>
            )}
            {modules.map((m) => (
              <ModuleRow key={m.id} m={m} onChanged={load} onError={setActionError} />
            ))}
          </tbody>
        </table>
      </div>

      <SourceHosts hosts={sourceHosts} onSaved={load} />
    </section>
  )
}
