import { useEffect, useRef, useState } from 'react'
import { Link, useNavigate, useSearchParams } from 'react-router'
import {
  ArrowLongLeftIcon,
  PlusIcon,
  TrashIcon,
  CheckIcon,
  XMarkIcon,
} from '@heroicons/react/24/outline'
import type { OAuthSession } from '@atproto/oauth-client-browser'
import { useAuth } from '~/lib/auth-context'
import { useData, usePublications } from '~/lib/data-context'
import { createPost, getPublicPost, notifyPostUpdate } from '~/lib/api-client'
import { resolvePdsUrl } from '~/lib/atproto'
import CenteredMessage from '~/components/CenteredMessage'
import Spinner from '~/components/Spinner'

// ─── Types ────────────────────────────────────────────────────────────────────

type KanbanTask = {
  id: string
  label: string
  metadata: Record<string, string>
}

type KanbanColumn = {
  id: string
  label: string
  tasks: KanbanTask[]
}

type BoardState = {
  configBlock: string
  columns: KanbanColumn[]
}

type DropTarget = { colId: string; taskIndex: number } | null

// ─── ID generation ────────────────────────────────────────────────────────────

function newId(): string {
  return `k${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`
}

// ─── Parsing utilities ────────────────────────────────────────────────────────

function parseMetadata(metaStr: string): Record<string, string> {
  const result: Record<string, string> = {}
  const re = /(\w+)\s*:\s*(?:'([^']*)'|([^,}]+))/g
  let m: RegExpExecArray | null
  while ((m = re.exec(metaStr)) !== null) {
    result[m[1]] = m[2] !== undefined ? m[2] : m[3].trim()
  }
  return result
}

function serializeMetadata(meta: Record<string, string>): string {
  const entries = Object.entries(meta)
  if (!entries.length) return ''
  return `@{ ${entries.map(([k, v]) => `${k}: '${v}'`).join(', ')} }`
}

function parseKanbanLine(rawLine: string): { id: string; label: string; metadata: Record<string, string> } {
  let rest = rawLine.trim()
  let metadata: Record<string, string> = {}

  const metaIdx = rest.lastIndexOf('@{')
  if (metaIdx !== -1) {
    const closingIdx = rest.indexOf('}', metaIdx)
    if (closingIdx !== -1) {
      metadata = parseMetadata(rest.slice(metaIdx + 2, closingIdx))
      rest = rest.slice(0, metaIdx).trim()
    }
  }

  const bracketMatch = rest.match(/^([^\[]*)\[([^\]]+)\]$/)
  let id: string, label: string
  if (bracketMatch) {
    id = bracketMatch[1].trim() || newId()
    label = bracketMatch[2].trim()
  } else {
    id = newId()
    label = rest.trim()
  }

  return { id, label, metadata }
}

function parseMermaidKanban(mermaidText: string): BoardState {
  const lines = mermaidText.split('\n')
  const configLines: string[] = []
  let inConfig = false
  let foundKanban = false
  const columns: KanbanColumn[] = []
  let currentColumn: KanbanColumn | null = null

  for (const line of lines) {
    const trimmed = line.trim()

    if (!foundKanban) {
      if (trimmed === '---') {
        configLines.push(line)
        inConfig = !inConfig
        continue
      }
      if (inConfig) {
        configLines.push(line)
        continue
      }
      if (trimmed === 'kanban') {
        foundKanban = true
        continue
      }
      continue
    }

    if (!trimmed) continue

    const indent = line.length - line.trimStart().length

    if (indent <= 2) {
      const item = parseKanbanLine(trimmed)
      currentColumn = { id: item.id, label: item.label, tasks: [] }
      columns.push(currentColumn)
    } else if (currentColumn) {
      const item = parseKanbanLine(trimmed)
      currentColumn.tasks.push(item)
    }
  }

  return { configBlock: configLines.join('\n'), columns }
}

function serializeMermaidKanban(board: BoardState): string {
  const lines: string[] = []
  if (board.configBlock) lines.push(board.configBlock)
  lines.push('kanban')
  for (const col of board.columns) {
    lines.push(`  ${col.id}[${col.label}]`)
    for (const task of col.tasks) {
      const meta = serializeMetadata(task.metadata)
      lines.push(`    ${task.id}[${task.label}]${meta}`)
    }
  }
  return lines.join('\n')
}

function extractMermaidKanban(markdownContent: string): string | null {
  const trimmed = markdownContent.trim()
  const match = trimmed.match(/^```mermaid\n([\s\S]*?)```\s*$/)
  if (!match) return null
  const mermaidText = match[1]
  const withoutFrontmatter = mermaidText.replace(/^---[\s\S]*?---\s*\n/, '')
  if (!withoutFrontmatter.trimStart().startsWith('kanban')) return null
  return mermaidText
}

// ─── Column accent palette ────────────────────────────────────────────────────

const COLUMN_ACCENTS = [
  '#8b5cf6', // violet
  '#10b981', // emerald
  '#f59e0b', // amber
  '#f43f5e', // rose
  '#0ea5e9', // sky
  '#6366f1', // indigo
  '#ec4899', // pink
  '#14b8a6', // teal
]

// ─── Priority badge colors ────────────────────────────────────────────────────

function priorityClass(priority: string): string {
  const p = priority.toLowerCase()
  if (p.includes('very high') || p.includes('critical')) return 'bg-red-100 dark:bg-red-950/60 text-red-600 dark:text-red-400'
  if (p.includes('high')) return 'bg-orange-100 dark:bg-orange-950/60 text-orange-600 dark:text-orange-400'
  if (p.includes('medium')) return 'bg-amber-100 dark:bg-amber-950/60 text-amber-700 dark:text-amber-400'
  if (p.includes('low')) return 'bg-emerald-100 dark:bg-emerald-950/60 text-emerald-700 dark:text-emerald-400'
  return 'bg-stone-100 dark:bg-stone-800 text-stone-500 dark:text-stone-400'
}

// ─── Task card ────────────────────────────────────────────────────────────────

type TaskCardProps = {
  task: KanbanTask
  isEditable: boolean
  isDragging: boolean
  isDropTarget: boolean
  dropPosition: 'before' | 'after' | null
  onUpdate: (label: string, metadata: Record<string, string>) => void
  onDelete: () => void
  onDragStart: () => void
  onDragEnd: () => void
  onDragOver: (e: React.DragEvent, position: 'before' | 'after') => void
  onDrop: (e: React.DragEvent) => void
  initiallyEditing?: boolean
}

function TaskCard({
  task,
  isEditable,
  isDragging,
  isDropTarget,
  dropPosition,
  onUpdate,
  onDelete,
  onDragStart,
  onDragEnd,
  onDragOver,
  onDrop,
  initiallyEditing = false,
}: TaskCardProps) {
  const [editing, setEditing] = useState(initiallyEditing)
  const [value, setValue] = useState(task.label)
  const [metaEdits, setMetaEdits] = useState({ ticket: '', priority: '', assigned: '' })
  const textareaRef = useRef<HTMLTextAreaElement>(null)

  useEffect(() => {
    if (editing) {
      setMetaEdits({
        ticket: task.metadata['ticket'] ?? '',
        priority: task.metadata['priority'] ?? '',
        assigned: task.metadata['assigned'] ?? '',
      })
      textareaRef.current?.focus()
      textareaRef.current?.select()
    }
  }, [editing])

  function commit() {
    const newLabel = value.trim() || task.label
    const known = { ticket: metaEdits.ticket.trim(), priority: metaEdits.priority.trim(), assigned: metaEdits.assigned.trim() }
    const other = Object.fromEntries(Object.entries(task.metadata).filter(([k]) => !(k in known)))
    const newMeta: Record<string, string> = { ...other }
    if (known.ticket) newMeta['ticket'] = known.ticket
    if (known.priority) newMeta['priority'] = known.priority
    if (known.assigned) newMeta['assigned'] = known.assigned
    onUpdate(newLabel, newMeta)
    setValue(newLabel)
    setEditing(false)
  }

  function cancelEdit() {
    setValue(task.label)
    setEditing(false)
  }

  const priority = task.metadata['priority']
  const assigned = task.metadata['assigned']
  const ticket = task.metadata['ticket']
  const hasMeta = !!(priority || assigned || ticket)

  return (
    <div className="relative">
      {isDropTarget && dropPosition === 'before' && (
        <div className="h-0.5 rounded-full bg-violet-400 dark:bg-violet-500 mx-1 mb-2 shadow-[0_0_6px_rgba(139,92,246,0.4)]" />
      )}
      <div
        draggable={isEditable && !editing}
        onDragStart={
          isEditable && !editing
            ? (e) => { e.dataTransfer.effectAllowed = 'move'; onDragStart() }
            : undefined
        }
        onDragEnd={isEditable ? onDragEnd : undefined}
        onDragOver={
          isEditable
            ? (e) => {
                e.preventDefault()
                e.stopPropagation()
                const rect = e.currentTarget.getBoundingClientRect()
                onDragOver(e, e.clientY < rect.top + rect.height / 2 ? 'before' : 'after')
              }
            : undefined
        }
        onDrop={
          isEditable
            ? (e) => { e.stopPropagation(); onDrop(e) }
            : undefined
        }
        className={`group relative rounded-xl bg-white dark:bg-stone-800 p-3 select-none border border-stone-100 dark:border-stone-700/50 transition-all duration-150 ${
          isDragging
            ? 'opacity-40 rotate-1 scale-[0.97] shadow-none'
            : 'shadow-[0_1px_3px_rgba(0,0,0,0.05),0_1px_2px_rgba(0,0,0,0.03)] hover:shadow-[0_4px_12px_rgba(0,0,0,0.09)] hover:-translate-y-px'
        } ${isEditable && !editing ? 'cursor-grab active:cursor-grabbing' : ''}`}
      >
        {editing ? (
          <div onMouseDown={(e) => e.stopPropagation()}>
            <textarea
              ref={textareaRef}
              value={value}
              onChange={(e) => setValue(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); commit() }
                if (e.key === 'Escape') cancelEdit()
              }}
              rows={3}
              className="w-full text-sm text-stone-900 dark:text-white bg-transparent resize-none focus:outline-none leading-snug"
            />
            <div className="mt-2.5 grid grid-cols-3 gap-1.5">
              {(['ticket', 'priority', 'assigned'] as const).map((field) => (
                <div key={field}>
                  <label className="text-[10px] font-medium text-stone-400 dark:text-stone-500 block mb-1 uppercase tracking-wider">{field}</label>
                  <input
                    type="text"
                    value={metaEdits[field]}
                    onChange={(e) => setMetaEdits({ ...metaEdits, [field]: e.target.value })}
                    onKeyDown={(e) => {
                      if (e.key === 'Enter') { e.preventDefault(); commit() }
                      if (e.key === 'Escape') cancelEdit()
                    }}
                    placeholder="—"
                    className="w-full text-xs bg-stone-50 dark:bg-stone-900/80 border border-stone-200 dark:border-stone-700 rounded-lg px-2 py-1.5 text-stone-800 dark:text-stone-200 focus:outline-none focus:border-stone-400 dark:focus:border-stone-600 transition-colors placeholder:text-stone-300 dark:placeholder:text-stone-600"
                  />
                </div>
              ))}
            </div>
            <div className="flex justify-end gap-1 mt-2.5">
              <button
                onMouseDown={(e) => { e.preventDefault(); cancelEdit() }}
                className="p-1.5 text-stone-400 hover:text-stone-600 dark:hover:text-stone-300 rounded-lg hover:bg-stone-100 dark:hover:bg-stone-700 transition-colors"
              >
                <XMarkIcon className="size-3.5" />
              </button>
              <button
                onMouseDown={(e) => { e.preventDefault(); commit() }}
                className="p-1.5 text-stone-400 hover:text-stone-600 dark:hover:text-stone-300 rounded-lg hover:bg-stone-100 dark:hover:bg-stone-700 transition-colors"
              >
                <CheckIcon className="size-3.5" />
              </button>
            </div>
          </div>
        ) : (
          <>
            <p
              onClick={isEditable ? () => { setValue(task.label); setEditing(true) } : undefined}
              className={`text-sm text-stone-800 dark:text-stone-200 leading-snug tracking-[-0.01em] ${isEditable ? 'cursor-text' : ''}`}
            >
              {task.label}
            </p>
            {hasMeta && (
              <div className="mt-2 flex flex-wrap gap-1">
                {ticket && (
                  <span className="text-[11px] px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400 font-mono border border-blue-100 dark:border-blue-900/50">
                    {ticket}
                  </span>
                )}
                {priority && (
                  <span className={`text-[11px] px-2 py-0.5 rounded-full border border-transparent ${priorityClass(priority)}`}>
                    {priority}
                  </span>
                )}
                {assigned && (
                  <span className="text-[11px] px-2 py-0.5 rounded-full bg-stone-100 dark:bg-stone-700/60 text-stone-500 dark:text-stone-400 border border-stone-200 dark:border-stone-700/50">
                    @{assigned}
                  </span>
                )}
              </div>
            )}
            {isEditable && (
              <button
                onClick={(e) => { e.stopPropagation(); onDelete() }}
                className="absolute top-2 right-2 hidden group-hover:flex p-1 text-stone-300 dark:text-stone-600 hover:text-red-400 dark:hover:text-red-500 transition-colors rounded-lg"
                title="Delete task"
              >
                <TrashIcon className="size-3.5" />
              </button>
            )}
          </>
        )}
      </div>
      {isDropTarget && dropPosition === 'after' && (
        <div className="h-0.5 rounded-full bg-violet-400 dark:bg-violet-500 mx-1 mt-2 shadow-[0_0_6px_rgba(139,92,246,0.4)]" />
      )}
    </div>
  )
}

// ─── Column card ──────────────────────────────────────────────────────────────

type ColumnCardProps = {
  column: KanbanColumn
  isEditable: boolean
  isDragOver: boolean
  draggingTaskId: string | null
  dropTarget: DropTarget
  pendingEditTaskId: string | null
  accentColor: string
  onUpdateLabel: (label: string) => void
  onDelete: () => void
  onAddTask: () => void
  onUpdateTask: (taskId: string, label: string, metadata: Record<string, string>) => void
  onDeleteTask: (taskId: string) => void
  onDragStart: (taskId: string) => void
  onDragEnd: () => void
  onColumnDragOver: (e: React.DragEvent) => void
  onColumnDrop: (e: React.DragEvent) => void
  onTaskDragOver: (e: React.DragEvent, taskId: string, position: 'before' | 'after') => void
  onTaskDrop: (e: React.DragEvent, taskId: string) => void
}

function ColumnCard({
  column,
  isEditable,
  isDragOver,
  draggingTaskId,
  dropTarget,
  pendingEditTaskId,
  accentColor,
  onUpdateLabel,
  onDelete,
  onAddTask,
  onUpdateTask,
  onDeleteTask,
  onDragStart,
  onDragEnd,
  onColumnDragOver,
  onColumnDrop,
  onTaskDragOver,
  onTaskDrop,
}: ColumnCardProps) {
  const [editingLabel, setEditingLabel] = useState(false)
  const [labelValue, setLabelValue] = useState(column.label)
  const inputRef = useRef<HTMLInputElement>(null)

  useEffect(() => {
    if (editingLabel && inputRef.current) {
      inputRef.current.focus()
      inputRef.current.select()
    }
  }, [editingLabel])

  function commitLabel() {
    const trimmed = labelValue.trim()
    if (trimmed) onUpdateLabel(trimmed)
    else setLabelValue(column.label)
    setEditingLabel(false)
  }

  const isAtEndDropTarget =
    dropTarget?.colId === column.id && dropTarget.taskIndex === column.tasks.length

  return (
    <div
      className="shrink-0 w-80 flex flex-col rounded-2xl bg-white dark:bg-stone-900 overflow-hidden transition-all duration-200"
      style={{
        borderTop: `3px solid ${accentColor}`,
        boxShadow: isDragOver
          ? `0 0 0 2px ${accentColor}44, 0 8px 28px rgba(0,0,0,0.12)`
          : '0 1px 4px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04)',
      }}
      onDragOver={onColumnDragOver}
      onDrop={onColumnDrop}
    >
      {/* Header */}
      <div className="flex items-center justify-between px-4 py-3.5 gap-2">
        {editingLabel ? (
          <input
            ref={inputRef}
            value={labelValue}
            onChange={(e) => setLabelValue(e.target.value)}
            onBlur={commitLabel}
            onKeyDown={(e) => {
              if (e.key === 'Enter') commitLabel()
              if (e.key === 'Escape') { setLabelValue(column.label); setEditingLabel(false) }
            }}
            className="flex-1 text-sm font-semibold text-stone-900 dark:text-white bg-transparent border-b-2 focus:outline-none py-0.5 min-w-0 transition-colors"
            style={{ borderColor: accentColor }}
          />
        ) : (
          <button
            onClick={isEditable ? () => { setLabelValue(column.label); setEditingLabel(true) } : undefined}
            disabled={!isEditable}
            className={`flex-1 text-sm font-semibold text-stone-900 dark:text-white text-left truncate min-w-0 tracking-[-0.01em] ${
              isEditable ? 'hover:opacity-70 transition-opacity' : 'cursor-default'
            }`}
          >
            {column.label}
          </button>
        )}
        <div className="flex items-center gap-1.5 shrink-0">
          <span
            className="text-xs font-semibold tabular-nums px-2 py-0.5 rounded-full"
            style={{ backgroundColor: `${accentColor}18`, color: accentColor }}
          >
            {column.tasks.length}
          </span>
          {isEditable && (
            <button
              onClick={onDelete}
              className="p-1 text-stone-300 dark:text-stone-600 hover:text-red-400 dark:hover:text-red-500 transition-colors rounded-lg"
              title="Delete column"
            >
              <TrashIcon className="size-3.5" />
            </button>
          )}
        </div>
      </div>

      <div className="h-px bg-stone-100 dark:bg-stone-800/80 mx-4" />

      {/* Tasks */}
      <div className="flex flex-col gap-2 p-3 flex-1 min-h-[5rem]">
        {column.tasks.map((task, idx) => {
          const isThisDropTarget = dropTarget?.colId === column.id && dropTarget.taskIndex === idx
          const isNextDropTarget = dropTarget?.colId === column.id && dropTarget.taskIndex === idx + 1
          return (
            <TaskCard
              key={task.id}
              task={task}
              isEditable={isEditable}
              isDragging={draggingTaskId === task.id}
              isDropTarget={isThisDropTarget || isNextDropTarget}
              dropPosition={isThisDropTarget ? 'before' : isNextDropTarget ? 'after' : null}
              onUpdate={(label, metadata) => onUpdateTask(task.id, label, metadata)}
              onDelete={() => onDeleteTask(task.id)}
              onDragStart={() => onDragStart(task.id)}
              onDragEnd={onDragEnd}
              onDragOver={(e, position) => onTaskDragOver(e, task.id, position)}
              onDrop={(e) => onTaskDrop(e, task.id)}
              initiallyEditing={pendingEditTaskId === task.id}
            />
          )
        })}
        {isAtEndDropTarget && (
          <div className="h-0.5 rounded-full bg-violet-400 dark:bg-violet-500 mx-1 shadow-[0_0_6px_rgba(139,92,246,0.4)]" />
        )}
      </div>

      {/* Add task */}
      {isEditable && (
        <div className="px-3 pb-3">
          <button
            onClick={onAddTask}
            className="w-full flex items-center gap-1.5 px-3 py-2 rounded-xl text-sm text-stone-400 dark:text-stone-500 hover:text-stone-700 dark:hover:text-stone-300 hover:bg-stone-50 dark:hover:bg-stone-800/60 transition-all duration-150"
          >
            <PlusIcon className="size-3.5" />
            Add task
          </button>
        </div>
      )}
    </div>
  )
}

// ─── Default kanban content ───────────────────────────────────────────────────

const DEFAULT_KANBAN_CONTENT = [
  '```mermaid',
  'kanban',
  '  todo[To Do]',
  '    task1[First Task]',
  '  inprog[In Progress]',
  '  done[Done]',
  '```',
].join('\n')

// ─── Create board ─────────────────────────────────────────────────────────────

function CreateBoard({ session }: { session: OAuthSession }) {
  const navigate = useNavigate()
  const publications = usePublications()
  const { invalidatePosts } = useData()
  const [selectedPubUri, setSelectedPubUri] = useState('')
  const [boardTitle, setBoardTitle] = useState('New Kanban Board')
  const [creating, setCreating] = useState(false)
  const [createError, setCreateError] = useState<string | null>(null)

  useEffect(() => {
    if (publications && publications.length > 0 && !selectedPubUri) {
      setSelectedPubUri(publications[0].atUri)
    }
  }, [publications])

  async function handleCreate() {
    if (!selectedPubUri || !boardTitle.trim()) return
    setCreating(true)
    setCreateError(null)
    try {
      const post = await createPost(session, selectedPubUri, boardTitle.trim(), DEFAULT_KANBAN_CONTENT, 'published')
      invalidatePosts()
      navigate(`/kanban?uri=${encodeURIComponent(post.atUri)}`)
    } catch (err) {
      setCreateError(err instanceof Error ? err.message : 'Failed to create board')
      setCreating(false)
    }
  }

  if (publications === null) {
    return (
      <div className="flex-1 flex items-center justify-center">
        <Spinner />
      </div>
    )
  }

  if (publications.length === 0) {
    return <CenteredMessage message="Create a publication before creating a kanban board." action={{ label: 'Go Home', to: '/' }} />
  }

  return (
    <div className="flex-1 flex items-center justify-center px-4">
      <div className="w-full max-w-sm space-y-5">
        <h1 className="text-xl font-semibold text-stone-900 dark:text-white">New Kanban Board</h1>
        <div className="space-y-3">
          <div>
            <label className="text-sm text-stone-600 dark:text-stone-400 block mb-1.5">Title</label>
            <input
              type="text"
              value={boardTitle}
              onChange={(e) => setBoardTitle(e.target.value)}
              onKeyDown={(e) => { if (e.key === 'Enter') handleCreate() }}
              className="w-full border border-stone-300 dark:border-stone-600 rounded-lg px-3 py-2 bg-white dark:bg-stone-900 text-stone-900 dark:text-white text-sm focus:outline-none focus:border-stone-500 dark:focus:border-stone-400"
            />
          </div>
          {publications.length > 1 && (
            <div>
              <label className="text-sm text-stone-600 dark:text-stone-400 block mb-1.5">Publication</label>
              <select
                value={selectedPubUri}
                onChange={(e) => setSelectedPubUri(e.target.value)}
                className="w-full border border-stone-300 dark:border-stone-600 rounded-lg px-3 py-2 bg-white dark:bg-stone-900 text-stone-900 dark:text-white text-sm focus:outline-none focus:border-stone-500 dark:focus:border-stone-400"
              >
                {publications.map((p) => (
                  <option key={p.atUri} value={p.atUri}>{p.name || p.url}</option>
                ))}
              </select>
            </div>
          )}
          {createError && (
            <p className="text-sm text-red-500 dark:text-red-400">{createError}</p>
          )}
          <button
            onClick={handleCreate}
            disabled={creating || !selectedPubUri || !boardTitle.trim()}
            className="w-full px-4 py-2 bg-stone-900 dark:bg-stone-100 text-white dark:text-stone-900 rounded-lg text-sm font-medium hover:bg-stone-700 dark:hover:bg-stone-300 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
          >
            {creating ? 'Creating…' : 'Create Board'}
          </button>
        </div>
      </div>
    </div>
  )
}

// ─── Main page ────────────────────────────────────────────────────────────────

export default function KanbanPage() {
  const [searchParams] = useSearchParams()
  const { session } = useAuth()

  const atUri = searchParams.get('uri') ?? ''

  const [board, setBoard] = useState<BoardState | null>(null)
  const [error, setError] = useState<string | null>(null)
  const [loading, setLoading] = useState(true)
  const [saving, setSaving] = useState(false)
  const [saveError, setSaveError] = useState<string | null>(null)
  const [saved, setSaved] = useState(false)
  const [title, setTitle] = useState<string | null>(null)
  const [ownerDid, setOwnerDid] = useState<string | null>(null)
  const [pendingEditTaskId, setPendingEditTaskId] = useState<string | null>(null)

  // Drag state
  const [draggingTask, setDraggingTask] = useState<{ id: string; fromColId: string } | null>(null)
  const [dragOverColId, setDragOverColId] = useState<string | null>(null)
  const [dropTarget, setDropTarget] = useState<DropTarget>(null)

  const isOwner = !!session && ownerDid === session.sub
  const isEditable = isOwner

  // ── Load record ──────────────────────────────────────────────────────────────

  useEffect(() => {
    if (!atUri) {
      setLoading(false)
      return
    }

    let cancelled = false

    async function load() {
      try {
        const parts = atUri.split('/')
        if (parts.length < 5 || parts[0] !== 'at:' || parts[1] !== '') {
          throw new Error('Invalid AT URI — expected at://did/collection/rkey')
        }
        const did = parts[2]
        const collection = parts[3]
        const rkey = parts[4]

        if (collection !== 'site.standard.document') {
          throw new Error(`Expected collection site.standard.document, got ${collection}`)
        }

        const indexData = await getPublicPost(did, rkey)
        if (!indexData) throw new Error('Post not found')
        if (typeof indexData.content !== 'string') throw new Error('Post content is empty')

        const mermaidText = extractMermaidKanban(indexData.content)
        if (!mermaidText) throw new Error('Post content is not a Mermaid kanban diagram')

        if (!cancelled) {
          setBoard(parseMermaidKanban(mermaidText))
          setTitle(indexData.title)
          setOwnerDid(did)
        }
      } catch (err) {
        if (!cancelled) setError(err instanceof Error ? err.message : 'Failed to load record')
      } finally {
        if (!cancelled) setLoading(false)
      }
    }

    load()
    return () => { cancelled = true }
  }, [atUri])

  // ── Save ─────────────────────────────────────────────────────────────────────

  async function handleSave() {
    if (!session || !board || !ownerDid) return
    setSaving(true)
    setSaveError(null)
    setSaved(false)
    try {
      const parts = atUri.split('/')
      const collection = parts[3]
      const rkey = parts[4]

      const mermaidText = serializeMermaidKanban(board)
      const newMarkdown = '```mermaid\n' + mermaidText + '\n```'
      const updatedAt = new Date().toISOString()

      // Fetch the current PDS record to preserve all fields (coverImage blob ref,
      // site, path, etc.) that aren't stored in the index.
      const pdsUrl = await resolvePdsUrl(ownerDid)
      const getRes = await fetch(
        `${pdsUrl}/xrpc/com.atproto.repo.getRecord?` +
        `repo=${encodeURIComponent(ownerDid)}&collection=${encodeURIComponent(collection)}&rkey=${encodeURIComponent(rkey)}`
      )
      if (!getRes.ok) throw new Error(`Failed to fetch record (${getRes.status})`)
      const { value } = await getRes.json() as { value: Record<string, unknown> }

      const existingContent = value['content'] as Record<string, unknown> | undefined
      const existingLicense = existingContent?.['license'] as string | undefined
      const updatedRecord = {
        ...value,
        updatedAt,
        content: { ...existingContent, $type: 'pub.lemma.blog.entry', content: newMarkdown },
      }

      const putRes = await session.fetchHandler(
        `${pdsUrl}/xrpc/com.atproto.repo.putRecord`,
        {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ repo: ownerDid, collection, rkey, record: updatedRecord }),
        }
      )
      if (!putRes.ok) {
        const body = await putRes.text().catch(() => '')
        throw new Error(`Save failed (${putRes.status})${body ? `: ${body}` : ''}`)
      }

      await notifyPostUpdate(session, {
        atUri: `at://${ownerDid}/${collection}/${rkey}`,
        site: value['site'] as string,
        rkey,
        title: value['title'] as string,
        publishedAt: value['publishedAt'] as string,
        updatedAt,
        content: newMarkdown,
        coverImageRef: (value['coverImage'] as { ref?: { $link?: string } } | undefined)?.ref?.$link,
        bskyPostRef: value['bskyPostRef'] as { uri: string; cid: string } | undefined,
        license: existingLicense,
      })

      setSaved(true)
      setTimeout(() => setSaved(false), 3000)
    } catch (err) {
      setSaveError(err instanceof Error ? err.message : 'Failed to save')
    } finally {
      setSaving(false)
    }
  }

  // ── Board mutations ───────────────────────────────────────────────────────────

  function addColumn() {
    if (!board) return
    const id = newId()
    setBoard({ ...board, columns: [...board.columns, { id, label: 'New Column', tasks: [] }] })
  }

  function updateColumn(colId: string, label: string) {
    if (!board) return
    setBoard({ ...board, columns: board.columns.map((c) => c.id === colId ? { ...c, label } : c) })
  }

  function deleteColumn(colId: string) {
    if (!board) return
    setBoard({ ...board, columns: board.columns.filter((c) => c.id !== colId) })
  }

  function addTask(colId: string) {
    if (!board) return
    const id = newId()
    setBoard({
      ...board,
      columns: board.columns.map((c) =>
        c.id === colId ? { ...c, tasks: [...c.tasks, { id, label: 'New Task', metadata: {} }] } : c
      ),
    })
    setPendingEditTaskId(id)
  }

  function updateTask(colId: string, taskId: string, label: string, metadata: Record<string, string>) {
    if (!board) return
    setBoard({
      ...board,
      columns: board.columns.map((c) =>
        c.id === colId
          ? { ...c, tasks: c.tasks.map((t) => t.id === taskId ? { ...t, label, metadata } : t) }
          : c
      ),
    })
  }

  function deleteTask(colId: string, taskId: string) {
    if (!board) return
    setBoard({
      ...board,
      columns: board.columns.map((c) =>
        c.id === colId ? { ...c, tasks: c.tasks.filter((t) => t.id !== taskId) } : c
      ),
    })
  }

  // ── Drag and drop ─────────────────────────────────────────────────────────────

  function handleDragStart(taskId: string, fromColId: string) {
    setDraggingTask({ id: taskId, fromColId })
    setDropTarget(null)
  }

  function handleDragEnd() {
    setDraggingTask(null)
    setDragOverColId(null)
    setDropTarget(null)
  }

  function handleColumnDragOver(e: React.DragEvent, colId: string) {
    e.preventDefault()
    setDragOverColId(colId)
    const col = board?.columns.find((c) => c.id === colId)
    if (col) {
      // Drop at end of column (used when not hovering over a specific task)
      setDropTarget({ colId, taskIndex: col.tasks.length })
    }
  }

  function handleColumnDrop(e: React.DragEvent, targetColId: string) {
    e.preventDefault()
    if (!draggingTask || !board) { handleDragEnd(); return }
    const target = dropTarget ?? { colId: targetColId, taskIndex: board.columns.find((c) => c.id === targetColId)?.tasks.length ?? 0 }
    performDrop(target)
  }

  function handleTaskDragOver(e: React.DragEvent, targetTaskId: string, targetColId: string, position: 'before' | 'after') {
    e.preventDefault()
    setDragOverColId(targetColId)
    const col = board?.columns.find((c) => c.id === targetColId)
    if (!col) return
    const taskIdx = col.tasks.findIndex((t) => t.id === targetTaskId)
    if (taskIdx === -1) return
    const insertIdx = position === 'before' ? taskIdx : taskIdx + 1
    setDropTarget({ colId: targetColId, taskIndex: insertIdx })
  }

  function handleTaskDrop(e: React.DragEvent, targetTaskId: string, targetColId: string) {
    e.preventDefault()
    if (!draggingTask || !board || !dropTarget) { handleDragEnd(); return }
    performDrop(dropTarget)
  }

  function performDrop(target: DropTarget) {
    if (!draggingTask || !board || !target) { handleDragEnd(); return }
    const { id: taskId, fromColId } = draggingTask

    const sourceCol = board.columns.find((c) => c.id === fromColId)
    const movedTask = sourceCol?.tasks.find((t) => t.id === taskId)
    if (!movedTask) { handleDragEnd(); return }

    // Remove from source
    const withoutTask = board.columns.map((c) => {
      if (c.id !== fromColId) return c
      return { ...c, tasks: c.tasks.filter((t) => t.id !== taskId) }
    })

    // Insert at target
    const finalColumns = withoutTask.map((c) => {
      if (c.id !== target.colId) return c
      const tasks = [...c.tasks]
      // If same column, the source removal shifts indices — recalculate
      let insertIdx = target.taskIndex
      if (fromColId === target.colId) {
        const origIdx = sourceCol!.tasks.findIndex((t) => t.id === taskId)
        if (origIdx < target.taskIndex) insertIdx = Math.max(0, target.taskIndex - 1)
      }
      tasks.splice(Math.min(insertIdx, tasks.length), 0, movedTask)
      return { ...c, tasks }
    })

    setBoard({ ...board, columns: finalColumns })
    handleDragEnd()
  }

  // ── Render ───────────────────────────────────────────────────────────────────

  if (!atUri) {
    if (!session) {
      return (
        <div className="flex-1 flex items-center justify-center">
          <p className="text-stone-500 dark:text-stone-400 text-sm">Sign in to create a kanban board.</p>
        </div>
      )
    }
    return <CreateBoard session={session} />
  }

  if (loading) {
    return (
      <div className="flex-1 flex items-center justify-center">
        <Spinner />
      </div>
    )
  }

  if (error) {
    return <CenteredMessage message={error} error action={{ label: 'Go home', to: '/' }} />
  }

  if (!board) return null

  return (
    <div className="flex-1 flex flex-col min-h-0 overflow-hidden">
      {/* Header */}
      <div className="flex items-center justify-between px-4 md:px-6 py-3 border-b border-stone-100 dark:border-stone-800/80 shrink-0 gap-3 bg-white/90 dark:bg-stone-950/90 backdrop-blur-md">
        <div className="flex items-center gap-3 min-w-0">
          <Link
            to="/"
            className="text-stone-400 hover:text-stone-700 dark:hover:text-stone-200 transition-colors shrink-0"
          >
            <ArrowLongLeftIcon className="size-5" />
          </Link>
          <span className="text-sm font-semibold text-stone-900 dark:text-white truncate tracking-[-0.01em]">
            {title ?? 'Kanban Board'}
          </span>
          {!isEditable && (
            <span className="text-[11px] px-2.5 py-0.5 rounded-full bg-stone-100 dark:bg-stone-800 text-stone-500 dark:text-stone-400 shrink-0 font-medium">
              Read-only
            </span>
          )}
        </div>

        {isEditable && (
          <div className="flex items-center gap-3 shrink-0">
            {saveError && (
              <p className="text-xs text-red-500 dark:text-red-400 max-w-xs truncate">{saveError}</p>
            )}
            {saved && (
              <span className="text-[11px] font-medium text-emerald-600 dark:text-emerald-400 px-2.5 py-0.5 rounded-full bg-emerald-50 dark:bg-emerald-950/50">
                Saved
              </span>
            )}
            <button
              onClick={handleSave}
              disabled={saving}
              className="rounded-full bg-stone-900 dark:bg-stone-100 hover:bg-stone-700 dark:hover:bg-stone-200 disabled:opacity-50 disabled:cursor-not-allowed px-5 py-1.5 text-sm font-medium text-white dark:text-stone-900 transition-all duration-150 shadow-sm"
            >
              {saving ? 'Saving…' : 'Save'}
            </button>
          </div>
        )}
      </div>

      {/* Board */}
      <div className="flex-1 overflow-x-auto overflow-y-auto bg-stone-50 dark:bg-stone-950">
        <div className="flex gap-4 p-5 md:p-7 min-h-full items-start">
          {board.columns.map((col, idx) => (
            <ColumnCard
              key={col.id}
              column={col}
              isEditable={isEditable}
              isDragOver={dragOverColId === col.id}
              draggingTaskId={draggingTask?.id ?? null}
              dropTarget={dropTarget?.colId === col.id ? dropTarget : null}
              pendingEditTaskId={pendingEditTaskId}
              accentColor={COLUMN_ACCENTS[idx % COLUMN_ACCENTS.length]}
              onUpdateLabel={(label) => updateColumn(col.id, label)}
              onDelete={() => deleteColumn(col.id)}
              onAddTask={() => addTask(col.id)}
              onUpdateTask={(taskId, label, metadata) => updateTask(col.id, taskId, label, metadata)}
              onDeleteTask={(taskId) => deleteTask(col.id, taskId)}
              onDragStart={(taskId) => handleDragStart(taskId, col.id)}
              onDragEnd={handleDragEnd}
              onColumnDragOver={(e) => handleColumnDragOver(e, col.id)}
              onColumnDrop={(e) => handleColumnDrop(e, col.id)}
              onTaskDragOver={(e, taskId, position) => handleTaskDragOver(e, taskId, col.id, position)}
              onTaskDrop={(e, taskId) => handleTaskDrop(e, taskId, col.id)}
            />
          ))}

          {isEditable && (
            <button
              onClick={addColumn}
              className="shrink-0 flex items-center gap-2 px-5 py-4 rounded-2xl border-2 border-dashed border-stone-200 dark:border-stone-800 text-stone-400 dark:text-stone-600 hover:border-stone-300 dark:hover:border-stone-700 hover:text-stone-600 dark:hover:text-stone-400 hover:bg-white/60 dark:hover:bg-stone-900/60 transition-all duration-200 text-sm font-medium"
            >
              <PlusIcon className="size-4" />
              Add column
            </button>
          )}
        </div>
      </div>
    </div>
  )
}
