import { useState, useRef, useEffect, useMemo, type KeyboardEvent } from "react";
import { Tooltip } from "../../components/Tooltip";
import { Plus, ImagePlus, BarChart3, Trash2, PencilLine, Pencil } from "lucide-react";
import { useFrontmatter } from "./useFrontmatter";
import { buildDoc } from "../embeds/build-doc";
import { useTheme } from "../../hooks/useTheme";
import { uploadImage } from "../upload";
import { AuthorsManager } from "./AuthorsManager";
import type { FrontmatterStore } from "./frontmatter-store";
import type { EmbedStore } from "../embeds/embed-store";

const BANNER_KEY = "banner.html";

const CHART_PLACEHOLDER_HTML = `<div class="d3-chart" style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--muted-color);font-family:system-ui;">
  <p style="margin:0;font-size:14px;">Generating chart...</p>
</div>`;

function makeImageBannerHtml(url: string) {
  return `<div style="width:100%;height:100%;overflow:hidden;">
  <img src="${url}" alt="Banner" style="width:100%;height:100%;object-fit:cover;display:block;" />
</div>`;
}

interface FrontmatterHeroProps {
  store: FrontmatterStore | null;
  embedStore?: EmbedStore | null;
}

export function FrontmatterHero({ store, embedStore }: FrontmatterHeroProps) {
  const { data, update } = useFrontmatter(store);
  // Single entry point for all byline editing (add/edit/remove/reorder of
  // authors and affiliations). Reordering used to be an undiscoverable inline
  // drag; it now lives in this modal behind an explicit affordance.
  const [showManager, setShowManager] = useState(false);
  const openManager = () => {
    if (store) setShowManager(true);
  };

  const bannerSrc = data?.banner || "";
  const [bannerHtml, setBannerHtml] = useState("");
  const [uploadingBanner, setUploadingBanner] = useState(false);
  const bannerFileRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    if (!embedStore || !bannerSrc) { setBannerHtml(""); return; }
    setBannerHtml(embedStore.get(bannerSrc));
    return embedStore.observeKey(bannerSrc, setBannerHtml);
  }, [embedStore, bannerSrc]);

  const { isDark, primaryColor } = useTheme();
  const bannerSrcdoc = useMemo(() => {
    if (!bannerHtml) return "";
    // Banner iframes are size-constrained by the parent (5:2 aspect ratio),
    // so we render them edge-to-edge: no body padding, full height chain.
    // primaryColor must be forwarded so banners using `--primary-color`
    // pick up the article's accent (otherwise they fall back to the
    // generic #4e79a7 from buildEmbedSrcdoc).
    return buildDoc(bannerHtml, { isDark, primaryColor, fullBleed: true });
  }, [bannerHtml, isDark, primaryColor]);

  const handleBannerUpload = async (file: File) => {
    if (!embedStore || !file.type.startsWith("image/")) return;
    setUploadingBanner(true);
    try {
      const url = await uploadImage(file);
      embedStore.set(BANNER_KEY, makeImageBannerHtml(url));
      update("banner", BANNER_KEY);
    } catch (err) {
      console.error("[banner-upload]", err);
    } finally {
      setUploadingBanner(false);
    }
  };

  const handleCreateChart = () => {
    if (!embedStore) return;
    embedStore.set(BANNER_KEY, CHART_PLACEHOLDER_HTML);
    update("banner", BANNER_KEY);
    window.dispatchEvent(
      new CustomEvent("open-embed-studio", { detail: { src: BANNER_KEY } }),
    );
  };

  const handleRemoveBanner = () => {
    if (!embedStore) return;
    embedStore.remove(bannerSrc || BANNER_KEY);
    update("banner", "");
  };

  const handleEditBanner = () => {
    // The banner content lives in the shared embed store under `bannerSrc`,
    // so the Embed Studio handles it exactly like any other htmlEmbed:
    // chat with the AI, iterate on the HTML, see live preview. Works
    // whether the banner was originally an image upload (just <img>) or
    // a chart placeholder - the studio doesn't care, it edits whatever
    // HTML lives at this key.
    if (!embedStore) return;
    window.dispatchEvent(
      new CustomEvent("open-embed-studio", {
        detail: { src: bannerSrc || BANNER_KEY },
      }),
    );
  };

  if (!data) {
    return <div className="hero" />;
  }

  const hasAuthors = data.authors.length > 0;
  const hasAffiliations = data.affiliations.length > 0;
  const multipleAffiliations = data.affiliations.length > 1;

  return (
    <div>
      <section className="hero">
        <EditableText
          value={data.title}
          placeholder="Article title"
          onChange={(v) => update("title", v)}
          className="hero-title"
          multiline
        />

        {bannerSrcdoc ? (
          <div className="hero-banner hero-banner--filled">
            <iframe
              srcDoc={bannerSrcdoc}
              title="Banner"
              sandbox="allow-scripts allow-same-origin"
              style={{ width: "100%", height: "100%", border: "none", display: "block", background: "transparent" }}
            />
            {embedStore && (
              <div className="hero-banner-actions">
                <Tooltip title="Edit banner in Embed Studio">
                  <button
                    type="button"
                    className="hero-banner-action"
                    onClick={handleEditBanner}
                    aria-label="Edit banner"
                  >
                    <PencilLine size={14} />
                  </button>
                </Tooltip>
                <Tooltip title="Remove banner">
                  <button
                    type="button"
                    className="hero-banner-action hero-banner-action--danger"
                    onClick={handleRemoveBanner}
                    aria-label="Remove banner"
                  >
                    <Trash2 size={14} />
                  </button>
                </Tooltip>
              </div>
            )}
          </div>
        ) : embedStore ? (
          <div className="hero-banner hero-banner--empty" aria-label="Banner placeholder">
            <input
              ref={bannerFileRef}
              type="file"
              accept="image/*"
              style={{ display: "none" }}
              onChange={(e) => {
                const file = e.target.files?.[0];
                if (file) handleBannerUpload(file);
                e.target.value = "";
              }}
            />
            <div className="hero-banner-empty__inner">
              <p className="hero-banner-empty__hint">
                Add a banner to set the mood of your article.
              </p>
              <div className="hero-banner-empty__actions">
                <button
                  type="button"
                  className="btn"
                  disabled={uploadingBanner}
                  onClick={() => bannerFileRef.current?.click()}
                >
                  <ImagePlus size={16} />
                  <span>{uploadingBanner ? "Uploading..." : "Upload image"}</span>
                </button>
                <button
                  type="button"
                  className="btn"
                  onClick={handleCreateChart}
                >
                  <BarChart3 size={16} />
                  <span>Create chart</span>
                </button>
              </div>
            </div>
          </div>
        ) : null}

        <EditableText
          value={data.subtitle}
          placeholder="Subtitle (optional)"
          onChange={(v) => update("subtitle", v)}
          className="hero-desc"
          multiline
        />
      </section>

      <header className="meta" aria-label="Article meta information">
        <div className="meta-container">
          <div className="meta-container-cell">
            <h3>Authors</h3>
            {hasAuthors ? (
              <ul className="authors">
                {data.authors.map((author, i) => (
                  <li key={`author-${i}`}>
                    <span
                      className="author-editable"
                      onClick={openManager}
                      title="Manage authors & affiliations"
                    >
                      {author.url ? (
                        <a href={author.url} target="_blank" rel="noopener noreferrer">
                          {author.name}
                        </a>
                      ) : (
                        author.name
                      )}
                      {multipleAffiliations && author.affiliations?.length > 0 && (
                        <sup>{author.affiliations.join(",")}</sup>
                      )}
                    </span>
                    {i < data.authors.length - 1 && <>,&nbsp;</>}
                  </li>
                ))}
                <li className="author-add-btn">
                  <Tooltip title="Manage authors & affiliations">
                    <button
                      className="icon-btn"
                      onClick={openManager}
                      aria-label="Manage authors and affiliations"
                      style={{ padding: 2 }}
                    >
                      <Pencil size={13} />
                    </button>
                  </Tooltip>
                </li>
              </ul>
            ) : (
              <button className="meta-placeholder-btn" onClick={openManager}>
                <Plus size={14} />
                <span>Add author</span>
              </button>
            )}
          </div>

          {hasAffiliations && (
            <div className="meta-container-cell">
              <h3>Affiliations</h3>
              {multipleAffiliations ? (
                <ol
                  className="affiliations affiliations--clickable"
                  onClick={openManager}
                  title="Manage authors & affiliations"
                >
                  {data.affiliations.map((aff, i) => (
                    <li key={`aff-${i}`}>
                      {aff.url ? (
                        <a href={aff.url} target="_blank" rel="noopener noreferrer">
                          {aff.name}
                        </a>
                      ) : (
                        aff.name
                      )}
                    </li>
                  ))}
                </ol>
              ) : (
                <p
                  className="affiliations--clickable"
                  onClick={openManager}
                  title="Manage authors & affiliations"
                >
                  {data.affiliations[0].url ? (
                    <a href={data.affiliations[0].url} target="_blank" rel="noopener noreferrer">
                      {data.affiliations[0].name}
                    </a>
                  ) : (
                    data.affiliations[0].name
                  )}
                </p>
              )}
            </div>
          )}

          <div className="meta-container-cell">
            <h3>Published</h3>
            <EditableText
              value={data.published}
              placeholder="Date"
              onChange={(v) => update("published", v)}
              inline
            />
          </div>
        </div>
      </header>

      {store && (
        <AuthorsManager
          open={showManager}
          store={store}
          authors={data.authors}
          affiliations={data.affiliations}
          onClose={() => setShowManager(false)}
        />
      )}
    </div>
  );
}

interface EditableTextProps {
  value: string;
  placeholder: string;
  onChange: (value: string) => void;
  className?: string;
  multiline?: boolean;
  inline?: boolean;
}

// Debounce window for live-commits. Long enough that we don't spam
// Yjs with one update per keystroke when the user is typing fast,
// short enough that the "Saving..." indicator kicks in within the
// same beat as the editing action (so it actually feels like the
// app is reacting to what we're doing).
const COMMIT_DEBOUNCE_MS = 300;

function EditableText({ value, placeholder, onChange, className, multiline, inline }: EditableTextProps) {
  const [draft, setDraft] = useState(value);
  const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
  // Debounce timer for live-commits while typing. Cleared on every
  // keystroke and on blur (which fires `flush` to commit immediately).
  const commitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  // Last value we propagated to Yjs. We compare against this rather
  // than `value`, because `value` updates only after Yjs round-trips
  // back and there's a brief window where we've committed locally
  // but the prop hasn't refreshed yet - re-committing in that window
  // would dispatch a no-op Yjs update and risk an event loop with
  // the parent's `useFrontmatter` hook.
  const lastCommittedRef = useRef(value);

  useEffect(() => {
    setDraft(value);
    lastCommittedRef.current = value;
  }, [value]);

  useEffect(() => {
    if (multiline && inputRef.current instanceof HTMLTextAreaElement) {
      autoResize(inputRef.current);
    }
  }, [draft, multiline]);

  // Flush pending commit timer on unmount so an in-flight edit
  // isn't lost when the hero re-mounts (theme switch, etc.).
  useEffect(() => {
    return () => {
      if (commitTimerRef.current) {
        clearTimeout(commitTimerRef.current);
        commitTimerRef.current = null;
      }
    };
  }, []);

  const autoResize = (el: HTMLTextAreaElement) => {
    el.style.height = "0";
    el.style.height = `${el.scrollHeight}px`;
  };

  // Commit immediately and cancel any pending debounce. Used by
  // onBlur and by Enter (single-line inputs) so leaving the field
  // never feels laggy.
  const flush = (next: string) => {
    if (commitTimerRef.current) {
      clearTimeout(commitTimerRef.current);
      commitTimerRef.current = null;
    }
    if (next !== lastCommittedRef.current) {
      lastCommittedRef.current = next;
      onChange(next);
    }
  };

  // Schedule a debounced commit. Called on every keystroke so the
  // doc updates while the user is still typing (and the sync
  // indicator flips to "Saving..." in real time), without flooding
  // Yjs with one update per keypress.
  const scheduleCommit = (next: string) => {
    if (commitTimerRef.current) clearTimeout(commitTimerRef.current);
    commitTimerRef.current = setTimeout(() => {
      commitTimerRef.current = null;
      if (next !== lastCommittedRef.current) {
        lastCommittedRef.current = next;
        onChange(next);
      }
    }, COMMIT_DEBOUNCE_MS);
  };

  const handleChange = (
    e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
  ) => {
    const next = e.target.value;
    setDraft(next);
    scheduleCommit(next);
  };

  const handleKeyDown = (e: KeyboardEvent) => {
    if (e.key === "Enter" && !multiline) {
      e.preventDefault();
      (e.target as HTMLElement).blur();
    }
    if (e.key === "Escape") {
      // Cancel any pending commit and snap back to the last
      // server-confirmed value. Anything we already debounce-
      // committed before pressing Escape is gone from this draft
      // but still persisted - Yjs undo (Ctrl+Z) is the way to
      // truly roll back. Same behaviour as a Notion/Linear field.
      if (commitTimerRef.current) {
        clearTimeout(commitTimerRef.current);
        commitTimerRef.current = null;
      }
      setDraft(value);
      lastCommittedRef.current = value;
      (e.target as HTMLElement).blur();
    }
  };

  const Tag = multiline ? "textarea" : "input";
  const inputCls = inline
    ? "editable-text-input editable-text-input--inline"
    : "editable-text-input";

  return (
    <span
      className={`editable-text ${className || ""}`}
      style={{ display: inline ? "inline" : "block" }}
    >
      <Tag
        ref={inputRef as any}
        className={inputCls}
        value={draft}
        onChange={handleChange}
        onBlur={() => flush(draft)}
        onKeyDown={handleKeyDown}
        placeholder={placeholder}
        rows={multiline ? 1 : undefined}
      />
    </span>
  );
}
