'use client'

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'

import {
  type Comment,
  type CommentAnchor,
  createComment as apiCreateComment,
  deleteComment as apiDeleteComment,
  groupCommentsIntoThreads,
  listComments as apiListComments,
  reopenComment as apiReopenComment,
  resolveComment as apiResolveComment,
  updateComment as apiUpdateComment,
  type CommentThread,
  type CommentOutcome,
} from '../lib/comments-client.js'
import type { MentionUser } from '../lib/mention-picker.js'
import { MentionableTextarea } from './MentionableTextarea.js'

/**
 * Side-panel view of the slice-4 comments API. Renders threaded comments
 * for a document, lets the current user compose new threads and replies,
 * and offers per-comment actions: edit, resolve / reopen, delete.
 *
 * The component is *pure I/O* against the comments-client module —
 * anchor encoding/decoding, editor mark painting, and bubble-menu wiring
 * are layered on top in `TipTapEditor.tsx` through the `comments` prop.
 * Keeping the panel logic agnostic of TipTap means the unit tests can
 * verify thread ordering / state transitions without spinning up an
 * editor.
 *
 * Behaviour
 * ─────────
 * - Active comments (not resolved, not deleted) are shown by default.
 * - A toggle reveals resolved threads in the same list, sorted by
 *   resolved-at descending so the most recently closed appears first.
 * - Composer accepts free-form text. When `onComposeAnchor` returns a
 *   non-null anchor the new comment is bound to that range; otherwise
 *   it is a document-level comment.
 * - The list refreshes after every successful mutation so optimistic
 *   state can't drift from the server.
 *
 * Errors fall through to the `onError` callback so the host shell can
 * surface them with its own toast / banner system (we don't import
 * sonner here to keep the component testable in plain JSDOM).
 */
export interface CommentSidePanelProps {
  documentId: string
  /** Current user id — used to gate edit / delete affordances. */
  currentUserId: string
  /** Set to `true` to show admin-only affordances (delete others'). */
  isAdmin?: boolean
  /**
   * Called when the user clicks "Comment on selection". Should return
   * the encoded anchor for the active editor selection, or `null` for a
   * doc-level comment. Wiring lives in `TipTapEditor` so this component
   * stays editor-agnostic.
   */
  onComposeAnchor?: () => CommentAnchor | null
  /**
   * Called whenever the active comment changes (focus shift / open
   * thread). The editor uses this to scroll the highlighted range into
   * view and paint the `actuate-comment--active` modifier.
   */
  onActiveCommentChange?: (commentId: string | null) => void
  /** Error sink — bubble up server failures to a toast / banner. */
  onError?: (message: string) => void
  /**
   * Override the comments-client API. Tests inject a fake; real callers
   * should leave this unset.
   */
  api?: CommentsApi
  /**
   * Override the mention search — tests pass a synchronous fake so
   * the picker is deterministic without HTTP mocking. Production
   * callers leave this unset and the textarea uses the real
   * `/users/search` REST helper.
   */
  mentionSearch?: (query: string) => Promise<MentionUser[]>
  className?: string
}

/**
 * Tiny adapter type that lets tests swap the comments-client module out.
 * Mirrors the slice-4 client surface 1:1 — keeping it explicit prevents
 * accidental drift when new endpoints are added later.
 */
export interface CommentsApi {
  list: (
    documentId: string,
    opts?: { includeResolved?: boolean },
  ) => Promise<CommentOutcome<Comment[]>>
  create: (input: {
    documentId: string
    body: string
    anchor?: CommentAnchor | null
    parentId?: string | null
  }) => Promise<CommentOutcome<Comment>>
  update: (id: string, body: string) => Promise<CommentOutcome<Comment>>
  resolve: (id: string) => Promise<CommentOutcome<Comment>>
  reopen: (id: string) => Promise<CommentOutcome<Comment>>
  remove: (id: string) => Promise<CommentOutcome<Comment>>
}

const defaultApi: CommentsApi = {
  list: (documentId, opts) => apiListComments(documentId, opts),
  create: (input) => apiCreateComment(input),
  update: (id, body) => apiUpdateComment(id, body),
  resolve: (id) => apiResolveComment(id),
  reopen: (id) => apiReopenComment(id),
  remove: (id) => apiDeleteComment(id),
}

export function CommentSidePanel({
  documentId,
  currentUserId,
  isAdmin = false,
  onComposeAnchor,
  onActiveCommentChange,
  onError,
  api = defaultApi,
  mentionSearch,
  className,
}: CommentSidePanelProps): React.JSX.Element {
  const [comments, setComments] = useState<Comment[]>([])
  const [includeResolved, setIncludeResolved] = useState(false)
  const [loading, setLoading] = useState(true)
  const [draft, setDraft] = useState('')
  const [activeCommentId, setActiveCommentId] = useState<string | null>(null)
  const mountedRef = useRef(true)
  useEffect(() => {
    return () => {
      mountedRef.current = false
    }
  }, [])

  const setActive = useCallback(
    (id: string | null) => {
      setActiveCommentId(id)
      onActiveCommentChange?.(id)
    },
    [onActiveCommentChange],
  )

  const refresh = useCallback(async () => {
    setLoading(true)
    const out = await api.list(documentId, { includeResolved })
    if (!mountedRef.current) return
    if (!out.ok) {
      onError?.(out.error)
      setComments([])
    } else {
      setComments(out.result)
    }
    setLoading(false)
  }, [api, documentId, includeResolved, onError])

  useEffect(() => {
    void refresh()
  }, [refresh])

  const threads = useMemo(() => groupCommentsIntoThreads(comments), [comments])

  const handleCompose = useCallback(async () => {
    const body = draft.trim()
    if (!body) return
    const anchor = onComposeAnchor?.() ?? null
    const out = await api.create({ documentId, body, anchor })
    if (!out.ok) {
      onError?.(out.error)
      return
    }
    setDraft('')
    setActive(out.result.id)
    await refresh()
  }, [draft, onComposeAnchor, api, documentId, onError, setActive, refresh])

  const handleReply = useCallback(
    async (parentId: string, body: string) => {
      const trimmed = body.trim()
      if (!trimmed) return
      const out = await api.create({ documentId, body: trimmed, parentId })
      if (!out.ok) {
        onError?.(out.error)
        return
      }
      await refresh()
    },
    [api, documentId, onError, refresh],
  )

  const handleEdit = useCallback(
    async (id: string, body: string) => {
      const out = await api.update(id, body)
      if (!out.ok) {
        onError?.(out.error)
        return false
      }
      await refresh()
      return true
    },
    [api, onError, refresh],
  )

  const handleResolve = useCallback(
    async (id: string, alreadyResolved: boolean) => {
      const out = alreadyResolved ? await api.reopen(id) : await api.resolve(id)
      if (!out.ok) {
        onError?.(out.error)
        return
      }
      await refresh()
    },
    [api, onError, refresh],
  )

  const handleDelete = useCallback(
    async (id: string) => {
      const out = await api.remove(id)
      if (!out.ok) {
        onError?.(out.error)
        return
      }
      if (activeCommentId === id) setActive(null)
      await refresh()
    },
    [api, onError, refresh, activeCommentId, setActive],
  )

  const wrapperClass = `flex h-full flex-col gap-3 border-l border-gray-200 bg-white p-3 ${className ?? ''}`

  return (
    <aside className={wrapperClass} data-testid="comment-side-panel">
      <header className="flex items-center justify-between">
        <h2 className="text-sm font-semibold text-gray-900">Comments</h2>
        <label className="flex cursor-pointer items-center gap-1 text-xs text-gray-600">
          <input
            type="checkbox"
            checked={includeResolved}
            onChange={(e) => setIncludeResolved(e.target.checked)}
            data-testid="include-resolved-toggle"
          />
          Show resolved
        </label>
      </header>

      <section className="flex flex-col gap-1.5">
        <MentionableTextarea
          className="w-full resize-none rounded border border-gray-300 p-2 text-sm focus:border-blue-500 focus:outline-none"
          rows={3}
          placeholder="Write a comment…  Type @ to mention a teammate."
          value={draft}
          onChange={setDraft}
          data-testid="comment-composer"
          search={mentionSearch}
        />
        <div className="flex justify-end">
          <button
            type="button"
            className="rounded bg-blue-600 px-3 py-1 text-xs font-medium text-white hover:bg-blue-500 disabled:opacity-50"
            disabled={!draft.trim()}
            onClick={handleCompose}
            data-testid="comment-submit"
          >
            Comment
          </button>
        </div>
      </section>

      <section className="flex-1 overflow-y-auto" aria-busy={loading} data-testid="comment-list">
        {loading ? (
          <p className="text-xs text-gray-500">Loading…</p>
        ) : threads.length === 0 ? (
          <p className="text-xs text-gray-500">No comments yet.</p>
        ) : (
          <ul className="flex flex-col gap-2">
            {threads.map((thread) => (
              <CommentThreadView
                key={thread.root.id}
                thread={thread}
                currentUserId={currentUserId}
                isAdmin={isAdmin}
                isActive={activeCommentId === thread.root.id}
                onFocus={() => setActive(thread.root.id)}
                onReply={(body) => handleReply(thread.root.id, body)}
                onEdit={(id, body) => handleEdit(id, body)}
                onToggleResolve={(id, alreadyResolved) => handleResolve(id, alreadyResolved)}
                onDelete={(id) => handleDelete(id)}
                mentionSearch={mentionSearch}
              />
            ))}
          </ul>
        )}
      </section>
    </aside>
  )
}

interface CommentThreadViewProps {
  thread: CommentThread
  currentUserId: string
  isAdmin: boolean
  isActive: boolean
  onFocus: () => void
  onReply: (body: string) => void
  onEdit: (id: string, body: string) => Promise<boolean>
  onToggleResolve: (id: string, alreadyResolved: boolean) => void
  onDelete: (id: string) => void
  mentionSearch?: (query: string) => Promise<MentionUser[]>
}

function CommentThreadView({
  thread,
  currentUserId,
  isAdmin,
  isActive,
  onFocus,
  onReply,
  onEdit,
  onToggleResolve,
  onDelete,
  mentionSearch,
}: CommentThreadViewProps): React.JSX.Element {
  const [replyDraft, setReplyDraft] = useState('')
  const cardClass = `rounded border bg-gray-50 p-2 ${
    isActive ? 'border-yellow-400 ring-1 ring-yellow-300' : 'border-gray-200'
  }`

  return (
    <li
      className={cardClass}
      onClick={onFocus}
      data-testid={`comment-thread-${thread.root.id}`}
      data-active={isActive ? 'true' : 'false'}
    >
      <CommentRow
        comment={thread.root}
        currentUserId={currentUserId}
        isAdmin={isAdmin}
        onEdit={onEdit}
        onToggleResolve={() => onToggleResolve(thread.root.id, thread.root.isResolved)}
        onDelete={() => onDelete(thread.root.id)}
        mentionSearch={mentionSearch}
      />
      {thread.replies.length > 0 && (
        <ul className="mt-2 flex flex-col gap-1 border-l-2 border-gray-200 pl-2">
          {thread.replies.map((reply) => (
            <li key={reply.id}>
              <CommentRow
                comment={reply}
                currentUserId={currentUserId}
                isAdmin={isAdmin}
                onEdit={onEdit}
                // Replies inherit the thread's resolve action; we only
                // expose delete here.
                onToggleResolve={null}
                onDelete={() => onDelete(reply.id)}
                mentionSearch={mentionSearch}
              />
            </li>
          ))}
        </ul>
      )}
      <div className="mt-2 flex items-start gap-1">
        <div className="flex-1">
          <MentionableTextarea
            className="w-full resize-none rounded border border-gray-300 px-2 py-1 text-xs focus:border-blue-500 focus:outline-none"
            placeholder="Reply…  Type @ to mention."
            rows={1}
            value={replyDraft}
            onChange={setReplyDraft}
            data-testid={`reply-input-${thread.root.id}`}
            onClick={(e) => e.stopPropagation()}
            search={mentionSearch}
          />
        </div>
        <button
          type="button"
          className="rounded bg-gray-700 px-2 py-1 text-xs font-medium text-white hover:bg-gray-600 disabled:opacity-50"
          disabled={!replyDraft.trim()}
          onClick={(e) => {
            e.stopPropagation()
            onReply(replyDraft)
            setReplyDraft('')
          }}
          data-testid={`reply-submit-${thread.root.id}`}
        >
          Reply
        </button>
      </div>
    </li>
  )
}

interface CommentRowProps {
  comment: Comment
  currentUserId: string
  isAdmin: boolean
  onEdit: (id: string, body: string) => Promise<boolean>
  onToggleResolve: null | (() => void)
  onDelete: () => void
  mentionSearch?: (query: string) => Promise<MentionUser[]>
}

function CommentRow({
  comment,
  currentUserId,
  isAdmin,
  onEdit,
  onToggleResolve,
  onDelete,
  mentionSearch,
}: CommentRowProps): React.JSX.Element {
  const [editing, setEditing] = useState(false)
  const [draft, setDraft] = useState(comment.body)
  useEffect(() => {
    setDraft(comment.body)
  }, [comment.body])

  const canMutate = isAdmin || comment.userId === currentUserId
  const meta = (
    <div className="text-xs text-gray-500">
      {comment.userId ?? 'deleted user'} · {formatRelative(comment.createdAt)}
      {comment.isResolved ? ' · resolved' : null}
    </div>
  )

  return (
    <div className="flex flex-col gap-1">
      {meta}
      {editing ? (
        <>
          <MentionableTextarea
            className="w-full resize-none rounded border border-gray-300 p-1 text-sm"
            rows={3}
            value={draft}
            onChange={setDraft}
            data-testid={`edit-textarea-${comment.id}`}
            onClick={(e) => e.stopPropagation()}
            search={mentionSearch}
          />
          <div className="flex gap-1">
            <button
              type="button"
              className="rounded bg-blue-600 px-2 py-1 text-xs font-medium text-white"
              onClick={async (e) => {
                e.stopPropagation()
                const ok = await onEdit(comment.id, draft)
                if (ok) setEditing(false)
              }}
              data-testid={`edit-save-${comment.id}`}
            >
              Save
            </button>
            <button
              type="button"
              className="rounded border border-gray-300 px-2 py-1 text-xs"
              onClick={(e) => {
                e.stopPropagation()
                setDraft(comment.body)
                setEditing(false)
              }}
              data-testid={`edit-cancel-${comment.id}`}
            >
              Cancel
            </button>
          </div>
        </>
      ) : (
        <p
          className="text-sm whitespace-pre-wrap text-gray-900"
          data-testid={`comment-body-${comment.id}`}
        >
          {comment.body}
        </p>
      )}
      {!editing && (
        <div className="flex flex-wrap gap-1 text-xs">
          {canMutate && (
            <button
              type="button"
              className="text-gray-600 hover:text-gray-900"
              onClick={(e) => {
                e.stopPropagation()
                setEditing(true)
              }}
              data-testid={`edit-${comment.id}`}
            >
              Edit
            </button>
          )}
          {onToggleResolve && (
            <button
              type="button"
              className="text-gray-600 hover:text-gray-900"
              onClick={(e) => {
                e.stopPropagation()
                onToggleResolve()
              }}
              data-testid={`resolve-${comment.id}`}
            >
              {comment.isResolved ? 'Reopen' : 'Resolve'}
            </button>
          )}
          {canMutate && (
            <button
              type="button"
              className="text-red-600 hover:text-red-700"
              onClick={(e) => {
                e.stopPropagation()
                onDelete()
              }}
              data-testid={`delete-${comment.id}`}
            >
              Delete
            </button>
          )}
        </div>
      )}
    </div>
  )
}

/**
 * Cheap "n seconds / minutes / hours / days ago" formatter. We avoid
 * importing a full date-fns dependency just for this view; the panel is
 * fine being approximate.
 */
function formatRelative(iso: string): string {
  const t = Date.parse(iso)
  if (Number.isNaN(t)) return iso
  const deltaSec = Math.max(0, Math.round((Date.now() - t) / 1000))
  if (deltaSec < 60) return `${deltaSec}s ago`
  const m = Math.round(deltaSec / 60)
  if (m < 60) return `${m}m ago`
  const h = Math.round(m / 60)
  if (h < 24) return `${h}h ago`
  const d = Math.round(h / 24)
  return `${d}d ago`
}

// Re-export for tests that build their own CommentsApi.
export { formatRelative as _formatRelative }
