import { useCallback, useEffect, useState } from 'react'
import { api, ApiError } from './api'
import type {
  Connection, HostKeyInfo, JobSnapshot, Options, Plan, PreviewResponse, SourceResponse, TargetInventory,
} from './types'
import { Check, Field, humanBytes, Modal, Notice } from './ui'
import { HostKeyDialog, SshFields } from './SshFields'

export function Sidebar({
  source, plan, includedCount, options, setOptions,
  connections, activeConn, setActiveConn, reloadConnections,
  targetInv, connectTarget, onJobStarted, onError,
}: {
  source: SourceResponse | null
  plan: Plan
  includedCount: number
  options: Options
  setOptions: React.Dispatch<React.SetStateAction<Options>>
  connections: Connection[]
  activeConn: string
  setActiveConn: (id: string) => void
  reloadConnections: () => void
  targetInv: TargetInventory | null
  connectTarget: (id: string) => Promise<void>
  onJobStarted: (j: JobSnapshot) => void
  onError: (msg: string) => void
}) {
  const [editing, setEditing] = useState<Partial<Connection> | null>(null)
  const [hostKey, setHostKey] = useState<HostKeyInfo | null>(null)
  const [preview, setPreview] = useState<PreviewResponse | null>(null)
  const [busy, setBusy] = useState('')
  const [packageName, setPackageName] = useState('')
  const [packageFormat, setPackageFormat] = useState<'tar' | 'dir'>('tar')

  const conn = connections.find((c) => c.id === activeConn)
  const pre = targetInv?.preflight
  const targetReady = !!pre?.serverVersion

  async function withBusy(what: string, fn: () => Promise<void>) {
    setBusy(what)
    try {
      await fn()
    } catch (e) {
      // An untrusted host key is not an error to report but a decision to
      // put in front of the operator, so it opens the fingerprint dialog.
      if (e instanceof ApiError && e.needsTrust) {
        await checkHostKey()
      } else {
        onError(e instanceof Error ? e.message : String(e))
      }
    } finally {
      setBusy('')
    }
  }

  const connect = useCallback(() => {
    if (!activeConn) return
    void withBusy('test', () => connectTarget(activeConn))
    // withBusy and connectTarget are stable for the lifetime of a selection.
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [activeConn])

  // Selecting a target connects to it straight away; if its key is not trusted
  // yet the dialog opens by itself rather than leaving the panel blank.
  useEffect(() => {
    connect()
  }, [connect])

  async function checkHostKey() {
    if (!activeConn) return
    try {
      setHostKey(await api.probe(activeConn))
    } catch (e) {
      onError(e instanceof Error ? e.message : String(e))
    }
  }

  async function trustHostKey() {
    if (!activeConn || !hostKey) return
    await withBusy('trust', async () => {
      await api.trust(activeConn, hostKey.fingerprint)
      setHostKey(null)
      await connectTarget(activeConn)
    })
  }

  const canMigrate = includedCount > 0 && targetReady && !busy
  const canPackage = includedCount > 0 && !busy

  return (
    <>
      <div className="section">
        <h3>target host</h3>
        <div className="stack">
          <div className="row">
            <select value={activeConn} onChange={(e) => setActiveConn(e.target.value)}>
              <option value="">— no target selected —</option>
              {connections.map((c) => (
                <option key={c.id} value={c.id}>{c.name} ({c.user}@{c.host})</option>
              ))}
            </select>
            <button className="btn tiny" onClick={() => setEditing({ port: 22, auth: 'password', saveSecrets: false, sudo: false })}>
              new
            </button>
          </div>

          {conn && (
            <div className="row wrap" style={{ gap: 6 }}>
              <button className="btn tiny" disabled={!!busy} onClick={connect}>
                {busy === 'test' ? 'connecting…' : 'connect'}
              </button>
              <button className="btn tiny" onClick={() => setEditing(conn)}>edit</button>
              <button className="btn tiny" onClick={checkHostKey}>host key</button>
              <button
                className="btn tiny danger"
                onClick={() => {
                  if (!confirm(`Delete connection "${conn.name}"?`)) return
                  withBusy('del', async () => {
                    await api.deleteConnection(conn.id)
                    reloadConnections()
                  })
                }}
              >
                delete
              </button>
            </div>
          )}

          {conn && !targetInv && <div className="small faint">not connected yet</div>}

          {pre && (
            <>
              {(pre.problems ?? []).map((p, i) => (
                <Notice key={i} kind="warn">{p}</Notice>
              ))}
              {targetReady && (
                <dl className="kv">
                  <dt>host</dt><dd>{targetInv?.host || conn?.host}</dd>
                  <dt>docker</dt><dd>{pre.serverVersion} · {pre.os}/{pre.arch}</dd>
                  <dt>free space</dt><dd>{humanBytes(pre.diskFreeBytes)} on {pre.dockerRoot}</dd>
                  <dt>existing</dt>
                  <dd>
                    {(targetInv?.containers ?? []).length} containers ·{' '}
                    {(targetInv?.volumes ?? []).length} volumes
                  </dd>
                  <dt>gzip</dt><dd>{pre.hasGzip ? 'yes' : 'missing'}</dd>
                </dl>
              )}
            </>
          )}
        </div>
      </div>

      <div className="section">
        <h3>options</h3>
        <div className="stack">
          <Field label="if the name already exists on the target">
            <select
              value={options.conflict}
              onChange={(e) => setOptions((o) => ({ ...o, conflict: e.target.value as Options['conflict'] }))}
            >
              <option value="fail">stop with an error</option>
              <option value="skip">skip that container</option>
              <option value="rename">create it under a new name</option>
              <option value="replace">remove the target's container first</option>
            </select>
          </Field>

          {options.conflict === 'rename' && (
            <Field label="suffix">
              <input
                type="text"
                value={options.renameSuffix ?? ''}
                onChange={(e) => setOptions((o) => ({ ...o, renameSuffix: e.target.value }))}
              />
            </Field>
          )}

          {options.conflict === 'replace' && (
            <Notice kind="warn">
              Existing containers and volumes with the same name are deleted on the target before the copy.
            </Notice>
          )}

          <Check
            checked={options.compress}
            onChange={(v) => setOptions((o) => ({ ...o, compress: v }))}
            label="compress transfers (gzip)"
          />
          <Check
            checked={options.verifyAfter}
            onChange={(v) => setOptions((o) => ({ ...o, verifyAfter: v }))}
            label="verify each container after migrating"
          />
          <Check
            checked={options.dryRun}
            onChange={(v) => setOptions((o) => ({ ...o, dryRun: v }))}
            label="dry run — show every command, change nothing"
          />

          <Field label={`containers at a time: ${options.parallelism}`}>
            <input
              type="range"
              min={1}
              max={6}
              value={options.parallelism}
              onChange={(e) => setOptions((o) => ({ ...o, parallelism: Number(e.target.value) }))}
              style={{ width: '100%' }}
            />
          </Field>
        </div>
      </div>

      <div className="section">
        <h3>migrate over ssh</h3>
        <div className="stack">
          <button
            className="btn primary"
            disabled={!canMigrate}
            onClick={() =>
              withBusy('ssh', async () => {
                const j = await api.migrateSSH(activeConn, plan)
                onJobStarted(j)
              })
            }
          >
            {busy === 'ssh' ? 'starting…' : `migrate ${includedCount} container${includedCount === 1 ? '' : 's'} to target`}
          </button>
          <button
            className="btn"
            disabled={includedCount === 0 || !!busy}
            onClick={() =>
              withBusy('preview', async () => {
                setPreview(await api.preview(plan))
              })
            }
          >
            preview the commands
          </button>
          {includedCount === 0 && <div className="small faint">select at least one container</div>}
          {includedCount > 0 && !targetReady && <div className="small faint">connect to a target first</div>}
        </div>
      </div>

      <div className="section">
        <h3>migration package</h3>
        <div className="stack">
          <div className="small muted">
            Builds a self-contained folder with the data, the images and an <span className="mono">install.sh</span> to
            run on the target. No network between the hosts required.
          </div>
          <Field label="package name">
            <input
              type="text"
              placeholder="auto (timestamped)"
              value={packageName}
              onChange={(e) => setPackageName(e.target.value)}
            />
          </Field>
          <Field label="format">
            <select value={packageFormat} onChange={(e) => setPackageFormat(e.target.value as 'tar' | 'dir')}>
              <option value="tar">single .tar file (downloadable)</option>
              <option value="dir">directory on this host</option>
            </select>
          </Field>
          <button
            className="btn"
            disabled={!canPackage}
            onClick={() =>
              withBusy('pkg', async () => {
                const j = await api.buildPackage({ ...plan, packageName }, packageFormat)
                onJobStarted(j)
              })
            }
          >
            {busy === 'pkg' ? 'starting…' : 'build package'}
          </button>
        </div>
      </div>

      {source?.inventory.warnings?.length ? (
        <div className="section">
          <h3>source warnings</h3>
          <div className="stack">
            {source.inventory.warnings.map((w, i) => (
              <Notice key={i} kind="warn">{w}</Notice>
            ))}
          </div>
        </div>
      ) : null}

      {editing && (
        <ConnectionDialog
          initial={editing}
          onClose={() => setEditing(null)}
          onSaved={(c) => {
            setEditing(null)
            reloadConnections()
            setActiveConn(c.id)
          }}
          onError={onError}
        />
      )}

      {hostKey && (
        <HostKeyDialog info={hostKey} onClose={() => setHostKey(null)} onTrust={trustHostKey} />
      )}

      {preview && <PreviewModal data={preview} onClose={() => setPreview(null)} />}
    </>
  )
}

function ConnectionDialog({
  initial, onClose, onSaved, onError,
}: {
  initial: Partial<Connection>
  onClose: () => void
  onSaved: (c: Connection) => void
  onError: (m: string) => void
}) {
  const [c, setC] = useState<Partial<Connection>>(initial)
  const [saving, setSaving] = useState(false)

  function set<K extends keyof Connection>(k: K, v: Connection[K]) {
    setC((prev) => ({ ...prev, [k]: v }))
  }

  return (
    <Modal
      title={initial.id ? `Edit ${initial.name}` : 'New target host'}
      onClose={onClose}
      footer={
        <>
          <button className="btn" onClick={onClose}>cancel</button>
          <button
            className="btn primary"
            disabled={saving || !c.host || !c.user}
            onClick={async () => {
              setSaving(true)
              try {
                onSaved(await api.saveConnection(c))
              } catch (e) {
                onError(e instanceof Error ? e.message : String(e))
              } finally {
                setSaving(false)
              }
            }}
          >
            {saving ? 'saving…' : 'save'}
          </button>
        </>
      }
    >
      <div className="stack" style={{ gap: 12 }}>
        <Field label="label"><input type="text" value={c.name ?? ''} onChange={(e) => set('name', e.target.value)} /></Field>
        <SshFields value={c} set={set} where="target" />
      </div>
    </Modal>
  )
}

function PreviewModal({ data, onClose }: { data: PreviewResponse; onClose: () => void }) {
  const total = data.items.reduce((a, i) => a + i.totalBytes, 0)
  return (
    <Modal
      title="What this migration will run"
      wide
      onClose={onClose}
      footer={<button className="btn" onClick={onClose}>close</button>}
    >
      <div className="stack" style={{ gap: 16 }}>
        <div className="small muted">
          {data.items.length} container(s){total > 0 && <> · about {humanBytes(total)} of known volume data</>}. These are
          the commands that run on the target; data is streamed into <span className="mono">docker cp</span> rather than
          written to a file.
        </div>

        {(data.networkCommands ?? []).length > 0 && (
          <div>
            <h3 style={{ margin: '0 0 6px', fontSize: 12 }}>shared networks</h3>
            <pre className="cmdblock">{(data.networkCommands ?? []).join('\n')}</pre>
          </div>
        )}

        {data.items.map((it) => (
          <div key={it.containerId}>
            <h3 style={{ margin: '0 0 6px', fontSize: 12 }}>
              {it.name}
              {it.targetName !== it.name && <span className="faint"> → {it.targetName}</span>}
            </h3>
            {(it.warnings ?? []).map((w, i) => <Notice key={`w${i}`} kind="warn">{w}</Notice>)}
            {(it.notes ?? []).map((w, i) => <div key={`n${i}`} className="small faint">· {w}</div>)}
            <pre className="cmdblock">{(it.commands ?? []).join('\n')}</pre>
          </div>
        ))}
      </div>
    </Modal>
  )
}
