"use client";

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";

import { discardAutosave, saveBlog } from "@/features/blogs/actions";
import type { BlogEditorData, TaxonomyOptions } from "@/features/blogs/queries";
import type { BlogEditorMeta, BlogFormValues, SaveIntent } from "@/features/blogs/schemas";
import type { BlogDefaults } from "@/features/settings/schemas";
import { slugify } from "@/lib/slug";

/** Quiet period after the last edit before autosaving. */
const AUTOSAVE_DELAY_MS = 2500;

export type EditorSection = "content" | "seo" | "faq" | "cta" | "related";
/** Page order of the editor's sections. */
export const EDITOR_SECTIONS: EditorSection[] = ["content", "seo", "faq", "cta", "related"];

/** DOM id of a section, for jump links and scrolling to errors. */
export const sectionId = (section: EditorSection) => `editor-section-${section}`;

/** Scrolls the page to a section. */
export function scrollToSection(section: EditorSection, behavior: ScrollBehavior = "smooth") {
  document.getElementById(sectionId(section))?.scrollIntoView({ behavior, block: "start" });
}

/** Which section (or "settings" for the side panel) shows a field, so errors can be revealed. */
export function sectionForField(field: string): EditorSection | "settings" {
  if (field.startsWith("faqs")) return "faq";
  if (field.startsWith("cta")) return "cta";
  if (field.startsWith("related")) return "related";
  if (
    ["seoTitle", "metaDescription", "canonicalUrl", "focusKeyword", "ogTitle", "ogDescription", "ogImage"].includes(field)
  ) {
    return "seo";
  }
  if (["categoryId", "authorId", "tags", "publishedAt", "featuredImage", "featuredImageAlt", "featuredImageTitle"].includes(field)) {
    return "settings";
  }
  return "content";
}

export type SaveStatus =
  | { state: "idle" }
  | { state: "saving"; intent: SaveIntent }
  | { state: "saved"; at: string }
  | { state: "error"; message: string };

type EditorContextValue = {
  values: BlogFormValues;
  setField: <K extends keyof BlogFormValues>(key: K, value: BlogFormValues[K]) => void;
  /** Replaces several fields at once (an import, or undoing one). The slug is taken as given. */
  applyValues: (patch: Partial<BlogFormValues>) => void;
  meta: BlogEditorMeta | null;
  errors: Record<string, string>;
  save: (intent: SaveIntent, options?: { overrides?: Partial<BlogFormValues>; silent?: boolean }) => Promise<boolean>;
  saving: SaveIntent | null;
  status: SaveStatus;
  /** Differs from what's saved (or live, for published articles). */
  isDirty: boolean;
  /** Edits not yet persisted anywhere, not even in autosave. */
  hasUnpersistedChanges: boolean;
  /** Edits to a live article held in autosave. */
  hasParkedChanges: boolean;
  conflict: boolean;
  slugLocked: boolean;
  setSlugTouched: (touched: boolean) => void;
  settingsOpen: boolean;
  setSettingsOpen: (open: boolean) => void;
  discardParkedChanges: () => Promise<void>;
  openPreview: () => Promise<void>;
  options: TaxonomyOptions;
  defaults: BlogDefaults;
  siteUrl: string | null;
  tagSuggestions: string[];
};

const EditorContext = createContext<EditorContextValue | null>(null);

export function useBlogEditor() {
  const context = useContext(EditorContext);
  if (!context) throw new Error("useBlogEditor must be used inside BlogEditorProvider");
  return context;
}

export function BlogEditorProvider({
  initial,
  options,
  defaults,
  siteUrl,
  tagSuggestions,
  initialSection,
  children,
}: {
  initial: BlogEditorData;
  options: TaxonomyOptions;
  defaults: BlogDefaults;
  siteUrl: string | null;
  tagSuggestions: string[];
  /** Section to scroll to on load, e.g. from `?tab=seo`. */
  initialSection: EditorSection | null;
  children: ReactNode;
}) {
  const router = useRouter();
  const [values, setValues] = useState<BlogFormValues>(initial.pending ?? initial.saved);
  const [saved, setSaved] = useState<BlogFormValues>(initial.saved);
  const [meta, setMeta] = useState<BlogEditorMeta | null>(initial.meta);
  const [persisted, setPersisted] = useState(() => JSON.stringify(initial.pending ?? initial.saved));
  const [hasParkedChanges, setHasParkedChanges] = useState(Boolean(initial.pending));
  const [errors, setErrors] = useState<Record<string, string>>({});
  const [saving, setSaving] = useState<SaveIntent | null>(null);
  const [status, setStatus] = useState<SaveStatus>(
    initial.meta ? { state: "saved", at: initial.meta.autosavedAt ?? initial.meta.updatedAt } : { state: "idle" },
  );
  const [conflict, setConflict] = useState(false);
  const [settingsOpen, setSettingsOpen] = useState(false);
  const [slugTouched, setSlugTouched] = useState(
    () => Boolean(initial.meta) && initial.saved.slug !== slugify(initial.saved.title),
  );

  const valuesRef = useRef(values);
  const metaRef = useRef(meta);
  const persistedRef = useRef(persisted);
  const queue = useRef<Promise<unknown>>(Promise.resolve());
  useEffect(() => {
    valuesRef.current = values;
    metaRef.current = meta;
    persistedRef.current = persisted;
  });

  // A slug is only derived from the title until it has been public, or edited by hand.
  const slugLocked = slugTouched || Boolean(meta && (meta.status !== "DRAFT" || meta.publishedAt));

  const setField = useCallback(
    <K extends keyof BlogFormValues>(key: K, value: BlogFormValues[K]) => {
      setValues((current) => {
        const next = { ...current, [key]: value };
        if (key === "title" && !slugLocked) next.slug = slugify(String(value));
        return next;
      });
      setErrors((current) => {
        if (!Object.keys(current).some((field) => field === key || field.startsWith(`${String(key)}.`))) {
          return current;
        }
        return Object.fromEntries(
          Object.entries(current).filter(([field]) => field !== key && !field.startsWith(`${String(key)}.`)),
        );
      });
    },
    [slugLocked],
  );

  const applyValues = useCallback((patch: Partial<BlogFormValues>) => {
    setValues((current) => ({ ...current, ...patch }));
    setErrors((current) => {
      const keys = Object.keys(patch);
      const next = Object.fromEntries(
        Object.entries(current).filter(([field]) => !keys.some((key) => field === key || field.startsWith(`${key}.`))),
      );
      return Object.keys(next).length === Object.keys(current).length ? current : next;
    });
  }, []);

  const savedJson = useMemo(() => JSON.stringify(saved), [saved]);
  const valuesJson = useMemo(() => JSON.stringify(values), [values]);
  const isDirty = valuesJson !== savedJson;
  const hasUnpersistedChanges = valuesJson !== persisted;

  const save = useCallback<EditorContextValue["save"]>(
    (intent, { overrides, silent = intent === "autosave" } = {}) => {
      const run = async () => {
        const snapshot = { ...valuesRef.current, ...overrides };
        const snapshotJson = JSON.stringify(snapshot);
        if (intent === "autosave" && snapshotJson === persistedRef.current && metaRef.current) {
          return true;
        }

        setSaving(intent);
        setStatus({ state: "saving", intent });
        const result = await saveBlog({
          id: metaRef.current?.id ?? null,
          version: metaRef.current?.version ?? null,
          intent,
          values: snapshot,
        }).catch(() => ({
          ok: false as const,
          error: "Couldn't reach the server. Check your connection; your changes are still here.",
          conflict: undefined,
          fieldErrors: undefined,
        }));
        setSaving(null);

        if (!result.ok) {
          if (result.conflict) {
            setConflict(true);
            toast.error(result.error, {
              duration: Infinity,
              action: { label: "Reload", onClick: () => window.location.reload() },
            });
          } else if (!silent) {
            toast.error(result.error);
          }
          if (result.fieldErrors && intent !== "autosave") {
            setErrors(result.fieldErrors);
            const firstField = Object.keys(result.fieldErrors)[0];
            if (firstField) {
              const target = sectionForField(firstField);
              if (target === "settings") setSettingsOpen(true);
              else scrollToSection(target);
            }
          }
          setStatus({ state: "error", message: intent === "autosave" ? "Autosave failed" : "Not saved" });
          return false;
        }

        const { blog, values: serverValues } = result.data;
        const isNew = !metaRef.current;
        metaRef.current = blog;
        setMeta(blog);
        if (isNew) window.history.replaceState(null, "", `/admin/blogs/${blog.id}`);

        // The server fills in or de-duplicates slugs; adopt it unless the user has since edited it.
        const next = { ...snapshot, slug: serverValues.slug, publishedAt: serverValues.publishedAt };
        if (valuesRef.current.slug === snapshot.slug && serverValues.slug !== snapshot.slug) {
          setValues((current) => ({ ...current, slug: serverValues.slug }));
        }
        if (valuesRef.current.publishedAt === snapshot.publishedAt && serverValues.publishedAt !== snapshot.publishedAt) {
          setValues((current) => ({ ...current, publishedAt: serverValues.publishedAt }));
        }

        const nextJson = JSON.stringify(next);
        persistedRef.current = nextJson;
        setPersisted(nextJson);
        if (intent === "autosave" && blog.status !== "DRAFT") {
          setHasParkedChanges(true);
        } else {
          setSaved(next);
          setHasParkedChanges(false);
        }
        if (intent !== "autosave") setErrors({});
        setStatus({ state: "saved", at: new Date().toISOString() });
        if (!silent && result.message) toast.success(result.message);
        return true;
      };

      const next = queue.current.then(run, run);
      queue.current = next;
      return next;
    },
    [],
  );

  // Old links such as `?tab=seo` open the page at that section.
  useEffect(() => {
    if (initialSection && initialSection !== "content") scrollToSection(initialSection, "instant");
    // eslint-disable-next-line react-hooks/exhaustive-deps -- only on mount
  }, []);

  // Autosave after a pause in editing.
  useEffect(() => {
    if (conflict || !hasUnpersistedChanges) return;
    if (!meta && !values.title.trim() && !values.content) return;
    const timer = setTimeout(() => void save("autosave"), AUTOSAVE_DELAY_MS);
    return () => clearTimeout(timer);
  }, [valuesJson, hasUnpersistedChanges, conflict, meta, values.title, values.content, save]);

  const valuesJsonRef = useRef(valuesJson);
  useEffect(() => {
    valuesJsonRef.current = valuesJson;
  });

  // Save right away when the tab is hidden, e.g. before the laptop lid closes.
  useEffect(() => {
    const onHide = () => {
      if (document.visibilityState === "hidden" && valuesJsonRef.current !== persistedRef.current) {
        void save("autosave");
      }
    };
    document.addEventListener("visibilitychange", onHide);
    return () => document.removeEventListener("visibilitychange", onHide);
  }, [save]);

  // Warn before leaving with edits that haven't reached the server.
  const mustWarn = hasUnpersistedChanges || saving !== null;
  useEffect(() => {
    if (!mustWarn) return;
    const onBeforeUnload = (event: BeforeUnloadEvent) => {
      event.preventDefault();
    };
    const onClick = (event: MouseEvent) => {
      if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
        return;
      }
      const anchor = (event.target as Element | null)?.closest?.("a[href]");
      if (!(anchor instanceof HTMLAnchorElement) || anchor.target === "_blank" || anchor.hasAttribute("download")) {
        return;
      }
      const url = new URL(anchor.href, window.location.href);
      if (url.origin !== window.location.origin || url.pathname === window.location.pathname) return;
      if (!window.confirm("You have unsaved changes. They'll be saved automatically if you stay a moment. Leave anyway?")) {
        event.preventDefault();
        event.stopPropagation();
      }
    };
    window.addEventListener("beforeunload", onBeforeUnload);
    document.addEventListener("click", onClick, true);
    return () => {
      window.removeEventListener("beforeunload", onBeforeUnload);
      document.removeEventListener("click", onClick, true);
    };
  }, [mustWarn]);

  // ⌘S / Ctrl+S saves: a draft stays a draft; live articles update.
  useEffect(() => {
    const onKeyDown = (event: KeyboardEvent) => {
      if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "s") {
        event.preventDefault();
        const current = metaRef.current;
        void save(current && current.status !== "DRAFT" ? "update" : "draft");
      }
    };
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [save]);

  const discardParkedChanges = useCallback(async () => {
    const id = metaRef.current?.id;
    if (!id) return;
    const result = await discardAutosave(id);
    if (!result.ok) {
      toast.error(result.error);
      return;
    }
    const json = JSON.stringify(result.data.values);
    setValues(result.data.values);
    setSaved(result.data.values);
    setMeta(result.data.blog);
    persistedRef.current = json;
    setPersisted(json);
    setHasParkedChanges(false);
    setErrors({});
    toast.success(result.message ?? "Changes discarded");
  }, []);

  const openPreview = useCallback(async () => {
    if (!metaRef.current && !valuesRef.current.title.trim() && !valuesRef.current.content) {
      toast.error("Add a title or some content before previewing.");
      return;
    }
    // Open the tab synchronously so popup blockers allow it, then point it at the preview.
    const previewWindow = window.open("about:blank", "_blank");
    const ok = await save("autosave", { silent: false });
    const id = metaRef.current?.id;
    if (!ok || !id) {
      previewWindow?.close();
      return;
    }
    const url = `/admin/blogs/${id}/preview`;
    if (previewWindow) previewWindow.location.href = url;
    else router.push(url);
  }, [save, router]);

  const value: EditorContextValue = {
    values,
    setField,
    applyValues,
    meta,
    errors,
    save,
    saving,
    status,
    isDirty,
    hasUnpersistedChanges,
    hasParkedChanges,
    conflict,
    slugLocked,
    setSlugTouched,
    settingsOpen,
    setSettingsOpen,
    discardParkedChanges,
    openPreview,
    options,
    defaults,
    siteUrl,
    tagSuggestions,
  };

  return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
}
