import React, { createContext, useContext, useState, useCallback, useRef, ReactNode } from 'react';
import { useEditor } from '@craftjs/core';
import { PageData } from '../types';
import { SerializedTreeNode } from '../types/sitesmith';
import { useSiteDesign, SiteDesign } from './SiteDesignContext';
import { sanitizeAiTree, flattenTreeForCraft, FlatCraftNode } from '../utils/craft-tree';

interface PageContextValue {
  pages: PageData[];
  headerPage: PageData;
  footerPage: PageData;
  activePageId: string;
  isEditingHeader: boolean;
  isEditingFooter: boolean;
  switchPage: (pageId: string) => void;
  editHeader: () => void;
  editFooter: () => void;
  addPage: (name: string, slug: string) => void;
  deletePage: (pageId: string) => void;
  renamePage: (pageId: string, name: string, slug: string) => void;
  setHeaderCraftState: (craftState: string) => void;
  setFooterCraftState: (craftState: string) => void;
  setPagesCraftState: (pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => void;
  /**
   * Bookkeeping-only: point `activePageId` at an already-loaded page without
   * re-serializing/deserializing the canvas (the caller -- e.g. useWhpApi's
   * `load()` -- has already put the right state on the canvas itself). Used
   * to fix I-1: `activePageId` defaults to the hardcoded `'home'` and
   * `load()` never updated it, so after the original Home page was deleted
   * (its replacement gets a fresh `page_<ts>` id) and the app reloaded,
   * `activePageId` pointed at nothing in `pages`, and a subsequent edit+save
   * only reached the legacy top-level fields.
   */
  setActivePageIdDirect: (pageId: string) => void;
  /** AI helpers — replace entire site or page with a new tree */
  replaceAllPages: (pages: { name: string; tree: SerializedTreeNode }[]) => void;
  replaceCurrentPage: (page: { name: string; tree: SerializedTreeNode }) => void;
  setHeader: (tree: SerializedTreeNode) => void;
  setFooter: (tree: SerializedTreeNode) => void;
  siteDesign: SiteDesign;
}

const HEADER_ID = '__header__';
const FOOTER_ID = '__footer__';

// M-3: `page_${Date.now()}` alone collides when two pages are minted inside
// the same millisecond (addPage called twice in quick succession, or two AI
// replaceAllPages entries) -- then rename/delete/save operate on both pages
// at once since they share an id. A module-scoped monotonic counter,
// combined with the timestamp, guarantees uniqueness regardless of how many
// ids are minted within the same millisecond. This is state/id-minting code
// (not toHtml/export), so Date.now() here is fine -- see task-minors-brief.md.
let pageIdCounter = 0;

/** Mints a unique page id: timestamp (base36) + a monotonic per-process counter (base36). */
export function nextPageId(): string {
  return 'page_' + Date.now().toString(36) + '_' + (++pageIdCounter).toString(36);
}

const EMPTY_CANVAS =
  '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{"minHeight":"100vh","backgroundColor":"#ffffff"},"tag":"div"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}';

const EMPTY_HEADER =
  '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{"minHeight":"60px","backgroundColor":"#ffffff","padding":"12px 24px","display":"flex","alignItems":"center"},"tag":"header"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}';

const EMPTY_FOOTER =
  '{"ROOT":{"type":{"resolvedName":"Container"},"isCanvas":true,"props":{"style":{"minHeight":"60px","backgroundColor":"#0f172a","color":"#94a3b8","padding":"40px 24px","textAlign":"center"},"tag":"footer"},"displayName":"Container","custom":{},"hidden":false,"nodes":[],"linkedNodes":{}}}';

/**
 * Flatten a `SerializedTreeNode` (as produced by the AI, a template, etc.)
 * into a Craft.js `SerializedNodes` JSON string ready for
 * `actions.deserialize()`.
 *
 * Untrusted input is validated the same way `apply-ai-response.ts`'s
 * `buildNodeTree` validates AI `patch`/section-replace trees: any node whose
 * `type.resolvedName` isn't a registered component is dropped (subtree and
 * all) via `sanitizeAiTree`, and a bad/colliding/`'ROOT'` id is regenerated
 * — never left as-is. This matters here specifically because `replace`
 * scope `site`/`page` responses reach this function via `actions.deserialize`
 * with no further validation downstream, unlike the `buildNodeTree` path
 * which also gets Craft.js's own `parseFreshNode` as a second line of
 * defense. If the ROOT node itself is invalid, sanitizeAiTree returns null
 * and we fall back to an empty canvas rather than handing deserialize()
 * something that could throw.
 *
 * Exported standalone (not a hook) so it's directly unit-testable without
 * mounting a Craft.js `<Editor>`.
 */
export function treeToCraftState(tree: SerializedTreeNode): string {
  const sanitized = sanitizeAiTree(tree, new Set());
  if (!sanitized) return EMPTY_CANVAS;

  const { rootNodeId, nodes: flatNodes } = flattenTreeForCraft(sanitized);
  const nodes: Record<string, FlatCraftNode> = flatNodes;

  // Craft.js deserialize requires the root node keyed as 'ROOT'.
  if (rootNodeId !== 'ROOT') {
    nodes['ROOT'] = { ...nodes[rootNodeId], parent: null, isCanvas: true };
    delete nodes[rootNodeId];
    for (const childId of nodes['ROOT'].nodes) {
      if (nodes[childId]) nodes[childId].parent = 'ROOT';
    }
    // I-2: linkedNodes children (e.g. ColumnLayout's col-0/col-1, or a
    // Section/BackgroundSection/FormContainer's SHELL_INNER wrapper) need
    // the same reparenting as nodes[] children above. Without this, a
    // ColumnLayout/SHELL_INNER-rooted AI `replace` leaves those children's
    // `parent` pointing at the OLD root id, which is then `delete`d --
    // producing a dangling parent reference that breaks select/move/delete
    // of those nodes in the Craft.js editor.
    for (const linkedId of Object.values(nodes['ROOT'].linkedNodes)) {
      if (nodes[linkedId]) nodes[linkedId].parent = 'ROOT';
    }
  }
  return JSON.stringify(nodes);
}

// Default header seed: a ROOT header Container holding a Navbar with default
// links. New sites previously opened with an EMPTY header (just a bare
// Container), so there was no menu to edit and the empty zone rendered as a
// stray band above the page. Seeding a real Navbar gives every new site an
// editable menu-with-links out of the box (and removes the empty-header gap).
//
// Item 15: the nav used to link Home/About/Services/Contact, but a brand
// new site only has a "Home" page -- About/Services/Contact were dead links
// on first click. Rather than seeding three empty placeholder pages nobody
// asked for, the simpler default is a nav with just the one real page (Home)
// plus a CTA button, which is inert (`href: '#'`) rather than pointing at a
// page that doesn't exist. Users add pages/links as their site grows.
// Node shape matches treeToCraftState() / Craft's actions.deserialize().
export const DEFAULT_HEADER_STATE = JSON.stringify({
  ROOT: {
    type: { resolvedName: 'Container' },
    isCanvas: true,
    props: { style: { width: '100%' }, tag: 'header' },
    displayName: 'Container',
    custom: {},
    hidden: false,
    nodes: ['header-navbar'],
    linkedNodes: {},
  },
  'header-navbar': {
    type: { resolvedName: 'Navbar' },
    isCanvas: false,
    props: {
      logoType: 'text',
      logoText: 'MySite',
      logoImage: '',
      logoWidth: '120px',
      logoUrl: '/',
      logoFontFamily: 'Inter, sans-serif',
      logoFontSize: '20px',
      links: [
        { text: 'Home', href: '/' },
        { text: 'Get Started', href: '#', isCta: true },
      ],
      backgroundColor: '#ffffff',
      textColor: '#3f3f46',
      hoverColor: '#3b82f6',
      ctaColor: '#3b82f6',
      ctaTextColor: '#ffffff',
      padding: '16px 24px',
      navAlignment: 'space-between',
      isSticky: false,
      showMobileMenu: false,
      style: { borderBottom: '1px solid #e4e4e7' },
    },
    displayName: 'Navbar',
    parent: 'ROOT',
    custom: {},
    hidden: false,
    nodes: [],
    linkedNodes: {},
  },
});

const PageContext = createContext<PageContextValue>({
  pages: [],
  headerPage: { id: HEADER_ID, name: 'Header', slug: '__header__', craftState: null },
  footerPage: { id: FOOTER_ID, name: 'Footer', slug: '__footer__', craftState: null },
  activePageId: 'home',
  isEditingHeader: false,
  isEditingFooter: false,
  switchPage: () => {},
  editHeader: () => {},
  editFooter: () => {},
  addPage: () => {},
  deletePage: () => {},
  renamePage: () => {},
  setHeaderCraftState: () => {},
  setFooterCraftState: () => {},
  setPagesCraftState: () => {},
  setActivePageIdDirect: () => {},
  replaceAllPages: () => {},
  replaceCurrentPage: () => {},
  setHeader: () => {},
  setFooter: () => {},
  siteDesign: {} as SiteDesign,
});

export const usePages = () => useContext(PageContext);

function slugify(name: string): string {
  const slug = name
    .toLowerCase()
    .trim()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-');
  // M-4: a punctuation-only name (e.g. "!!!") strips down to '' -- without a
  // fallback, buildSavePayload would write filename = '' + '.html' for that
  // page. uniqueSlug's existing dedupe logic then applies on top of this
  // fallback the same way it does for any other base ('page', 'page-2', ...).
  return slug || 'page';
}

/**
 * Append `-2`, `-3`, … to `base` until it no longer collides with
 * `existingSlugs`. Two pages that slugify to the same string (e.g. both
 * named "About") must not both publish to `about.html` — the second write
 * would silently overwrite the first on publish.
 */
export function uniqueSlug(base: string, existingSlugs: string[]): string {
  if (!existingSlugs.includes(base)) return base;
  let i = 2;
  while (existingSlugs.includes(`${base}-${i}`)) i++;
  return `${base}-${i}`;
}

const DEFAULT_PAGE: PageData = {
  id: 'home',
  name: 'Home',
  slug: 'index',
  craftState: null,
};

const DEFAULT_HEADER: PageData = {
  id: HEADER_ID,
  name: 'Header',
  slug: '__header__',
  craftState: DEFAULT_HEADER_STATE,
};

const DEFAULT_FOOTER: PageData = {
  id: FOOTER_ID,
  name: 'Footer',
  slug: '__footer__',
  craftState: null,
};

export const PageProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const { query, actions } = useEditor();
  const { design } = useSiteDesign();
  const [pages, setPages] = useState<PageData[]>([DEFAULT_PAGE]);
  const [headerPage, setHeaderPage] = useState<PageData>(DEFAULT_HEADER);
  const [footerPage, setFooterPage] = useState<PageData>(DEFAULT_FOOTER);
  const [activePageId, setActivePageId] = useState('home');
  const activePageIdRef = useRef(activePageId);
  activePageIdRef.current = activePageId;

  // Mirror the latest state in refs so event handlers (switchPage,
  // deletePage) can synchronously read "what's current" without smuggling a
  // side effect into a setState updater function to peek at `prev` — updater
  // functions must stay pure since React (StrictMode) may invoke them twice.
  const pagesRef = useRef(pages);
  pagesRef.current = pages;
  const headerPageRef = useRef(headerPage);
  headerPageRef.current = headerPage;
  const footerPageRef = useRef(footerPage);
  footerPageRef.current = footerPage;

  const isEditingHeader = activePageId === HEADER_ID;
  const isEditingFooter = activePageId === FOOTER_ID;

  /** Save whatever is on the current Frame back to the right state slot */
  const saveCurrentState = useCallback(() => {
    const currentState = query.serialize();
    const currentId = activePageIdRef.current;

    if (currentId === HEADER_ID) {
      setHeaderPage((prev) => ({ ...prev, craftState: currentState }));
    } else if (currentId === FOOTER_ID) {
      setFooterPage((prev) => ({ ...prev, craftState: currentState }));
    } else {
      setPages((prev) =>
        prev.map((p) => (p.id === currentId ? { ...p, craftState: currentState } : p)),
      );
    }
  }, [query]);

  /** Load a craft state into the Frame */
  const loadState = useCallback(
    (craftState: string | null, fallback: string) => {
      setTimeout(() => {
        try {
          actions.deserialize(craftState || fallback);
        } catch (e) {
          console.error('Failed to deserialize state:', e);
          try {
            actions.deserialize(fallback);
          } catch (_e2) {
            // give up
          }
        }
      }, 0);
    },
    [actions],
  );

  const switchPage = useCallback(
    (pageId: string) => {
      if (pageId === activePageIdRef.current) return;

      // Serialize the current Craft.js state synchronously BEFORE switching
      const currentState = query.serialize();
      const currentId = activePageIdRef.current;

      // Persist the serialized state to the correct page slot
      if (currentId === HEADER_ID) {
        setHeaderPage((prev) => ({ ...prev, craftState: currentState }));
      } else if (currentId === FOOTER_ID) {
        setFooterPage((prev) => ({ ...prev, craftState: currentState }));
      } else {
        setPages((prev) =>
          prev.map((p) => (p.id === currentId ? { ...p, craftState: currentState } : p)),
        );
      }

      // Load target page state. `pageId` can never equal `currentId` here
      // (guarded by the early return above), so the target's stored state
      // was untouched by the setState calls just above — the refs (kept in
      // sync with state on every render) are safe to read synchronously
      // without waiting for a re-render, and without running the load as a
      // side effect inside a setState updater.
      if (pageId === HEADER_ID) {
        loadState(headerPageRef.current.craftState, EMPTY_HEADER);
      } else if (pageId === FOOTER_ID) {
        loadState(footerPageRef.current.craftState, EMPTY_FOOTER);
      } else {
        const target = pagesRef.current.find((p) => p.id === pageId);
        loadState(target?.craftState || null, EMPTY_CANVAS);
      }

      setActivePageId(pageId);
      activePageIdRef.current = pageId;
    },
    [query, loadState],
  );

  const editHeader = useCallback(() => {
    switchPage(HEADER_ID);
  }, [switchPage]);

  const editFooter = useCallback(() => {
    switchPage(FOOTER_ID);
  }, [switchPage]);

  const addPage = useCallback(
    (name: string, slug: string) => {
      const requestedSlug = slug || slugify(name);
      const id = nextPageId();

      // Save current page first
      saveCurrentState();

      setPages((prev) => [
        ...prev,
        {
          id,
          name,
          slug: uniqueSlug(requestedSlug, prev.map((p) => p.slug)),
          craftState: null,
        },
      ]);

      // Switch to the new page with empty canvas
      loadState(null, EMPTY_CANVAS);

      setActivePageId(id);
      activePageIdRef.current = id;
    },
    [saveCurrentState, loadState],
  );

  const deletePage = useCallback(
    (pageId: string) => {
      // Can't delete header/footer
      if (pageId === HEADER_ID || pageId === FOOTER_ID) return;

      const prev = pagesRef.current;
      if (prev.length <= 1) return;

      const filtered = prev.filter((p) => p.id !== pageId);
      setPages(filtered);

      // If deleting the active page, switch to the first remaining. This
      // runs after the state update is computed (not inside the setPages
      // updater) so the side effects (deserialize, ref/state mutation) fire
      // exactly once regardless of how many times React invokes updaters.
      if (pageId === activePageIdRef.current) {
        const nextPage = filtered[0];
        setActivePageId(nextPage.id);
        activePageIdRef.current = nextPage.id;
        loadState(nextPage.craftState, EMPTY_CANVAS);
      }
    },
    [loadState],
  );

  const renamePage = useCallback((pageId: string, name: string, slug: string) => {
    setPages((prev) =>
      prev.map((p, i) => {
        if (p.id !== pageId) return p;
        // First page is the landing page — its slug is locked to 'index' so
        // the file always publishes to index.html regardless of the user-set
        // name. The display name can change freely.
        if (i === 0) return { ...p, name, slug: 'index' };
        const requestedSlug = slug || slugify(name);
        const otherSlugs = prev.filter((pp) => pp.id !== pageId).map((pp) => pp.slug);
        return { ...p, name, slug: uniqueSlug(requestedSlug, otherSlugs) };
      }),
    );
  }, []);

  /** Allow external code (e.g., load from API) to set the header craft state */
  const setHeaderCraftState = useCallback((craftState: string) => {
    setHeaderPage((prev) => ({ ...prev, craftState }));
  }, []);

  /** Allow external code (e.g., load from API) to set the footer craft state */
  const setFooterCraftState = useCallback((craftState: string) => {
    setFooterPage((prev) => ({ ...prev, craftState }));
  }, []);

  /**
   * Bookkeeping-only setter for `activePageId` -- see the doc comment on
   * `PageContextValue.setActivePageIdDirect`. Does NOT serialize/deserialize
   * the canvas; callers that need that should use `switchPage` instead.
   */
  const setActivePageIdDirect = useCallback((pageId: string) => {
    setActivePageId(pageId);
    activePageIdRef.current = pageId;
  }, []);

  /** Allow external code (e.g., load from API) to restore pages with craft states */
  const setPagesCraftState = useCallback((pagesData: { id: string; name: string; slug: string; craftState: string | null }[]) => {
    setPages(pagesData.map((p, i) => ({
      id: p.id,
      name: p.name,
      // Heal legacy projects whose first page was saved with slug='home' (or
      // any other) before the landing-page rule existed. The first page is
      // ALWAYS the landing page → slug 'index' → file index.html.
      slug: i === 0 ? 'index' : p.slug,
      craftState: p.craftState,
    })));
  }, []);

  /**
   * AI helper: replace all pages with newly generated trees.
   * Stores each page's serialized state without touching the live canvas
   * (the canvas still shows the currently active page — call switchPage() if needed).
   */
  const replaceAllPages = useCallback((newPages: { name: string; tree: SerializedTreeNode }[]) => {
    if (newPages.length === 0) return;
    // Track slugs as they're assigned so later pages dedupe against earlier
    // ones in the same batch (e.g. the AI generating two "About" pages).
    const seenSlugs: string[] = [];
    const built = newPages.map((p, i) => {
      // First page must publish to index.html so it serves at the site root.
      // Apache resolves '/' to index.html, not home.html — without this, the
      // AI's "Home" page lands at /home.html and visitors hit a blank root.
      const slug = i === 0 ? 'index' : uniqueSlug(slugify(p.name), seenSlugs);
      seenSlugs.push(slug);
      return {
        id: i === 0 ? 'home' : nextPageId(),
        name: p.name,
        slug,
        craftState: treeToCraftState(p.tree),
      };
    });
    setPages(built);
    // Load the first page into the live canvas
    const firstState = built[0].craftState;
    setActivePageId(built[0].id);
    activePageIdRef.current = built[0].id;
    loadState(firstState, EMPTY_CANVAS);
  }, [loadState]);

  /**
   * AI helper: replace the current page's tree.
   * Deserializes the new tree into the live Craft.js canvas and persists it.
   */
  const replaceCurrentPage = useCallback((page: { name: string; tree: SerializedTreeNode }) => {
    const craftState = treeToCraftState(page.tree);
    const currentId = activePageIdRef.current;
    if (currentId === HEADER_ID) {
      setHeaderPage((prev) => ({ ...prev, name: page.name, craftState }));
    } else if (currentId === FOOTER_ID) {
      setFooterPage((prev) => ({ ...prev, name: page.name, craftState }));
    } else {
      setPages((prev) =>
        prev.map((p) => (p.id === currentId ? { ...p, name: page.name, craftState } : p)),
      );
    }
    loadState(craftState, EMPTY_CANVAS);
  }, [loadState]);

  /**
   * AI helper: replace the shared header tree.
   * Updates stored state; does NOT switch the canvas to header view.
   */
  const setHeader = useCallback((tree: SerializedTreeNode) => {
    const craftState = treeToCraftState(tree);
    setHeaderPage((prev) => ({ ...prev, craftState }));
    // If the canvas is currently showing the header, refresh it live
    if (activePageIdRef.current === HEADER_ID) {
      loadState(craftState, EMPTY_HEADER);
    }
  }, [loadState]);

  /**
   * AI helper: replace the shared footer tree.
   * Updates stored state; does NOT switch the canvas to footer view.
   */
  const setFooter = useCallback((tree: SerializedTreeNode) => {
    const craftState = treeToCraftState(tree);
    setFooterPage((prev) => ({ ...prev, craftState }));
    // If the canvas is currently showing the footer, refresh it live
    if (activePageIdRef.current === FOOTER_ID) {
      loadState(craftState, EMPTY_FOOTER);
    }
  }, [loadState]);

  return (
    <PageContext.Provider
      value={{
        pages,
        headerPage,
        footerPage,
        activePageId,
        isEditingHeader,
        isEditingFooter,
        switchPage,
        editHeader,
        editFooter,
        addPage,
        deletePage,
        renamePage,
        setHeaderCraftState,
        setFooterCraftState,
        setPagesCraftState,
        setActivePageIdDirect,
        replaceAllPages,
        replaceCurrentPage,
        setHeader,
        setFooter,
        siteDesign: design,
      }}
    >
      {children}
    </PageContext.Provider>
  );
};
