import { Alert } from '@/components/ui/Alert'
import { Button } from '@/components/ui/Button'
import { errorMessage } from '@/lib/api'
import { describeCounts, totalChanges, useGardenHistory, useUndo, type ChangeSet } from '@/lib/history'
import { cn } from '@/lib/cn'
import { UndoButton } from './UndoButton'

/**
 * The garden's change history, newest first, with an undo on each entry.
 *
 * A reverted entry stays in the list, marked — and the revert appears as its own
 * entry, because that is what it is. Making the original disappear would be
 * rewriting history rather than adding to it, and would leave no way to undo the
 * undo.
 */
export function HistoryPanel({ gardenId, canEdit }: { gardenId: number; canEdit: boolean }) {
  const history = useGardenHistory(gardenId)
  const undo = useUndo(gardenId)
  const sets = history.data?.pages.flatMap((p) => p.changeSets) ?? []

  return (
    <div className="flex flex-col gap-3">
      <h2 className="text-sm font-semibold text-fg">History</h2>

      {history.isPending && <p className="text-sm text-muted">Loading…</p>}
      {history.isError && sets.length === 0 && (
        <Alert>{errorMessage(history.error, 'Could not load history.')}</Alert>
      )}

      {history.isSuccess && sets.length === 0 && (
        <p className="text-sm text-muted">
          Nothing yet. Every change you make here shows up in this list, and can be undone from it.
        </p>
      )}

      <ol className="flex flex-col gap-2">
        {sets.map((cs) => (
          <HistoryEntry key={cs.id} changeSet={cs} canEdit={canEdit} undo={undo} />
        ))}
      </ol>

      {history.hasNextPage && (
        <>
          <Button
            variant="ghost"
            className="text-sm"
            disabled={history.isFetchingNextPage}
            onClick={() => void history.fetchNextPage()}
          >
            {history.isFetchingNextPage ? 'Loading…' : 'Load older'}
          </Button>
          {/* isError stays true for a failed page even though earlier pages
              loaded, so say so rather than leaving a button that did nothing. */}
          {history.isError && sets.length > 0 && (
            <p className="text-xs text-red-700 dark:text-red-400">
              {errorMessage(history.error, "Couldn't load older entries.")}
            </p>
          )}
        </>
      )}

      {/* Said plainly rather than discovered: deleting a garden bypasses this
          list entirely, because the delete cascades below the layer that records
          changes. Better to state the gap than to imply cover we don't have. */}
      <p className="border-t border-border pt-2 text-xs text-muted">
        Deleting a whole garden isn't covered here and can't be undone.
      </p>
    </div>
  )
}

function HistoryEntry({
  changeSet,
  canEdit,
  undo,
}: {
  changeSet: ChangeSet
  canEdit: boolean
  undo: ReturnType<typeof useUndo>
}) {
  const counts = describeCounts(changeSet)
  const reverted = changeSet.revertedById != null
  const isRevert = changeSet.revertsId != null

  return (
    <li
      className={cn(
        'rounded-lg border border-border px-2.5 py-2 text-sm',
        reverted && 'opacity-60',
      )}
    >
      <div className="flex items-start gap-2">
        <div className="min-w-0 flex-1">
          <p className={cn('text-fg', reverted && 'line-through decoration-muted')}>{changeSet.summary}</p>
          <p className="mt-0.5 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-muted">
            {changeSet.source === 'agent' && (
              <span className="rounded bg-accent/20 px-1 py-px font-medium uppercase tracking-wide text-accent-strong">
                agent
              </span>
            )}
            {isRevert && <span className="rounded bg-border/60 px-1 py-px font-medium">undo</span>}
            <span>{changeSet.actorName}</span>
            <span aria-hidden>·</span>
            <time dateTime={changeSet.createdAt}>{relativeTime(changeSet.createdAt)}</time>
            {counts && (
              <>
                <span aria-hidden>·</span>
                <span>{counts}</span>
              </>
            )}
          </p>
          {reverted && <p className="mt-1 text-xs text-muted">Undone</p>}
        </div>

        {canEdit && totalChanges(changeSet) > 0 && (
          <UndoButton
            changeSet={changeSet}
            undo={undo}
            className="w-32 shrink-0 text-right"
            label={reverted ? 'Undo again' : 'Undo'}
          />
        )}
      </div>
    </li>
  )
}

/** A compact "3m ago" / "yesterday" for a UTC timestamp. */
export function relativeTime(iso: string): string {
  const then = Date.parse(iso)
  if (Number.isNaN(then)) return iso
  const seconds = (Date.now() - then) / 1000
  // Clock skew between the server and this browser can put a just-written entry
  // slightly in the future; that's "just now", not a negative duration.
  if (seconds < 60) return 'just now'
  // Floor throughout: 18 hours ago is "18h ago", not "yesterday". Rounding up
  // into the next unit reads as a bigger gap than actually elapsed.
  const minutes = Math.floor(seconds / 60)
  if (minutes < 60) return `${minutes}m ago`
  const hours = Math.floor(minutes / 60)
  if (hours < 24) return `${hours}h ago`
  const days = Math.floor(hours / 24)
  if (days === 1) return 'yesterday'
  if (days < 30) return `${days}d ago`
  return new Date(then).toLocaleDateString()
}
