export const ALLOWED_PAGE_PATH_PREFIX = "/";
export const MAX_PDF_BYTES = 50 * 1024 * 1024;
export const TRANSFER_CHUNK_BYTES = 256 * 1024;

export function inspectDragDescriptor(element, locationHref) {
  if (!element?.dataset) {
    return { descriptor: null, reason: "missing_dataset" };
  }

  let pageUrl;
  let attachmentUrl;
  try {
    pageUrl = new URL(locationHref);
    attachmentUrl = new URL(element.dataset.nativeDragUrl || "", pageUrl);
  } catch {
    return { descriptor: null, reason: "invalid_url" };
  }

  const attachmentId = String(element.dataset.nativeDragId || "");
  const filename = String(element.dataset.nativeDragFilename || "");
  const mimeType = String(element.dataset.nativeDragMime || "").toLowerCase();

  if (pageUrl.protocol !== "https:" ||
      !pageUrl.pathname.startsWith(ALLOWED_PAGE_PATH_PREFIX)) {
    return { descriptor: null, reason: "page_not_https" };
  }
  if (attachmentUrl.protocol !== "https:") {
    return { descriptor: null, reason: "attachment_not_https" };
  }
  if (attachmentUrl.origin !== pageUrl.origin) {
    return { descriptor: null, reason: "cross_origin_attachment" };
  }
  if (attachmentUrl.username || attachmentUrl.password) {
    return { descriptor: null, reason: "attachment_has_credentials" };
  }
  if (!attachmentId || attachmentId.length > 512) {
    return { descriptor: null, reason: "invalid_attachment_id" };
  }
  if (!filename ||
      filename.length > 180 ||
      filename.includes("/") ||
      filename.includes("\\") ||
      !filename.toLowerCase().endsWith(".pdf")) {
    return { descriptor: null, reason: "invalid_filename" };
  }
  if (mimeType !== "application/pdf") {
    return { descriptor: null, reason: "invalid_mime" };
  }

  return {
    descriptor: {
      attachmentId,
      filename,
      mimeType,
      url: attachmentUrl.href
    },
    reason: ""
  };
}

export function readDragDescriptor(element, locationHref) {
  return inspectDragDescriptor(element, locationHref).descriptor;
}

export function validatePdfBytes(value) {
  const bytes = value instanceof Uint8Array ? value : new Uint8Array(value || 0);
  return bytes.length >= 5 &&
    bytes[0] === 0x25 &&
    bytes[1] === 0x50 &&
    bytes[2] === 0x44 &&
    bytes[3] === 0x46 &&
    bytes[4] === 0x2d;
}

export function encodeTransferChunks(value, chunkBytes = TRANSFER_CHUNK_BYTES) {
  const bytes = value instanceof Uint8Array ? value : new Uint8Array(value || 0);
  if (!Number.isSafeInteger(chunkBytes) || chunkBytes < 1) {
    throw new TypeError("chunkBytes must be a positive integer.");
  }

  const messages = [];
  for (let offset = 0, sequence = 0; offset < bytes.length; offset += chunkBytes, sequence += 1) {
    messages.push({
      sequence,
      base64: bytesToBase64(bytes.subarray(offset, Math.min(offset + chunkBytes, bytes.length)))
    });
  }
  return messages;
}

export function findActiveDetailDragChips(root = document) {
  if (!root?.querySelectorAll) {
    return [];
  }

  const allCandidates = Array.from(
    root.querySelectorAll(
      "[data-native-drag-id][data-native-drag-url]"
    )
  ).filter((chip) => chip.isConnected);
  const detailCandidates = allCandidates.filter((chip) => {
    const detailList = chip.closest?.("#spreadsheetDetailAttachmentList");
    const detailModal = chip.closest?.("#spreadsheetDetailShellModal");
    const attachmentPane = detailModal?.querySelector?.(
      "#spreadsheetDetailPaneAttachment"
    );
    return Boolean(
      chip.isConnected &&
      detailList &&
      detailModal &&
      !detailModal.hidden &&
      attachmentPane &&
      !attachmentPane.hidden
    );
  });

  const selected = detailCandidates.find((chip) => chip.matches?.(".active"));
  if (selected) {
    return [selected];
  }
  if (detailCandidates.length) {
    return detailCandidates.slice(0, 1);
  }

  const visibleFallback = allCandidates.find((chip) => {
    if (typeof chip.getClientRects !== "function") {
      return true;
    }
    return chip.getClientRects().length > 0;
  });
  return visibleFallback ? [visibleFallback] : [];
}

const NATIVE_DRAG_STATUS = Object.freeze({
  preparing: Object.freeze({
    label: "PREPARING PDF",
    tone: "preparing",
    animated: false
  }),
  prepared: Object.freeze({
    label: "PDF READY — HOVER TO DRAG",
    tone: "ready",
    animated: false
  }),
  starting: Object.freeze({
    label: "ARMING…",
    tone: "starting",
    animated: false
  }),
  ready: Object.freeze({
    label: "READY — PRESS & DRAG",
    tone: "ready",
    animated: true
  }),
  active: Object.freeze({
    label: "DRAGGING PDF",
    tone: "active",
    animated: false
  }),
  completed: Object.freeze({
    label: "DROP RECEIVED",
    tone: "completed",
    animated: false
  }),
  reset: Object.freeze({
    label: "HOVER PDF — FRESH DRAG",
    tone: "completed",
    animated: false
  }),
  canceled: Object.freeze({
    label: "HOVER PDF TO RETRY",
    tone: "canceled",
    animated: false
  }),
  error: Object.freeze({
    label: "DRAG UNAVAILABLE",
    tone: "error",
    animated: false
  })
});

export function nativeDragStatusPresentation(state) {
  return NATIVE_DRAG_STATUS[String(state || "")] || null;
}

export function nativeDragBadgeSide(
  _rect,
  _viewportWidth,
  _badgeWidth = 190,
  _gap = 8
) {
  return "left";
}

export function updateNativeDragStatusBadge(chip, presentation, side) {
  let badge = chip.querySelector(":scope > .wis225-native-drag-status");
  if (!badge) {
    badge = chip.ownerDocument.createElement("span");
    badge.className = "wis225-native-drag-status";
    badge.setAttribute("aria-hidden", "true");
    chip.append(badge);
  }
  badge.textContent = presentation.label;
  badge.dataset.tone = presentation.tone;
  badge.dataset.animated = presentation.animated ? "true" : "false";
  chip.dataset.nativeDragBadgeSide = side;
  return badge;
}

export function transitionDragSession(session, nextState) {
  const current = session && typeof session === "object"
    ? session
    : { state: "idle", token: "" };
  return {
    ...current,
    state: nextState,
    token: ["error", "reset"].includes(nextState)
      ? ""
      : String(current.token || "")
  };
}

export function canArmOverlay(session) {
  return Boolean(
    session &&
    typeof session.token === "string" &&
    session.token &&
    session.state === "prepared"
  );
}

function bytesToBase64(bytes) {
  let binary = "";
  for (let index = 0; index < bytes.length; index += 1) {
    binary += String.fromCharCode(bytes[index]);
  }
  return btoa(binary);
}
