import { useCallback, useEffect, useRef } from "react";
import type { Composition } from "@hyperframes/sdk";
import { parseGsapScriptAcorn } from "@hyperframes/core/gsap-parser-acorn";
import type { DomEditSelection } from "../components/editor/domEditingTypes";
import {
  sdkGsapTweenPersist,
  sdkGsapRemovePropertyPersist,
  type CutoverDeps,
} from "../utils/sdkCutover";
import { extractGsapScriptText } from "../utils/gsapSoftReload";
import { PROPERTY_DEFAULTS } from "./gsapScriptCommitHelpers";
import type { SafeGsapCommitMutation } from "./gsapScriptCommitTypes";

const DEBOUNCE_MS = 150;

/**
 * The SDK `setGsapTween` 'set' path REPLACES a tween's editable property set
 * (engine `handleSetGsapTween` → `updateAnimationInScript`), so sending only the
 * single edited key would silently drop the tween's other animated props. Mirror
 * the legacy server path (`{ ...anim.properties, [property]: val }`): read the
 * tween's CURRENT properties from the in-memory SDK doc and merge the one edit in,
 * so REPLACE semantics preserve siblings. Returns the single-key map unchanged
 * when the tween/script can't be found (best-effort; before===after then falls
 * back to the server path).
 */
export function mergeTweenProperties(
  sdkSession: Composition,
  animationId: string,
  edited: Record<string, number | string>,
  kind: "to" | "from",
): Record<string, number | string> {
  try {
    const script = extractGsapScriptText(sdkSession.serialize());
    if (!script) return { ...edited };
    const anim = parseGsapScriptAcorn(script).animations.find((a) => a.id === animationId);
    if (!anim) return { ...edited };
    const existing = kind === "from" ? (anim.fromProperties ?? {}) : anim.properties;
    return { ...existing, ...edited };
  } catch {
    return { ...edited };
  }
}

interface SdkPropertyDeps {
  sdkSession?: Composition | null;
  sdkDeps?: CutoverDeps | null;
  activeCompPath?: string | null;
}

export function useGsapPropertyDebounce(
  commitMutationSafely: SafeGsapCommitMutation,
  sdk?: SdkPropertyDeps,
) {
  const pendingPropertyEditRef = useRef<{
    selection: DomEditSelection;
    animationId: string;
    property: string;
    value: number | string;
  } | null>(null);
  const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  // The caller passes `sdk` as a fresh object literal every render. Keying any
  // callback's deps on it (esp. flushPendingPropertyEdit, whose identity drives
  // the unmount-flush cleanup effect) re-fires the cleanup on EVERY parent
  // re-render — so a playhead tick mid-slider-drag would flush + record an undo
  // entry per render. Hold the latest value in a ref instead so every callback
  // reads current deps without re-subscribing on identity churn.
  const sdkRef = useRef(sdk);
  sdkRef.current = sdk;

  const flushPendingPropertyEdit = useCallback(async () => {
    const pending = pendingPropertyEditRef.current;
    if (!pending) return;
    pendingPropertyEditRef.current = null;
    const { selection, animationId, property, value } = pending;
    const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
    if (sdkSession && sdkDeps) {
      const targetPath = selection.sourceFile || activeCompPath || "index.html";
      const handled = await sdkGsapTweenPersist(
        targetPath,
        {
          kind: "set",
          animationId,
          properties: {
            properties: mergeTweenProperties(sdkSession, animationId, { [property]: value }, "to"),
          },
        },
        sdkSession,
        sdkDeps,
        { label: `Edit GSAP ${property}`, coalesceKey: `gsap:${animationId}:${property}` },
      );
      if (handled) return;
    }
    commitMutationSafely(
      selection,
      { type: "update-property", animationId, property, value },
      {
        label: `Edit GSAP ${property}`,
        coalesceKey: `gsap:${animationId}:${property}`,
        softReload: true,
      },
    );
  }, [commitMutationSafely]);

  const updateGsapProperty = useCallback(
    (
      selection: DomEditSelection,
      animationId: string,
      property: string,
      value: number | string,
    ) => {
      pendingPropertyEditRef.current = { selection, animationId, property, value };
      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
      debounceTimerRef.current = setTimeout(() => {
        void flushPendingPropertyEdit();
      }, DEBOUNCE_MS);
    },
    [flushPendingPropertyEdit],
  );

  useEffect(() => {
    return () => {
      if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current);
      void flushPendingPropertyEdit();
    };
  }, [flushPendingPropertyEdit]);

  // fallow-ignore-next-line complexity
  const addGsapProperty = useCallback(
    // fallow-ignore-next-line complexity
    async (selection: DomEditSelection, animationId: string, property: string) => {
      let defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
      const el = selection.element;
      if (property === "width" || property === "height") {
        const rect = el.getBoundingClientRect();
        defaultValue = Math.round(property === "width" ? rect.width : rect.height);
      } else if (property === "opacity" || property === "autoAlpha") {
        const cs = el.ownerDocument.defaultView?.getComputedStyle(el);
        // Use `|| 1` only as a non-finite fallback, not a falsy fallback: an
        // element currently at opacity 0 must seed 0, not 1.
        const current = cs ? Number.parseFloat(cs.opacity) : Number.NaN;
        defaultValue = Number.isFinite(current) ? current : 1;
      }
      const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
      if (sdkSession && sdkDeps) {
        const targetPath = selection.sourceFile || activeCompPath || "index.html";
        const handled = await sdkGsapTweenPersist(
          targetPath,
          {
            kind: "set",
            animationId,
            properties: {
              properties: mergeTweenProperties(
                sdkSession,
                animationId,
                { [property]: defaultValue },
                "to",
              ),
            },
          },
          sdkSession,
          sdkDeps,
          { label: `Add GSAP ${property}` },
        );
        if (handled) return;
      }
      commitMutationSafely(
        selection,
        { type: "add-property", animationId, property, defaultValue },
        { label: `Add GSAP ${property}` },
      );
    },
    [commitMutationSafely],
  );

  const removeProperty = useCallback(
    async (selection: DomEditSelection, animationId: string, property: string, from: boolean) => {
      const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
      if (sdkSession && sdkDeps) {
        const targetPath = selection.sourceFile || activeCompPath || "index.html";
        const handled = await sdkGsapRemovePropertyPersist(
          targetPath,
          animationId,
          property,
          from,
          sdkSession,
          sdkDeps,
          { label: `Remove GSAP ${from ? `from-${property}` : property}` },
        );
        if (handled) return;
      }
      if (from) {
        commitMutationSafely(
          selection,
          { type: "remove-from-property", animationId, property },
          {
            label: `Remove GSAP from-${property}`,
          },
        );
      } else {
        commitMutationSafely(
          selection,
          { type: "remove-property", animationId, property },
          {
            label: `Remove GSAP ${property}`,
          },
        );
      }
    },
    [commitMutationSafely],
  );

  const removeGsapProperty = useCallback(
    (selection: DomEditSelection, animationId: string, property: string) =>
      removeProperty(selection, animationId, property, false),
    [removeProperty],
  );

  const updateGsapFromProperty = useCallback(
    async (
      selection: DomEditSelection,
      animationId: string,
      property: string,
      value: number | string,
    ) => {
      const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
      if (sdkSession && sdkDeps) {
        const targetPath = selection.sourceFile || activeCompPath || "index.html";
        const handled = await sdkGsapTweenPersist(
          targetPath,
          {
            kind: "set",
            animationId,
            properties: {
              fromProperties: mergeTweenProperties(
                sdkSession,
                animationId,
                { [property]: value },
                "from",
              ),
            },
          },
          sdkSession,
          sdkDeps,
          {
            label: `Edit GSAP from-${property}`,
            coalesceKey: `gsap:${animationId}:from:${property}`,
          },
        );
        if (handled) return;
      }
      commitMutationSafely(
        selection,
        { type: "update-from-property", animationId, property, value },
        {
          label: `Edit GSAP from-${property}`,
          coalesceKey: `gsap:${animationId}:from:${property}`,
        },
      );
    },
    [commitMutationSafely],
  );

  const addGsapFromProperty = useCallback(
    async (selection: DomEditSelection, animationId: string, property: string) => {
      const defaultValue = PROPERTY_DEFAULTS[property] ?? 0;
      const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
      if (sdkSession && sdkDeps) {
        const targetPath = selection.sourceFile || activeCompPath || "index.html";
        const handled = await sdkGsapTweenPersist(
          targetPath,
          {
            kind: "set",
            animationId,
            properties: {
              fromProperties: mergeTweenProperties(
                sdkSession,
                animationId,
                { [property]: defaultValue },
                "from",
              ),
            },
          },
          sdkSession,
          sdkDeps,
          { label: `Add GSAP from-${property}` },
        );
        if (handled) return;
      }
      commitMutationSafely(
        selection,
        { type: "add-from-property", animationId, property, defaultValue },
        { label: `Add GSAP from-${property}` },
      );
    },
    [commitMutationSafely],
  );

  const removeGsapFromProperty = useCallback(
    (selection: DomEditSelection, animationId: string, property: string) =>
      removeProperty(selection, animationId, property, true),
    [removeProperty],
  );

  return {
    updateGsapProperty,
    addGsapProperty,
    removeGsapProperty,
    updateGsapFromProperty,
    addGsapFromProperty,
    removeGsapFromProperty,
  };
}
