import { useCallback } from 'react';
import { useEditor } from '@craftjs/core';
import { useEditorConfig } from '../state/EditorConfigContext';
import { usePages, EMPTY_CANVAS } from '../state/PageContext';
import { useSiteDesign, SiteDesign } from '../state/SiteDesignContext';
import { exportBodyHtml } from '../utils/html-export';
import { repairOrphanNodes } from '../utils/orphan-repair';
import { PageData } from '../types';

export interface BuildSavePayloadInput {
  siteId: number;
  siteName: string;
  /** query.serialize() of whatever is currently on the live canvas. */
  liveCraftState: string;
  pages: PageData[];
  headerPage: PageData;
  footerPage: PageData;
  activePageId: string;
  /** True when the live canvas is showing the header zone (activePageId === '__header__'). */
  isEditingHeader: boolean;
  /** True when the live canvas is showing the footer zone (activePageId === '__footer__'). */
  isEditingFooter: boolean;
  /** Site-wide custom `<head>` code (also present on `design.headCode`). */
  headCode: string;
  /** Full site design tokens object -- lets load() restore colors/fonts/headCode. */
  design: SiteDesign;
}

/**
 * Pure payload builder for the save() API call. Extracted so the
 * header/footer-vs-page routing logic can be unit-tested without mounting
 * React/Craft.js.
 *
 * Bug this fixes: when the user is editing the Header or Footer,
 * `activePageId` is `'__header__'`/`'__footer__'` — which matches no entry
 * in `pages`. The live canvas serialization must be routed into
 * `header_craft_state`/`footer_craft_state` in that case, NOT into a page
 * slot or the top-level `craft_state`/`html` (which must always represent
 * an actual page). Conversely, header/footer must be sourced from the FRESH
 * live state when that zone is being edited, not from the stale stored
 * `headerPage.craftState`/`footerPage.craftState`.
 */
export function buildSavePayload(input: BuildSavePayloadInput) {
  const {
    siteId,
    siteName,
    liveCraftState,
    pages,
    headerPage,
    footerPage,
    activePageId,
    isEditingHeader,
    isEditingFooter,
    headCode,
    design,
  } = input;

  const isPageActive = !isEditingHeader && !isEditingFooter;

  // I-1 (data-loss): `activePageId` can go dangling -- e.g. the original
  // Home page (id 'home') is deleted, its replacement gets a fresh id
  // (`page_<ts>`), and a reload re-initializes `activePageId` back to the
  // hardcoded default `'home'` (see PageContext's `useState('home')`) before
  // `load()` has a chance to point it at the actually-restored page. If a
  // real page IS active but matches no entry in `pages`, treat `pages[0]`
  // (the landing page) as the active one so the live canvas serialization
  // still reaches the index.html page slot / `pages_craft_state[0]` instead
  // of only the legacy top-level `craft_state`/`html` fields.
  const activePageIndex = isPageActive ? pages.findIndex((p) => p.id === activePageId) : -1;
  const effectiveActivePageId = activePageIndex !== -1 ? activePageId : pages[0]?.id;

  // Fresh header/footer state: the live canvas wins when that zone is the
  // one currently being edited; otherwise fall back to the last-committed
  // stored state (updated on zone switch by PageContext's saveCurrentState).
  const headerCraftState = isEditingHeader ? liveCraftState : (headerPage.craftState || null);
  const footerCraftState = isEditingFooter ? liveCraftState : (footerPage.craftState || null);

  let headerHtml = '';
  try {
    if (headerCraftState) {
      headerHtml = exportBodyHtml(headerCraftState).html;
    }
  } catch (e) {
    console.error('Header HTML export failed:', e);
  }

  let footerHtml = '';
  try {
    if (footerCraftState) {
      footerHtml = exportBodyHtml(footerCraftState).html;
    }
  } catch (e) {
    console.error('Footer HTML export failed:', e);
  }

  // The top-level `craft_state`/`html` fields (and the matching per-page
  // slot below) must always represent an actual PAGE. When editing the
  // header/footer, activePageId matches no page — use the landing page's
  // own stored state instead of leaking the live header/footer canvas into
  // a page slot or mislabeling it as page content.
  let currentCraftState: string | null;
  let currentHtml = '';
  let css = '';

  if (isPageActive) {
    currentCraftState = liveCraftState;
    try {
      const result = exportBodyHtml(currentCraftState);
      currentHtml = result.html;
      css = result.css;
    } catch (e) {
      console.error('HTML export failed, saving state only:', e);
    }
  } else {
    const landingPage = pages[0] ?? null;
    currentCraftState = landingPage?.craftState ?? null;
    try {
      if (currentCraftState) {
        const result = exportBodyHtml(currentCraftState);
        currentHtml = result.html;
        css = result.css;
      }
    } catch (e) {
      console.error('HTML export failed, saving state only:', e);
    }
  }

  // Build the pages array with HTML for each page. For the active page (only
  // possible when a real page is active), use the freshly exported HTML from
  // the canvas; for others, export from their stored craft state.
  const pagesPayload = pages.map((page, i) => {
    // The first page is ALWAYS the landing page → publishes to index.html
    // regardless of the page name/slug. Apache serves '/' from index.html,
    // and renaming the first page should not break the root URL.
    const filename = i === 0 ? 'index.html' : page.slug + '.html';
    let pageHtml = '';

    if (isPageActive && page.id === effectiveActivePageId) {
      // Active page: use the current canvas HTML (already exported above)
      pageHtml = currentHtml;
    } else if (page.craftState) {
      try {
        pageHtml = exportBodyHtml(page.craftState).html;
      } catch (e) {
        console.error(`HTML export failed for page ${page.name}:`, e);
      }
    }

    return {
      filename,
      title: page.name,
      html: pageHtml,
      // PKG-H §5: per-page SEO overrides, read by the backend's
      // `handlePublish` (mirrors the existing `title` field above) to build
      // the published <head> for this file. Omitted (undefined) when the
      // page has no seo overrides -- JSON.stringify drops undefined keys,
      // so legacy-shaped payloads round-trip unchanged.
      seo: page.seo,
    };
  });

  // Build pages_craft_state array: for each page, store its craft state.
  // For the currently active page (only when a real page is active), always
  // use the fresh canvas state since page.craftState may be stale (not
  // updated until page switch). When editing header/footer, activePageId
  // matches no page, so every page correctly falls back to its own stored
  // state below.
  const pagesGrapesjs = pages.map((page, i) => ({
    id: page.id,
    name: page.name,
    // Pin the landing page's slug to 'index' on the wire too, so that on
    // reload the editor's clean-URL routing (.htaccess rewrite of /name →
    // name.html) lines up with the file we just wrote (index.html).
    slug: i === 0 ? 'index' : page.slug,
    craftState: (isPageActive && page.id === effectiveActivePageId) ? liveCraftState : (page.craftState || null),
    // PKG-H §5: round-trips PageData.seo through save -> load() so the
    // editor's SEO fields survive a reload (mirrors craftState above).
    seo: page.seo,
  }));

  return {
    site_id: siteId,
    name: siteName,
    html: currentHtml,
    css,
    pages: pagesPayload,
    header_html: headerHtml,
    footer_html: footerHtml,
    craft_state: currentCraftState,
    header_craft_state: headerCraftState,
    footer_craft_state: footerCraftState,
    pages_craft_state: pagesGrapesjs,
    head_code: headCode,
    design,
  };
}

export function useWhpApi() {
  const { query, actions } = useEditor();
  const { whpConfig, isWHP } = useEditorConfig();
  const {
    pages,
    headerPage,
    footerPage,
    activePageId,
    isEditingHeader,
    isEditingFooter,
    setHeaderCraftState,
    setFooterCraftState,
    setPagesCraftState,
    setActivePageIdDirect,
  } = usePages();
  const { design, updateDesign } = useSiteDesign();

  const save = useCallback(async () => {
    if (!isWHP || !whpConfig) return null;

    // Serialize whatever is currently on the live canvas (a page, the
    // header, or the footer — depending on activePageId/isEditingHeader/
    // isEditingFooter).
    const liveCraftState = query.serialize();

    const payload = buildSavePayload({
      siteId: whpConfig.siteId,
      siteName: whpConfig.siteName,
      liveCraftState,
      pages,
      headerPage,
      footerPage,
      activePageId,
      isEditingHeader,
      isEditingFooter,
      headCode: design.headCode,
      design,
    });

    const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-CSRF-Token': whpConfig.csrfToken,
      },
      body: JSON.stringify(payload),
    });
    return resp.json();
  }, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage, isEditingHeader, isEditingFooter, design]);

  const publish = useCallback(async () => {
    if (!isWHP || !whpConfig) return null;

    // First save to ensure staging is up to date
    await save();

    // Then publish from staging to live
    const resp = await fetch(
      `${whpConfig.apiUrl}?action=publish&site_id=${whpConfig.siteId}`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-CSRF-Token': whpConfig.csrfToken,
        },
        body: JSON.stringify({ site_id: whpConfig.siteId }),
      },
    );
    return resp.json();
  }, [isWHP, whpConfig, save]);

  const load = useCallback(async () => {
    if (!isWHP || !whpConfig) return null;

    const resp = await fetch(
      `${whpConfig.apiUrl}?action=load&site_id=${whpConfig.siteId}`,
    );
    const data = await resp.json();

    if (data.success && data.project) {
      const proj = data.project;

      // Restore site design tokens (colors/fonts/headCode) so the editor
      // reflects what was last saved. Prefer the full `design` object when
      // present; fall back to just `head_code` for older project.json files
      // saved before this field existed (backward-compatible: defaults for
      // everything else).
      if (proj.design && typeof proj.design === 'object') {
        updateDesign(proj.design);
      } else if (typeof proj.head_code === 'string') {
        updateDesign({ headCode: proj.head_code });
      }

      // Restore header craft state
      if (proj.header_craft_state) {
        setHeaderCraftState(typeof proj.header_craft_state === 'string'
          ? proj.header_craft_state : JSON.stringify(proj.header_craft_state));
      }

      // Restore footer craft state
      if (proj.footer_craft_state) {
        setFooterCraftState(typeof proj.footer_craft_state === 'string'
          ? proj.footer_craft_state : JSON.stringify(proj.footer_craft_state));
      }

      // Restore pages and load the first page into the canvas
      if (proj.pages_craft_state && Array.isArray(proj.pages_craft_state) && proj.pages_craft_state.length > 0) {
        setPagesCraftState(proj.pages_craft_state.map((p: { id: string; name: string; slug: string; craftState: string | null; seo?: PageData['seo'] }) => ({
          id: p.id, name: p.name, slug: p.slug, craftState: p.craftState || null, seo: p.seo,
        })));

        // Load the first page (home) into the canvas. Routed through
        // `repairOrphanNodes` first -- same as `PageContext.loadState` does
        // for every subsequent page switch -- so a node that's unreachable
        // from ROOT (invisible to Layers/selection) gets reattached here
        // too, on the load that actually puts it on screen, rather than only
        // on the next page switch. `repairOrphanNodes` never throws and
        // returns the original string reference when nothing needed fixing,
        // so this is cheap to run unconditionally.
        const firstPage = proj.pages_craft_state[0];
        if (firstPage.craftState) {
          try {
            const rawState = typeof firstPage.craftState === 'string'
              ? firstPage.craftState : JSON.stringify(firstPage.craftState);
            const { state, repaired } = repairOrphanNodes(rawState);
            if (repaired.length > 0) {
              // I5: console-buffer.ts only patches console.error, and this
              // reattach signal is the single most diagnostic clue for the
              // still-unreproduced "elements drop off the canvas" report --
              // it must reach the in-builder issue reporter's console buffer.
              console.error(
                `[site-builder] reattached ${repaired.length} unreachable node(s) to the page root:`,
                repaired.join(', '),
              );
            }
            actions.deserialize(state);
          } catch (e) {
            // I5: this is the initial load that decides whether the user
            // sees a working editor at all -- align with `loadState`'s
            // behaviour (console.error + a known-safe fallback) instead of
            // warning quietly and leaving the Frame on whatever it last had.
            console.error('Failed to load page state:', e);
            try {
              actions.deserialize(EMPTY_CANVAS);
            } catch (_e2) {
              // give up
            }
          }
        }

        // I-1 (data-loss): point activePageId at the page we just loaded
        // into the canvas. Without this, activePageId stays at whatever it
        // was initialized to (the hardcoded default 'home'), which goes
        // dangling the moment the original Home page has been deleted and
        // replaced (its replacement gets a fresh `page_<ts>` id) -- the next
        // edit+save would then only reach the legacy top-level fields
        // instead of the actual page slot. Only do this when a real page is
        // being loaded, i.e. we're not currently mid-edit of the header/
        // footer zone (switching zones is handled separately by switchPage).
        if (!isEditingHeader && !isEditingFooter) {
          setActivePageIdDirect(firstPage.id);
        }
      }
    }
    return data;
  }, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState, setActivePageIdDirect, isEditingHeader, isEditingFooter, updateDesign]);

  const uploadAsset = useCallback(
    async (file: File) => {
      if (!isWHP || !whpConfig) return null;

      const formData = new FormData();
      formData.append('file', file);

      const resp = await fetch(
        `${whpConfig.apiUrl}?action=upload_asset&site_id=${whpConfig.siteId}`,
        {
          method: 'POST',
          headers: { 'X-CSRF-Token': whpConfig.csrfToken },
          body: formData,
        },
      );
      return resp.json();
    },
    [isWHP, whpConfig],
  );

  return { save, publish, load, uploadAsset, isWHP };
}
