"use client";

import { useCallback, useEffect, useMemo, useRef, useState } from "react";

import { fetchVulnerabilityAttachmentUrl } from "@/lib/api/vulnerabilities";

const ALLOWED_TAGS = new Set([
  "p",
  "br",
  "strong",
  "em",
  "b",
  "i",
  "u",
  "ul",
  "ol",
  "li",
  "a",
  "code",
  "pre",
  "blockquote",
  "div",
  "span",
  "h1",
  "h2",
  "h3",
  "h4",
  "h5",
  "h6",
  "table",
  "thead",
  "tbody",
  "tfoot",
  "tr",
  "th",
  "td",
  "img",
]);

const isSafeImageSrc = (src: string) => {
  if (!src) return false;
  const normalized = src.trim().toLowerCase();
  return normalized.startsWith("https://") || normalized.startsWith("http://");
};
const isLikelyPresigned = (src: string) => {
  const normalized = src.toLowerCase();
  return normalized.includes("x-amz-signature=") || normalized.includes("x-amz-algorithm=") || normalized.includes("x-amz-credential=");
};
const SIGNED_URL_REFRESH_SKEW_MS = 60_000;

type AttachmentUrlState = {
  url: string;
  expiresAt: number | null;
};

function expiresAtFromSeconds(expiresIn: number): number | null {
  if (!expiresIn || expiresIn <= 0) return null;
  return Date.now() + expiresIn * 1000;
}

function refreshDelayFor(expiresAt: number): number {
  const ttlMs = Math.max(expiresAt - Date.now(), 0);
  if (ttlMs <= SIGNED_URL_REFRESH_SKEW_MS) {
    return Math.max(Math.floor(ttlMs / 2), 1_000);
  }
  return Math.max(ttlMs - SIGNED_URL_REFRESH_SKEW_MS, 1_000);
}

export function sanitizeVulnerabilityHtmlForDisplay(html: string): string {
  if (!html) return "";
  try {
    const doc = new DOMParser().parseFromString(html, "text/html");
    doc.querySelectorAll("script, iframe, object, embed, link, meta").forEach((node) => node.remove());

    const elements = Array.from(doc.body.querySelectorAll("*"));
    for (const el of elements) {
      const tag = el.tagName.toLowerCase();
      if (!ALLOWED_TAGS.has(tag)) {
        const text = doc.createTextNode(el.textContent || "");
        el.replaceWith(text);
        continue;
      }

      // Remove event handler attributes and most other attributes.
      for (const attr of [...el.attributes]) {
        const name = attr.name.toLowerCase();
        if (name.startsWith("on")) el.removeAttribute(attr.name);
      }

      if (tag === "a") {
        const href = (el.getAttribute("href") || "").trim();
        if (!href || href.toLowerCase().startsWith("javascript:")) {
          el.removeAttribute("href");
        }
        el.setAttribute("target", "_blank");
        el.setAttribute("rel", "noopener noreferrer");
        // Drop all other attributes.
        for (const attr of [...el.attributes]) {
          if (!["href", "target", "rel"].includes(attr.name.toLowerCase())) el.removeAttribute(attr.name);
        }
      } else if (tag === "img") {
        const attachmentId = el.getAttribute("data-sc-attachment-id");
        if (!attachmentId) {
          el.remove();
          continue;
        }
        const src = el.getAttribute("src") || "";
        const keepSrc = isSafeImageSrc(src);
        // Keep only our data attribute + safe src + alt; src will be populated at runtime if missing.
        const alt = el.getAttribute("alt") || "";
        for (const attr of [...el.attributes]) {
          const n = attr.name.toLowerCase();
          if (n !== "data-sc-attachment-id" && n !== "alt" && n !== "src") el.removeAttribute(attr.name);
        }
        if (alt) el.setAttribute("alt", alt);
        if (!keepSrc || isLikelyPresigned(src)) el.removeAttribute("src");
        el.removeAttribute("srcset");
        el.setAttribute("loading", "lazy");
      } else {
        // Drop all attributes for other tags.
        for (const attr of [...el.attributes]) el.removeAttribute(attr.name);
      }
    }

    return doc.body.innerHTML;
  } catch {
    return html;
  }
}

function extractAttachmentIdsFromHtml(html: string): number[] {
  if (!html) return [];
  try {
    const doc = new DOMParser().parseFromString(html, "text/html");
    const ids = Array.from(
      doc.body.querySelectorAll("img[data-sc-attachment-id]"),
    )
      .map((img) => Number(img.getAttribute("data-sc-attachment-id")))
      .filter((id) => id && !Number.isNaN(id));
    return Array.from(new Set(ids));
  } catch {
    return [];
  }
}

function applyAttachmentUrlsToHtml(
  html: string,
  attachmentUrls: Record<number, AttachmentUrlState>,
): string {
  if (!html) return "";
  try {
    const doc = new DOMParser().parseFromString(html, "text/html");
    doc.body.querySelectorAll("img[data-sc-attachment-id]").forEach((img) => {
      const attachmentId = Number(img.getAttribute("data-sc-attachment-id"));
      if (!attachmentId || Number.isNaN(attachmentId)) return;

      const resolvedUrl = attachmentUrls[attachmentId]?.url;
      if (resolvedUrl) {
        img.setAttribute("src", resolvedUrl);
      }
      img.setAttribute("style", "max-width: 100%; height: auto;");
    });
    return doc.body.innerHTML;
  } catch {
    return html;
  }
}

export function VulnerabilityHtml({ html }: { html: string }) {
  const containerRef = useRef<HTMLDivElement | null>(null);
  const retryAttemptedRef = useRef<Set<number>>(new Set());
  const [attachmentUrls, setAttachmentUrls] = useState<Record<number, AttachmentUrlState>>(
    {},
  );
  const sanitizedHtml = useMemo(
    () => sanitizeVulnerabilityHtmlForDisplay(html),
    [html],
  );
  const attachmentIds = useMemo(
    () => extractAttachmentIdsFromHtml(sanitizedHtml),
    [sanitizedHtml],
  );
  const attachmentIdsKey = attachmentIds.join(",");
  const renderedHtml = useMemo(
    () => applyAttachmentUrlsToHtml(sanitizedHtml, attachmentUrls),
    [attachmentUrls, sanitizedHtml],
  );

  const refreshAttachmentUrls = useCallback(
    async (ids: number[]) => {
      const uniqueIds = Array.from(new Set(ids)).filter(Boolean);
      if (!uniqueIds.length) return;

      const entries = await Promise.all(
        uniqueIds.map(async (attachmentId) => {
          try {
            const { url, expires_in: expiresIn } =
              await fetchVulnerabilityAttachmentUrl(attachmentId);
            return [
              attachmentId,
              {
                url,
                expiresAt: expiresAtFromSeconds(expiresIn),
              },
            ] as const;
          } catch (e) {
            console.error(e);
            return null;
          }
        }),
      );
      setAttachmentUrls((current) => {
        const next = { ...current };
        for (const entry of entries) {
          if (!entry) continue;
          next[entry[0]] = entry[1];
        }
        return next;
      });
    },
    [],
  );

  useEffect(() => {
    const missingIds = attachmentIds.filter((id) => !attachmentUrls[id]);
    if (!missingIds.length) return;

    void refreshAttachmentUrls(missingIds);
  }, [attachmentIds, attachmentIdsKey, attachmentUrls, refreshAttachmentUrls]);

  useEffect(() => {
    const refreshableEntries = attachmentIds
      .map((id) => ({ id, expiresAt: attachmentUrls[id]?.expiresAt ?? null }))
      .filter(
        (entry): entry is { id: number; expiresAt: number } =>
          entry.expiresAt !== null,
      );
    if (!refreshableEntries.length) return;

    const nextRefresh = refreshableEntries.reduce((current, entry) =>
      entry.expiresAt < current.expiresAt ? entry : current,
    );
    const timer = window.setTimeout(() => {
      void refreshAttachmentUrls([nextRefresh.id]);
    }, refreshDelayFor(nextRefresh.expiresAt));

    return () => window.clearTimeout(timer);
  }, [attachmentIds, attachmentIdsKey, attachmentUrls, refreshAttachmentUrls]);

  useEffect(() => {
    retryAttemptedRef.current.clear();
  }, [attachmentIdsKey]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const handleImageError = (event: Event) => {
      const img = event.target as HTMLImageElement | null;
      if (!img?.matches?.("img[data-sc-attachment-id]")) return;

      const attachmentId = Number(img.getAttribute("data-sc-attachment-id"));
      if (!attachmentId || Number.isNaN(attachmentId)) return;
      if (retryAttemptedRef.current.has(attachmentId)) {
        console.error("Vulnerability attachment image failed to load after refresh", attachmentId);
        return;
      }
      retryAttemptedRef.current.add(attachmentId);
      setAttachmentUrls((current) => {
        const next = { ...current };
        delete next[attachmentId];
        return next;
      });
      void refreshAttachmentUrls([attachmentId]);
    };

    container.addEventListener("error", handleImageError, true);
    return () => {
      container.removeEventListener("error", handleImageError, true);
    };
  }, [refreshAttachmentUrls]);

  return (
    <>
      <div
        ref={containerRef}
        className="vulnerability-html"
        style={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}
        dangerouslySetInnerHTML={{ __html: renderedHtml || "No description provided." }}
      />
      <style jsx global>{`
        .vulnerability-html table {
          border-collapse: collapse;
          display: block;
          max-width: 100%;
          overflow-x: auto;
          white-space: normal;
        }

        .vulnerability-html th,
        .vulnerability-html td {
          border: 1px solid #d9d9d9;
          padding: 6px 8px;
          text-align: left;
          vertical-align: top;
        }

        .vulnerability-html th {
          background: #fafafa;
          font-weight: 600;
        }
      `}</style>
    </>
  );
}
