'use client'

import { useCallback, useEffect, useMemo, useState } from 'react'
import { AlertTriangle, Loader2, Lock } from 'lucide-react'
import { toast } from 'sonner'
import { ConfirmDialog } from '../../components/ui/ConfirmDialog.js'
import { ErrorBoundary } from '../../components/ErrorBoundary.js'
import { VersionHistory } from '../../components/VersionHistory.js'
import {
  addSection,
  createSection,
  duplicateSection,
  emptyPostFromTemplate,
  fetchPostForEditor,
  fetchTemplateForEditor,
  missingPostFields,
  moveSection,
  publishPost,
  removeSection,
  reorderSections,
  savePostDraft,
  toggleSectionVisibility,
  updateSectionContent,
  updateSectionMeta,
  updateSectionSettings,
  validatePost,
  type EditorPost,
  type PostHeaderConfig,
  type ValidationResult,
} from '../../lib/post-editor-service.js'
import { defaultPostHeader } from '@actuate-media/cms-core/page-sections'
import {
  buildPublicPreviewUrl,
  createPreviewToken,
  resolveSiteUrl,
} from '../../lib/preview-link.js'
import type { SectionSettings, SectionTypeId } from '../page-editor/section-types.js'
import {
  hasCanvasRenderer,
  useAdminSectionRenderers,
  type PostRenderContext,
} from '../page-editor/sections/index.js'
import { DEFAULT_VIEWPORT, type ViewportId } from '../page-editor/viewports.js'
import { EditorTopBar, type SaveState } from '../page-editor/EditorTopBar.js'
import { SitePreviewFrame } from '../page-editor/SitePreviewFrame.js'
import { useSitePreview } from '../page-editor/useSitePreview.js'
import { SectionsPanel } from '../page-editor/SectionsPanel.js'
import { SectionInspector } from '../page-editor/SectionInspector.js'
import { AddSectionModal } from '../page-editor/AddSectionModal.js'
import { ValidationSummary } from '../page-editor/ValidationSummary.js'
import { PostEditorCanvas } from './PostEditorCanvas.js'
import { PostFieldsModal, type PostFieldConfig } from './PostFieldsModal.js'

/** The slice of the resolved CMS config this editor reads. */
interface EditorConfig {
  collections?: Record<
    string,
    | {
        labels?: { singular?: string }
        urlPrefix?: string
        fields?: Record<string, PostFieldConfig | undefined>
      }
    | undefined
  >
  seo?: { siteUrl?: string }
}

/** The slice of the admin session this editor reads. */
interface EditorSession {
  user?: { role?: string; name?: string; email?: string }
}

interface PostSectionEditorProps {
  postType: string
  /** undefined when creating a new post (`/posts/:type/new`) */
  documentId?: string
  config?: EditorConfig
  session?: EditorSession
  onNavigate: (path: string) => void
  /**
   * Open a preview route in a new browser tab. `prepare` runs before the tab
   * navigates (used to save an unsaved draft first); returning `false` cancels
   * the open. Falls back to in-tab navigation when not provided.
   */
  onOpenPreview?: (path: string, prepare?: () => Promise<boolean | void> | boolean | void) => void
}

function deriveCan(session: EditorSession | undefined): {
  canEdit: boolean
  canPublish: boolean
} {
  const role = session?.user?.role
  // Default closed: when we can't identify a role, hide the edit/publish
  // affordances rather than exposing them. The server independently enforces
  // access on every write, so this is a UX guard — but it should fail safe.
  if (!role) return { canEdit: false, canPublish: false }
  const editors = ['ADMIN', 'EDITOR', 'AUTHOR']
  const publishers = ['ADMIN', 'EDITOR']
  return { canEdit: editors.includes(role), canPublish: publishers.includes(role) }
}

export function PostSectionEditor({
  postType,
  documentId,
  config,
  session,
  onNavigate,
  onOpenPreview,
}: PostSectionEditorProps) {
  const [post, setPost] = useState<EditorPost | null>(null)
  const [header, setHeader] = useState<PostHeaderConfig>(defaultPostHeader())
  const [loading, setLoading] = useState(true)
  const [loadError, setLoadError] = useState<string | null>(null)

  const [selectedId, setSelectedId] = useState<string | null>(null)
  const [viewport, setViewport] = useState<ViewportId>(DEFAULT_VIEWPORT)
  const [dirty, setDirty] = useState(false)
  const [saveState, setSaveState] = useState<SaveState>('idle')
  const [publishing, setPublishing] = useState(false)

  const [addOpen, setAddOpen] = useState(false)
  const [fieldsOpen, setFieldsOpen] = useState(false)
  const [historyOpen, setHistoryOpen] = useState(false)
  const [deleteTargetId, setDeleteTargetId] = useState<string | null>(null)
  const [confirmPublish, setConfirmPublish] = useState(false)
  const [leaveTarget, setLeaveTarget] = useState<string | null>(null)
  const [validation, setValidation] = useState<ValidationResult | null>(null)

  const { canEdit, canPublish } = useMemo(() => deriveCan(session), [session])
  const customRenderers = useAdminSectionRenderers()

  const typeLabel = useMemo(() => {
    const col = config?.collections?.[postType]
    return col?.labels?.singular ?? postType
  }, [config, postType])

  // A misconfigured post-type collection (missing the fields the editor
  // writes) would silently drop content on save — detect it up front.
  // Memoized to a string/null so the load effect can depend on it without
  // re-running when `config`'s object identity changes between renders.
  const schemaError = useMemo(() => {
    const missing = missingPostFields(config, postType)
    if (missing.length === 0) return null
    const plural = missing.length > 1
    return (
      `The "${postType}" collection is missing required field${plural ? 's' : ''}: ` +
      `${missing.join(', ')}. Declare ${plural ? 'them' : 'it'} in your CMS config ` +
      `so the post editor can save without losing content.`
    )
  }, [config, postType])

  // ─── Load ──────────────────────────────────────────────────────────
  useEffect(() => {
    let cancelled = false
    setLoading(true)
    setLoadError(null)

    // Fail loud on a misconfigured post-type collection rather than letting
    // the user build a post whose content would be silently dropped on save.
    if (schemaError) {
      setLoadError(schemaError)
      setLoading(false)
      return
    }

    // Always load the type template header (the post editor renders it but
    // never edits it — header design lives at the type level).
    fetchTemplateForEditor(postType)
      .then((t) => {
        if (!cancelled) setHeader(t.header)
      })
      .catch(() => {
        /* fall back to the default header */
      })

    const load = documentId
      ? fetchPostForEditor(postType, documentId)
      : emptyPostFromTemplate(postType)

    load
      .then((p) => {
        if (cancelled) return
        setPost(p)
        setSelectedId(null)
        if (!documentId) setDirty(true)
      })
      .catch((err) => {
        if (!cancelled) setLoadError(err instanceof Error ? err.message : 'Failed to load post')
      })
      .finally(() => {
        if (!cancelled) setLoading(false)
      })

    return () => {
      cancelled = true
    }
  }, [postType, documentId, schemaError])

  // ─── Unsaved-changes guard ───────────────────────────────────────────
  useEffect(() => {
    if (!dirty) return
    function onBeforeUnload(e: BeforeUnloadEvent) {
      e.preventDefault()
      e.returnValue = ''
    }
    window.addEventListener('beforeunload', onBeforeUnload)
    return () => window.removeEventListener('beforeunload', onBeforeUnload)
  }, [dirty])

  const context: PostRenderContext = useMemo(
    () => ({
      title: post?.title,
      excerpt: post?.excerpt,
      body: post?.body,
      featuredImageUrl: post?.featuredImage,
      publishDate: post?.publishDate ?? post?.updatedAt ?? null,
      category: post?.category,
      author: { name: session?.user?.name || session?.user?.email },
    }),
    [post, session],
  )

  // ─── Section mutations ───────────────────────────────────────────────
  const mutateSections = useCallback(
    (fn: (sections: EditorPost['sections']) => EditorPost['sections']) => {
      setPost((prev) => (prev ? { ...prev, sections: fn(prev.sections) } : prev))
      setDirty(true)
      setSaveState('idle')
    },
    [],
  )

  const handleAdd = useCallback(
    (type: SectionTypeId) => {
      const section = createSection(type)
      mutateSections((s) => addSection(s, section))
      setSelectedId(section.id)
      setAddOpen(false)
    },
    [mutateSections],
  )

  const handleReorder = useCallback(
    (orderedIds: string[]) => mutateSections((s) => reorderSections(s, orderedIds)),
    [mutateSections],
  )
  const handleToggleVisibility = useCallback(
    (id: string) => mutateSections((s) => toggleSectionVisibility(s, id)),
    [mutateSections],
  )
  const handleDuplicate = useCallback(
    (id: string) => mutateSections((s) => duplicateSection(s, id)),
    [mutateSections],
  )
  const confirmDelete = useCallback(() => {
    if (!deleteTargetId) return
    mutateSections((s) => removeSection(s, deleteTargetId))
    setSelectedId((cur) => (cur === deleteTargetId ? null : cur))
    setDeleteTargetId(null)
  }, [deleteTargetId, mutateSections])

  const handleContentChange = useCallback(
    (patch: Record<string, unknown>) => {
      if (!selectedId) return
      mutateSections((s) => updateSectionContent(s, selectedId, patch))
    },
    [selectedId, mutateSections],
  )
  const handleSettingsChange = useCallback(
    (patch: Partial<SectionSettings>) => {
      if (!selectedId) return
      mutateSections((s) => updateSectionSettings(s, selectedId, patch))
    },
    [selectedId, mutateSections],
  )
  const handleMetaChange = useCallback(
    (patch: { name?: string; internalLabel?: string }) => {
      if (!selectedId) return
      mutateSections((s) => updateSectionMeta(s, selectedId, patch))
    },
    [selectedId, mutateSections],
  )

  const handleFields = useCallback((patch: Partial<EditorPost>) => {
    setPost((prev) => (prev ? { ...prev, ...patch } : prev))
    setDirty(true)
    setSaveState('idle')
  }, [])

  // ─── Save / publish ──────────────────────────────────────────────────
  const handleSaveDraft = useCallback(async (): Promise<boolean> => {
    if (!post) return false
    const result = validatePost(post, false)
    if (result.errors.length > 0) {
      setValidation(result)
      toast.error(result.errors[0]!)
      setFieldsOpen(true)
      return false
    }
    setSaveState('saving')
    try {
      const saved = await savePostDraft(post)
      setPost(saved)
      setDirty(false)
      setSaveState('saved')
      setValidation(null)
      toast.success('Draft saved')
      if (!documentId && saved.id) onNavigate(`/posts/${postType}/${saved.id}/edit`)
      return true
    } catch (err) {
      setSaveState('error')
      toast.error(err instanceof Error ? err.message : 'Failed to save draft')
      return false
    }
  }, [post, documentId, postType, onNavigate])

  // Inline branded preview — iframe of the public site rendering this draft.
  const sitePreview = useSitePreview({
    collection: postType,
    documentId: post?.id,
    slug: post?.slug,
    urlPrefix: config?.collections?.[postType]?.urlPrefix ?? postType,
    siteUrl: config?.seo?.siteUrl,
    dirty,
    saveState,
    saveDraft: handleSaveDraft,
  })

  const requestPublish = useCallback(() => {
    if (!post) return
    const result = validatePost(post, true)
    setValidation(result.errors.length || result.warnings.length ? result : null)
    if (result.errors.length > 0) {
      toast.error('Resolve the listed issues before publishing')
      return
    }
    setConfirmPublish(true)
  }, [post])

  const doPublish = useCallback(async () => {
    if (!post) return
    setPublishing(true)
    try {
      const published = await publishPost(post)
      setPost(published)
      setDirty(false)
      setSaveState('saved')
      setValidation(null)
      toast.success('Post published')
      if (!documentId && published.id) onNavigate(`/posts/${postType}/${published.id}/edit`)
    } catch (err) {
      toast.error(err instanceof Error ? err.message : 'Failed to publish post')
    } finally {
      setPublishing(false)
    }
  }, [post, documentId, postType, onNavigate])

  // After a version restore the server has already written the restored
  // data — refetch so the editor reflects it instead of the stale state.
  const handleRestored = useCallback(() => {
    if (!post?.id) return
    fetchPostForEditor(postType, post.id)
      .then((p) => {
        setPost(p)
        setSelectedId(null)
        setDirty(false)
        setSaveState('idle')
        setValidation(null)
      })
      .catch(() => {
        toast.error('Version restored — refresh the page to see it.')
      })
  }, [postType, post?.id])

  const guardedNavigate = useCallback(
    (path: string) => {
      if (dirty) setLeaveTarget(path)
      else onNavigate(path)
    },
    [dirty, onNavigate],
  )

  const handlePreview = useCallback(() => {
    if (!post?.id) {
      toast.error('Save the post before previewing')
      return
    }
    const path = `/posts/${postType}/${post.id}/preview`
    // New-tab preview: save first when there are unsaved edits so the preview
    // (which reads the persisted document) reflects what's on screen.
    if (onOpenPreview) {
      onOpenPreview(path, dirty ? handleSaveDraft : undefined)
    } else {
      guardedNavigate(path)
    }
  }, [post, postType, dirty, onOpenPreview, handleSaveDraft, guardedNavigate])

  // "View on site": render the draft through the consumer's real public route
  // (`/<urlPrefix>/<slug>?preview=<token>`) via a signed preview token.
  const handleViewOnSite = useCallback(() => {
    if (!post?.id || !post.slug) {
      toast.error('Save the post before viewing it on the site')
      return
    }
    const tab = window.open('about:blank', '_blank')
    if (!tab) {
      toast.error('Enable pop-ups for this site to open the preview in a new tab.')
      return
    }
    void (async () => {
      try {
        if (dirty) {
          const ok = await handleSaveDraft()
          if (!ok) {
            tab.close()
            return
          }
        }
        const { token } = await createPreviewToken(postType, post.id!)
        const urlPrefix = config?.collections?.[postType]?.urlPrefix ?? postType
        const target = buildPublicPreviewUrl({
          siteUrl: resolveSiteUrl(config?.seo?.siteUrl),
          urlPrefix,
          slug: post.slug,
          token,
        })
        try {
          tab.opener = null
        } catch {
          /* same-origin guard */
        }
        tab.location.replace(target)
      } catch (err) {
        tab.close()
        toast.error(err instanceof Error ? err.message : 'Failed to open site preview')
      }
    })()
  }, [post, postType, dirty, config, handleSaveDraft])

  // ─── Render states ───────────────────────────────────────────────────
  if (loading) {
    return (
      <div className="bg-background flex h-full items-center justify-center">
        <Loader2 className="text-muted-foreground h-6 w-6 animate-spin" aria-hidden />
        <span className="sr-only">Loading post editor…</span>
      </div>
    )
  }

  if (loadError || !post) {
    return (
      <div className="bg-background flex h-full items-center justify-center p-6">
        <div className="border-destructive/30 bg-destructive/5 flex max-w-md items-center gap-3 rounded-lg border p-4">
          <AlertTriangle className="text-destructive shrink-0" size={20} aria-hidden />
          <div className="min-w-0">
            <p className="text-foreground text-sm font-medium">Failed to load post</p>
            <p className="text-muted-foreground mt-1 text-sm">{loadError ?? 'Post not found.'}</p>
            <button
              type="button"
              onClick={() => onNavigate('/posts')}
              className="text-primary mt-2 text-sm font-medium hover:underline"
            >
              Back to Posts
            </button>
          </div>
        </div>
      </div>
    )
  }

  const selected = post.sections.find((s) => s.id === selectedId) ?? null

  // Sections whose type has no registered definition (seed/code-owned, e.g.
  // `opal-*`). They are preserved verbatim on save but can't be edited here.
  const unmanagedTypes = Array.from(
    new Set(post.sections.filter((s) => s.unmanaged).map((s) => s.sectionType)),
  )

  // Section types the canvas renders as placeholders — no built-in body
  // component and no consumer-registered admin renderer. When present the
  // editor surfaces "structure view" messaging and promotes "View on site"
  // as the primary design preview.
  const structuralTypes = Array.from(
    new Set(
      post.sections
        .filter((s) => !hasCanvasRenderer(s.sectionType) && !customRenderers[s.sectionType])
        .map((s) => s.sectionType),
    ),
  )

  return (
    <ErrorBoundary>
      <div className="bg-background flex h-full flex-col overflow-hidden">
        <EditorTopBar
          title={post.title || `New ${typeLabel}`}
          status={post.status}
          dirty={dirty}
          saveState={saveState}
          publishing={publishing}
          canEdit={canEdit}
          canPublish={canPublish && canEdit}
          viewport={viewport}
          backLabel="Posts"
          onViewport={setViewport}
          onBack={() => guardedNavigate('/posts')}
          onPreview={handlePreview}
          onViewOnSite={post.id ? handleViewOnSite : undefined}
          sitePreview={sitePreview.active}
          onToggleSitePreview={post.id && post.slug ? sitePreview.toggle : undefined}
          onHistory={post.id ? () => setHistoryOpen(true) : undefined}
          onOpenSettings={() => setFieldsOpen(true)}
          onSaveDraft={handleSaveDraft}
          onPublish={requestPublish}
          structuralTypes={structuralTypes}
        />

        {unmanagedTypes.length > 0 && (
          <div
            role="status"
            className="border-border bg-muted/50 flex items-start gap-3 border-b px-4 py-2.5"
          >
            <Lock className="text-muted-foreground mt-0.5 shrink-0" size={16} aria-hidden />
            <p className="text-muted-foreground flex-1 text-sm">
              This post has{' '}
              <span className="text-foreground font-medium">
                {unmanagedTypes.length} section type{unmanagedTypes.length > 1 ? 's' : ''}
              </span>{' '}
              managed outside the editor ({unmanagedTypes.join(', ')}). They are preserved on save
              but edited in your seed script or code, not here.
            </p>
          </div>
        )}

        {validation && (
          <ValidationSummary
            errors={validation.errors}
            warnings={validation.warnings}
            onDismiss={() => setValidation(null)}
          />
        )}

        <div className="flex flex-1 overflow-hidden">
          <SectionsPanel
            sections={post.sections}
            selectedId={selectedId}
            canEdit={canEdit}
            onSelect={setSelectedId}
            onReorder={handleReorder}
            onToggleVisibility={handleToggleVisibility}
            onDuplicate={handleDuplicate}
            onRequestDelete={(id) => setDeleteTargetId(id)}
            onAdd={() => setAddOpen(true)}
          />

          <div className="flex-1 overflow-hidden">
            {sitePreview.active ? (
              <SitePreviewFrame
                src={sitePreview.url}
                viewport={viewport}
                reloadKey={sitePreview.reloadKey}
                title={`Site preview: ${post.title || `New ${typeLabel}`}`}
              />
            ) : (
              <PostEditorCanvas
                sections={post.sections}
                header={header}
                context={context}
                selectedId={selectedId}
                viewport={viewport}
                onSelect={setSelectedId}
                onAddSection={() => setAddOpen(true)}
                structuralTypes={structuralTypes}
              />
            )}
          </div>

          {selected && (
            <SectionInspector
              section={selected}
              canEdit={canEdit}
              onClose={() => setSelectedId(null)}
              onContentChange={handleContentChange}
              onSettingsChange={handleSettingsChange}
              onMetaChange={handleMetaChange}
              onToggleVisibility={() => handleToggleVisibility(selected.id)}
            />
          )}
        </div>
      </div>

      <AddSectionModal
        open={addOpen}
        scope="post"
        onClose={() => setAddOpen(false)}
        onAdd={handleAdd}
      />

      {post.id && (
        <VersionHistory
          collectionSlug={postType}
          documentId={post.id}
          open={historyOpen}
          onClose={() => setHistoryOpen(false)}
          onRestore={handleRestored}
        />
      )}

      <PostFieldsModal
        open={fieldsOpen}
        post={post}
        canEdit={canEdit}
        fields={config?.collections?.[postType]?.fields}
        onClose={() => setFieldsOpen(false)}
        onSave={handleFields}
      />

      <ConfirmDialog
        open={deleteTargetId !== null}
        onClose={() => setDeleteTargetId(null)}
        onConfirm={confirmDelete}
        title="Delete section?"
        description="This removes the section from the post. You can undo by leaving without saving."
        confirmLabel="Delete"
        destructive
      />

      <ConfirmDialog
        open={confirmPublish}
        onClose={() => setConfirmPublish(false)}
        onConfirm={doPublish}
        title="Publish this post?"
        description="The current content will go live and replace the published version."
        confirmLabel="Publish"
      />

      <ConfirmDialog
        open={leaveTarget !== null}
        onClose={() => setLeaveTarget(null)}
        onConfirm={() => {
          const target = leaveTarget
          setLeaveTarget(null)
          setDirty(false)
          if (target) onNavigate(target)
        }}
        title="Discard unsaved changes?"
        description="You have unsaved changes that will be lost if you leave now."
        confirmLabel="Leave"
        cancelLabel="Stay"
        destructive
      />
    </ErrorBoundary>
  )
}
