/**
 * Cutover eligibility checks: whether a batch of patch ops is safe to route
 * through the SDK cutover path instead of the legacy server path. Split out of
 * sdkCutover.ts (which hit the packages/studio 600-line filesize cap) — this
 * block has no dependency on the persist/dispatch functions there.
 */
import type { PatchOperation } from "./sourcePatcher";
import { isAllowedHtmlAttribute, isSafeAttributeValue } from "./htmlAttrSafety";

const CUTOVER_OP_TYPES = new Set<PatchOperation["type"]>([
  "inline-style",
  "text-content",
  "attribute",
  "html-attribute",
]);

// Mirrors the SDK's RESERVED_ATTRS (mutate.ts): a bare `attribute` op is
// force-prefixed `data-`, so e.g. property "end" → "data-end", which the SDK
// rejects with a throw. Detect that up front and decline the whole batch so it
// takes the server path cleanly, instead of throwing inside the dispatch and
// silently falling back per op.
// ponytail: small mirror of the SDK set; if the SDK adds a reserved attr, a new
// op for it just reverts to the (working) throw→fallback path until synced.
const RESERVED_CUTOVER_ATTRS = new Set<string>([
  "data-hf-id",
  "data-composition-id",
  "data-width",
  "data-height",
  "data-start",
  "data-end",
  "data-track-index",
  "data-hold-start",
  "data-hold-end",
  "data-hold-fill",
]);

function sdkAttrName(op: PatchOperation): string | null {
  if (op.type === "attribute") {
    return op.property.startsWith("data-") ? op.property : `data-${op.property}`;
  }
  if (op.type === "html-attribute") return op.property;
  return null;
}

function mapsToReservedAttr(op: PatchOperation): boolean {
  const name = sdkAttrName(op);
  // Lowercase to match the SDK's validateSetAttribute (it lowercases before the
  // reserved check), so "DATA-START" is declined up front too; covers both
  // `attribute` (prefixed) and `html-attribute` (raw) ops.
  return name !== null && RESERVED_CUTOVER_ATTRS.has(name.toLowerCase());
}

// ─── html-attribute safety ───────────────────────────────────────────────────

function hasUnsafeHtmlAttributeOp(ops: PatchOperation[]): boolean {
  return ops.some(
    (op) =>
      op.type === "html-attribute" &&
      (!isAllowedHtmlAttribute(op.property) ||
        (op.value !== null && !isSafeAttributeValue(op.property, op.value))),
  );
}

function hasChildScopedOp(ops: PatchOperation[]): boolean {
  return ops.some((op) => op.childSelector !== undefined);
}

function hasTextContentOp(ops: PatchOperation[]): boolean {
  return ops.some((op) => op.type === "text-content");
}

function targetChildren(target: unknown): unknown[] | null {
  if (!target || typeof target !== "object" || !("children" in target)) return null;
  const children = target.children;
  return Array.isArray(children) ? children : null;
}

function elementTag(element: unknown): string | null {
  if (!element || typeof element !== "object" || !("tag" in element)) return null;
  const tag = element.tag;
  return typeof tag === "string" ? tag.toLowerCase() : null;
}

// Tags that are non-HTML namespace elements in a linkedom-parsed HTML body.
// Mirrors the engine's `isHTMLElementTarget` (model.ts) which uses `instanceof
// HTMLElement` — that runtime check catches the same set, but we can't use it
// here because `target` is a plain SDK object, not a DOM Element. If linkedom
// (or a future parser) surfaces additional foreign-content elements as
// non-HTMLElement, add them here.
const NON_HTML_CHILD_TAGS = new Set(["svg", "math"]);

export function shouldDeclineTextCutoverForTarget(target: unknown, ops: PatchOperation[]): boolean {
  if (!hasTextContentOp(ops)) return false;
  const children = targetChildren(target);
  if (!children) return false;
  // Legacy patch-element replaces the whole element for multi-child targets and
  // for single non-HTML children. The SDK text patch stream stores a scalar
  // inverse, so those shapes cannot be made both byte-identical and undo-safe
  // here. Let the server path remain authoritative for them.
  if (children.length > 1) return true;
  const tag = elementTag(children[0]);
  return tag !== null && NON_HTML_CHILD_TAGS.has(tag);
}

export function shouldUseSdkCutover(
  flagEnabled: boolean,
  hasSession: boolean,
  hfId: string | null | undefined,
  ops: PatchOperation[],
): boolean {
  return (
    flagEnabled &&
    hasSession &&
    !!hfId &&
    ops.length > 0 &&
    ops.every((o) => CUTOVER_OP_TYPES.has(o.type)) &&
    // SDK edit ops target only the element hfId; child-scoped patch ops need the server path.
    !hasChildScopedOp(ops) &&
    !ops.some(mapsToReservedAttr) &&
    !hasUnsafeHtmlAttributeOp(ops)
  );
}
