import fs from "node:fs/promises";
import path from "node:path";
import { Router } from "express";
import type { Response } from "express";
import { pool } from "../db/client.js";
import { storageProvider } from "../storage/index.js";
import { annotatePreviewTargets, getPreviewTargetSeeds } from "../workspace/previewTargets.js";
import { getPreviewSnapshot } from "./snapshotCache.js";

type ProjectSourceRow = {
  type: string;
  config: Record<string, unknown>;
};

type PageSourceRow = {
  source_path: string;
  source_type: string;
  source_config: Record<string, unknown>;
};

type ResolvedAsset = {
  path: string;
  body: Uint8Array;
  contentType?: string;
};

type PreviewRewriteOptions = {
  focusSelector?: string | null;
  focusIndex?: number | null;
};

export const previewRouter = Router();

function getStringConfig(config: Record<string, unknown>, key: string) {
  const value = config[key];
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}

function getSourceConfig(source: ProjectSourceRow | PageSourceRow) {
  return "source_config" in source ? source.source_config : source.config;
}

function getSourceType(source: ProjectSourceRow | PageSourceRow) {
  return "source_type" in source ? source.source_type : source.type;
}

function safeDecode(value: string) {
  try {
    return decodeURIComponent(value);
  } catch {
    return value;
  }
}

function normalizePreviewPath(value: string) {
  const withoutQuery = safeDecode(value).split("?")[0]?.split("#")[0] ?? "";
  const normalized = path.posix.normalize(`/${withoutQuery.replace(/\\/g, "/")}`).replace(/^\/+/, "");

  return normalized === "." ? "" : normalized;
}

function encodePathname(value: string) {
  return value
    .split("/")
    .filter(Boolean)
    .map((part) => encodeURIComponent(part))
    .join("/");
}

function getMimeType(filePath: string) {
  const extension = path.extname(filePath).toLowerCase();
  const mimeTypes: Record<string, string> = {
    ".avif": "image/avif",
    ".css": "text/css; charset=utf-8",
    ".gif": "image/gif",
    ".htm": "text/html; charset=utf-8",
    ".html": "text/html; charset=utf-8",
    ".ico": "image/x-icon",
    ".jpeg": "image/jpeg",
    ".jpg": "image/jpeg",
    ".js": "text/javascript; charset=utf-8",
    ".json": "application/json; charset=utf-8",
    ".map": "application/json; charset=utf-8",
    ".mp4": "video/mp4",
    ".otf": "font/otf",
    ".png": "image/png",
    ".svg": "image/svg+xml; charset=utf-8",
    ".ttf": "font/ttf",
    ".txt": "text/plain; charset=utf-8",
    ".webm": "video/webm",
    ".webmanifest": "application/manifest+json; charset=utf-8",
    ".webp": "image/webp",
    ".woff": "font/woff",
    ".woff2": "font/woff2",
    ".xml": "application/xml; charset=utf-8"
  };

  return mimeTypes[extension] ?? "application/octet-stream";
}

function isHtml(filePath: string, contentType?: string) {
  return /\.html?$/i.test(filePath) || Boolean(contentType?.includes("text/html"));
}

function isCss(filePath: string, contentType?: string) {
  return /\.css$/i.test(filePath) || Boolean(contentType?.includes("text/css"));
}

function isTextLike(filePath: string, contentType?: string) {
  return isHtml(filePath, contentType) || isCss(filePath, contentType) || /\.(js|json|svg|txt|xml|webmanifest)$/i.test(filePath);
}

function getSiteRoot(config: Record<string, unknown>, currentPath: string) {
  const rootName = getStringConfig(config, "rootName");

  if (rootName) {
    const normalizedRoot = normalizePreviewPath(rootName);

    if (normalizedRoot && (currentPath === normalizedRoot || currentPath.startsWith(`${normalizedRoot}/`))) {
      return normalizedRoot;
    }
  }

  const firstSegment = currentPath.split("/").filter(Boolean)[0] ?? "";

  return firstSegment.includes(".") ? "" : firstSegment;
}

function buildCandidatePaths(requestedPath: string, config: Record<string, unknown>) {
  const normalizedPath = normalizePreviewPath(requestedPath);
  const rootName = normalizePreviewPath(getStringConfig(config, "rootName") ?? "");
  const candidates = new Set<string>();

  function addCandidate(candidate: string) {
    const normalized = normalizePreviewPath(candidate);

    if (!normalized) {
      candidates.add("index.html");
      return;
    }

    candidates.add(normalized);

    if (normalized.endsWith("/")) {
      candidates.add(`${normalized}index.html`);
    }
  }

  addCandidate(normalizedPath);

  if (rootName) {
    if (!normalizedPath.startsWith(`${rootName}/`) && normalizedPath !== rootName) {
      addCandidate(`${rootName}/${normalizedPath}`);
    }

    if (normalizedPath.startsWith(`${rootName}/`)) {
      addCandidate(normalizedPath.slice(rootName.length + 1));
    }
  }

  return [...candidates];
}

async function getLatestProjectSource(projectId: string) {
  const result = await pool.query<ProjectSourceRow>(
    `
      select type, config
      from project_sources
      where project_id = $1
      order by created_at desc
      limit 1;
    `,
    [projectId]
  );

  return result.rows[0] ?? null;
}

async function getLatestPageSource(projectId: string, pageId: string) {
  const result = await pool.query<PageSourceRow>(
    `
      select pv.source_path, ps.type as source_type, ps.config as source_config
      from page_versions pv
      join scan_versions sv on sv.id = pv.scan_version_id
      join project_sources ps on ps.id = sv.source_id
      where sv.project_id = $1
        and pv.page_id = $2
        and sv.status = 'done'
      order by sv.completed_at desc nulls last, sv.created_at desc
      limit 1;
    `,
    [projectId, pageId]
  );

  return result.rows[0] ?? null;
}

async function readBrowserFolderAsset(source: ProjectSourceRow | PageSourceRow, requestedPath: string) {
  const sourceConfig = getSourceConfig(source);
  const importPrefix = getStringConfig(sourceConfig, "importPrefix");

  if (!importPrefix) {
    throw new Error("У проекта нет importPrefix.");
  }

  const normalizedPrefix = importPrefix.endsWith("/") ? importPrefix : `${importPrefix}/`;

  for (const candidate of buildCandidatePaths(requestedPath, sourceConfig)) {
    try {
      const object = await storageProvider.getObject(`${normalizedPrefix}${candidate}`);

      return {
        path: candidate,
        body: object.body,
        contentType: object.contentType
      };
    } catch {
      // Try the next deterministic candidate: rootName/path, path without rootName, etc.
    }
  }

  return null;
}

async function readLocalFolderAsset(source: ProjectSourceRow | PageSourceRow, requestedPath: string) {
  const sourceConfig = getSourceConfig(source);
  const sourceRoot = getStringConfig(sourceConfig, "path");

  if (!sourceRoot) {
    throw new Error("У проекта нет локального пути.");
  }

  const resolvedRoot = path.resolve(sourceRoot);

  for (const candidate of buildCandidatePaths(requestedPath, sourceConfig)) {
    const resolvedPath = path.resolve(resolvedRoot, candidate);

    if (resolvedPath !== resolvedRoot && !resolvedPath.startsWith(`${resolvedRoot}${path.sep}`)) {
      continue;
    }

    try {
      return {
        path: candidate,
        body: await fs.readFile(resolvedPath),
        contentType: getMimeType(candidate)
      };
    } catch {
      // Try the next deterministic candidate.
    }
  }

  return null;
}

async function readProjectAsset(source: ProjectSourceRow | PageSourceRow, requestedPath: string) {
  if (getSourceType(source) === "browser_folder") {
    return readBrowserFolderAsset(source, requestedPath);
  }

  if (getSourceType(source) === "local_folder") {
    return readLocalFolderAsset(source, requestedPath);
  }

  throw new Error("Preview для этого типа источника пока не поддерживается.");
}

function sendNotFound(response: Response) {
  response.status(404).type("text/plain").send("Preview asset not found.");
}

function splitUrlSuffix(value: string) {
  const suffixIndex = value.search(/[?#]/);

  if (suffixIndex === -1) {
    return { pathname: value, suffix: "" };
  }

  return {
    pathname: value.slice(0, suffixIndex),
    suffix: value.slice(suffixIndex)
  };
}

function shouldKeepOriginalUrl(value: string) {
  const trimmed = value.trim();

  return (
    !trimmed ||
    trimmed.startsWith("#") ||
    trimmed.startsWith("//") ||
    /^[a-z][a-z0-9+.-]*:/i.test(trimmed)
  );
}

function buildPreviewSiteUrl(projectId: string, assetPath: string) {
  return `/preview/projects/${encodeURIComponent(projectId)}/site/${encodePathname(assetPath)}`;
}

function buildPreviewDirectoryUrl(projectId: string, directoryPath: string) {
  const normalizedDirectory = normalizePreviewPath(directoryPath);
  const siteUrl = buildPreviewSiteUrl(projectId, normalizedDirectory);

  return siteUrl.endsWith("/") ? siteUrl : `${siteUrl}/`;
}

function rewritePreviewUrl(value: string, projectId: string, siteRoot: string) {
  const trimmed = value.trim();

  if (shouldKeepOriginalUrl(trimmed) || !trimmed.startsWith("/") || trimmed.startsWith("//")) {
    return value;
  }

  const { pathname, suffix } = splitUrlSuffix(trimmed);
  const rootedPath = normalizePreviewPath(`${siteRoot}/${pathname.replace(/^\/+/, "")}`);

  return `${buildPreviewSiteUrl(projectId, rootedPath)}${suffix}`;
}

function rewriteSrcset(value: string, projectId: string, siteRoot: string) {
  return value
    .split(",")
    .map((entry) => {
      const trimmedEntry = entry.trim();

      if (!trimmedEntry) {
        return entry;
      }

      const [url, ...descriptor] = trimmedEntry.split(/\s+/);
      return [rewritePreviewUrl(url ?? "", projectId, siteRoot), ...descriptor].join(" ");
    })
    .join(", ");
}

function buildPreviewBridgeScript() {
  return `
    <script>
      (() => {
        const state = { workspaceKey: "", targets: [], scopePages: [], activeScopeId: "", activeScopePageId: "", lastScopeReportAt: 0 };
        const layerId = "seo-mode-preview-marker-layer";
        let frame = 0;
        let lastActivation = { id: "", at: 0 };

        function ensureLayer() {
          let layer = document.getElementById(layerId);

          if (layer) {
            return layer;
          }

          layer = document.createElement("div");
          layer.id = layerId;
          layer.setAttribute("aria-hidden", "true");
          layer.style.position = "absolute";
          layer.style.left = "0";
          layer.style.top = "0";
          layer.style.zIndex = "2147483647";
          layer.style.pointerEvents = "none";
          document.body.appendChild(layer);
          return layer;
        }

        function scheduleMeasure() {
          window.cancelAnimationFrame(frame);
          frame = window.requestAnimationFrame(measure);
        }

        function syncLayerBounds(layer) {
          const root = document.documentElement;
          const width = Math.max(root.scrollWidth, document.body?.scrollWidth || 0, window.innerWidth);
          const height = Math.max(root.scrollHeight, document.body?.scrollHeight || 0, window.innerHeight);
          layer.style.width = width + "px";
          layer.style.height = height + "px";
        }

        function intersects(rect) {
          return rect.right > rect.left && rect.bottom > rect.top;
        }

        function intersectRects(rect, clipRect) {
          return {
            left: Math.max(rect.left, clipRect.left),
            top: Math.max(rect.top, clipRect.top),
            right: Math.min(rect.right, clipRect.right),
            bottom: Math.min(rect.bottom, clipRect.bottom)
          };
        }

        function getVisibleRect(element) {
          let visibleRect = element.getBoundingClientRect();

          if (visibleRect.width < 2 || visibleRect.height < 2) return null;

          visibleRect = {
            left: visibleRect.left,
            top: visibleRect.top,
            right: visibleRect.right,
            bottom: visibleRect.bottom
          };

          visibleRect = intersectRects(visibleRect, {
            left: 0,
            top: 0,
            right: window.innerWidth,
            bottom: window.innerHeight
          });

          if (!intersects(visibleRect)) return null;

          let parent = element.parentElement;

          while (parent && parent !== document.body && parent !== document.documentElement) {
            const parentStyle = window.getComputedStyle(parent);
            const clipsContent = /(auto|scroll|hidden|clip)/.test(
              [parentStyle.overflow, parentStyle.overflowX, parentStyle.overflowY].join(" ")
            );

            if (clipsContent) {
              visibleRect = intersectRects(visibleRect, parent.getBoundingClientRect());

              if (!intersects(visibleRect)) return null;
            }

            parent = parent.parentElement;
          }

          return visibleRect;
        }

        function getElementForSelector(selector, selectorIndex) {
          if (!selector) return null;

          try {
            const index = Number.isInteger(selectorIndex) && selectorIndex >= 0 ? selectorIndex : 0;
            const matches = Array.from(document.querySelectorAll(selector));

            return matches[Math.max(0, Math.min(index, matches.length - 1))] || null;
          } catch {
            return null;
          }
        }

        function getUsableRect(selector, selectorIndex) {
          const element = getElementForSelector(selector, selectorIndex);
          if (!element) return null;
          const style = window.getComputedStyle(element);
          if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return null;
          return getVisibleRect(element);
        }

        function getHorizontalScroller() {
          const candidates = Array.from(document.querySelectorAll("*"))
            .filter((element) => element instanceof HTMLElement)
            .filter((element) => element.scrollWidth > element.clientWidth + 8)
            .map((element) => {
              const rect = element.getBoundingClientRect();
              const overflow = element.scrollWidth - element.clientWidth;
              const area = Math.max(0, rect.width) * Math.max(0, rect.height);

              return { element, overflow, area };
            })
            .filter((candidate) => candidate.area > 1000);

          candidates.sort((first, second) => {
            if (second.overflow !== first.overflow) return second.overflow - first.overflow;
            return second.area - first.area;
          });

          return candidates[0]?.element ?? null;
        }

        function getPageStartPosition() {
          const scroller = getHorizontalScroller();

          if (scroller) {
            const rect = scroller.getBoundingClientRect();

            return {
              left: rect.left + window.scrollX - scroller.scrollLeft + 14,
              top: rect.top + window.scrollY + 14
            };
          }

          return { left: 14, top: 14 };
        }

        function activateTarget(target, event) {
          event.preventDefault();
          event.stopPropagation();

          if (typeof event.stopImmediatePropagation === "function") {
            event.stopImmediatePropagation();
          }

          const now = Date.now();

          if (lastActivation.id === target.id && now - lastActivation.at < 250) {
            return;
          }

          lastActivation = { id: target.id, at: now };

          window.parent?.postMessage({
            type: "seo-mode:activate-target",
            workspaceKey: state.workspaceKey,
            targetId: target.id,
            sectionId: target.sectionId || ""
          }, "*");
        }

        function createMarker(target, left, top) {
          const marker = document.createElement("button");
          marker.type = "button";
          marker.textContent = String(target.number);
          marker.title = target.label || "Открыть поле правки";
          marker.style.position = "absolute";
          marker.style.left = left + "px";
          marker.style.top = top + "px";
          marker.style.width = "30px";
          marker.style.height = "30px";
          marker.style.display = "grid";
          marker.style.placeItems = "center";
          marker.style.padding = "0";
          marker.style.color = "#fff";
          marker.style.background = "#ff4fb8";
          marker.style.border = "2px solid rgba(255,255,255,.95)";
          marker.style.borderRadius = "999px";
          marker.style.boxShadow = "0 6px 16px rgba(255,79,184,.32)";
          marker.style.cursor = "pointer";
          marker.style.font = "700 12px/1 Inter, Arial, sans-serif";
          marker.style.pointerEvents = "auto";
          marker.style.touchAction = "manipulation";
          marker.style.userSelect = "none";
          marker.addEventListener("pointerdown", (event) => activateTarget(target, event));
          marker.addEventListener("click", (event) => activateTarget(target, event));
          return marker;
        }

        function reportActiveScopePage() {
          if (!state.workspaceKey || !Array.isArray(state.scopePages) || state.scopePages.length === 0) {
            return;
          }

          let activeAtProbe = null;
          let nearestBelow = null;
          let lastAbove = null;
          const probeLine = Math.min(Math.max(window.innerHeight * 0.38, 180), Math.max(180, window.innerHeight - 120));

          for (const page of state.scopePages) {
            const element = getElementForSelector(page.selector, page.selectorIndex);
            if (!element) continue;

            const rect = element.getBoundingClientRect();

            if (rect.height <= 0 || rect.width <= 0) continue;

            if (rect.top <= probeLine && rect.bottom > probeLine) {
              if (!activeAtProbe || rect.top > activeAtProbe.top) {
                activeAtProbe = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, top: rect.top };
              }
              continue;
            }

            if (rect.top > probeLine) {
              if (!nearestBelow || rect.top < nearestBelow.top) {
                nearestBelow = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, top: rect.top };
              }
              continue;
            }

            if (!lastAbove || rect.bottom > lastAbove.bottom) {
              lastAbove = { pageId: page.pageId, scopeId: page.scopeId || page.pageId, bottom: rect.bottom };
            }
          }

          const bestPage = activeAtProbe || nearestBelow || lastAbove;

          if (!bestPage || bestPage.scopeId === state.activeScopeId) {
            return;
          }

          const now = Date.now();
          if (now - state.lastScopeReportAt < 120) {
            return;
          }

          state.activeScopeId = bestPage.scopeId;
          state.activeScopePageId = bestPage.pageId;
          state.lastScopeReportAt = now;
          window.parent?.postMessage({
            type: "seo-mode:active-scope-page",
            workspaceKey: state.workspaceKey,
            scopeId: bestPage.scopeId,
            pageId: bestPage.pageId
          }, "*");
        }

        function reportPreviewPointerDown() {
          if (!state.workspaceKey) {
            return;
          }

          window.parent?.postMessage({
            type: "seo-mode:preview-pointerdown",
            workspaceKey: state.workspaceKey
          }, "*");
        }

        function measure() {
          const layer = ensureLayer();
          const root = document.documentElement;
          const documentWidth = Math.max(root.scrollWidth, document.body?.scrollWidth || 0, window.innerWidth);
          syncLayerBounds(layer);
          layer.textContent = "";
          const clusters = new Map();

          for (const target of state.targets) {
            try {
              const exactSelectors = Array.isArray(target.selectors) && target.selectors.length > 0
                ? target.selectors
                : [];
              const prefersPageStart = target.preferPageStart || (target.pageStartFallback && exactSelectors.length === 0);
              const selectors = prefersPageStart ? [] : (exactSelectors.length > 0 ? exactSelectors : [target.selector].filter(Boolean));
              let placedExactMarker = false;
              let placedFallbackMarker = false;

              for (const selector of selectors) {
                const rect = getUsableRect(selector, target.selectorIndex);
                if (!rect) continue;
                placedExactMarker = true;
                const baseLeft = Math.max(12, rect.left + window.scrollX + 10);
                const baseTop = Math.max(12, rect.top + window.scrollY + 10);
                const clusterKey = Math.round(baseLeft / 40) + ":" + Math.round(baseTop / 40);
                const cluster = clusters.get(clusterKey) || [];
                cluster.push({ target, baseLeft, baseTop });
                clusters.set(clusterKey, cluster);
              }

              if (!prefersPageStart && !placedExactMarker && target.fallbackSelector) {
                const rect = getUsableRect(target.fallbackSelector, target.fallbackSelectorIndex);
                if (rect) {
                  placedFallbackMarker = true;
                  const baseLeft = Math.max(12, rect.left + window.scrollX + 10);
                  const baseTop = Math.max(12, rect.top + window.scrollY + 10);
                  const clusterKey = Math.round(baseLeft / 40) + ":" + Math.round(baseTop / 40);
                  const cluster = clusters.get(clusterKey) || [];
                  cluster.push({ target, baseLeft, baseTop });
                  clusters.set(clusterKey, cluster);
                }
              }

              if (!placedExactMarker && !placedFallbackMarker && target.pageStartFallback) {
                const pageStartPosition = getPageStartPosition();
                const baseLeft = pageStartPosition.left;
                const baseTop = pageStartPosition.top;
                const clusterKey = "page-start";
                const cluster = clusters.get(clusterKey) || [];
                cluster.push({ target, baseLeft, baseTop, allowViewportOverflow: true });
                clusters.set(clusterKey, cluster);
              }
            } catch {
              // A broken selector should not break the preview bridge.
            }
          }

          for (const cluster of clusters.values()) {
            const markerSize = 30;
            const gap = 6;
            const maxColumns = 8;
            const columns = Math.min(maxColumns, cluster.length);
            const clusterWidth = columns * markerSize + Math.max(0, columns - 1) * gap;
            const shouldClamp = !cluster.some((entry) => entry.allowViewportOverflow);
            const startLeft = shouldClamp
              ? Math.min(
                  Math.max(12, cluster[0].baseLeft),
                  Math.max(12, documentWidth - clusterWidth - 12)
                )
              : cluster[0].baseLeft;
            const startTop = Math.max(12, cluster[0].baseTop);

            cluster.forEach((entry, index) => {
              const column = index % columns;
              const row = Math.floor(index / columns);
              layer.appendChild(
                createMarker(
                  entry.target,
                  startLeft + column * (markerSize + gap),
                  startTop + row * (markerSize + gap)
                )
              );
            });
          }

          reportActiveScopePage();
        }

        window.addEventListener("message", (event) => {
          const data = event.data;

          if (!data || data.type !== "seo-mode:measure-targets") {
            return;
          }

          state.workspaceKey = data.workspaceKey || "";
          state.targets = Array.isArray(data.targets) ? data.targets : [];
          state.scopePages = Array.isArray(data.scopePages) ? data.scopePages : [];
          state.activeScopeId = data.activeScopeId || state.activeScopeId || "";
          state.activeScopePageId = data.activePageId || state.activeScopePageId || "";
          scheduleMeasure();
        });

        window.addEventListener("resize", scheduleMeasure);
        window.addEventListener("scroll", scheduleMeasure, { passive: true });
        document.addEventListener("scroll", scheduleMeasure, { capture: true, passive: true });
        document.addEventListener("pointerdown", reportPreviewPointerDown, { capture: true, passive: true });
        window.addEventListener("load", () => {
          window.setTimeout(scheduleMeasure, 50);
          window.setTimeout(scheduleMeasure, 500);
          window.setTimeout(scheduleMeasure, 1500);
        });
        document.addEventListener("DOMContentLoaded", () => {
          window.setTimeout(scheduleMeasure, 50);
          window.setTimeout(scheduleMeasure, 500);
        });
      })();
    </script>
  `;
}

function buildPreviewFocusScript(selector: string | null | undefined, index: number | null | undefined = null) {
  if (!selector) {
    return "";
  }

  return `
    <script>
      (() => {
        const selector = ${JSON.stringify(selector)};
        const targetIndex = ${Number.isInteger(index) && index !== null ? index : 0};
        const focusTarget = () => {
          let target = null;

          try {
            const matches = Array.from(document.querySelectorAll(selector));
            target = matches[Math.max(0, Math.min(targetIndex, matches.length - 1))] || null;
          } catch {
            target = null;
          }

          if (!target) return;

          target.scrollIntoView({ block: "center", inline: "center" });

          if (target instanceof HTMLElement) {
            target.style.outline = "2px solid rgba(34, 197, 94, 0.55)";
            target.style.outlineOffset = "8px";
          }
        };

        window.addEventListener("load", () => {
          window.setTimeout(focusTarget, 80);
          window.setTimeout(focusTarget, 600);
          window.setTimeout(focusTarget, 1400);
        });
        document.addEventListener("DOMContentLoaded", () => window.setTimeout(focusTarget, 80));
      })();
    </script>
  `;
}

function getQueryString(value: unknown) {
  const candidate = Array.isArray(value) ? value[0] : value;

  if (typeof candidate !== "string") {
    return null;
  }

  const trimmed = candidate.trim();

  return trimmed.length > 0 && trimmed.length < 300 ? trimmed : null;
}

function getQueryInteger(value: unknown) {
  const candidate = Array.isArray(value) ? value[0] : value;
  const parsed = typeof candidate === "string" ? Number.parseInt(candidate, 10) : Number.NaN;

  return Number.isInteger(parsed) && parsed >= 0 && parsed < 200 ? parsed : null;
}

function getQueryDimension(value: unknown, fallback: number) {
  const candidate = Array.isArray(value) ? value[0] : value;
  const parsed = typeof candidate === "string" ? Number.parseInt(candidate, 10) : Number.NaN;

  return Number.isInteger(parsed) && parsed >= 320 && parsed <= 4096 ? parsed : fallback;
}

function rewriteHtml(
  html: string,
  projectId: string,
  resolvedPath: string,
  sourceConfig: Record<string, unknown>,
  options: PreviewRewriteOptions = {}
) {
  const siteRoot = getSiteRoot(sourceConfig, resolvedPath);
  const currentDirectory = path.posix.dirname(resolvedPath);
  const baseDirectory = currentDirectory === "." ? "" : `${currentDirectory}/`;
  const baseHref = buildPreviewDirectoryUrl(projectId, baseDirectory);
  const annotatedHtml = annotatePreviewTargets(html, getPreviewTargetSeeds(html));
  const withoutBase = annotatedHtml.replace(/<base\b[^>]*>/gi, "");
  const withRootedAttributes = withoutBase
    .replace(/\s(src|href|poster|action)\s*=\s*(["'])([^"']*)\2/gi, (match, attribute: string, quote: string, value: string) => {
      return ` ${attribute}=${quote}${rewritePreviewUrl(value, projectId, siteRoot)}${quote}`;
    })
    .replace(/\s(srcset)\s*=\s*(["'])([^"']*)\2/gi, (match, attribute: string, quote: string, value: string) => {
      return ` ${attribute}=${quote}${rewriteSrcset(value, projectId, siteRoot)}${quote}`;
    });
  const baseTag = `<base href="${baseHref}">`;
  const bridgeScript = buildPreviewBridgeScript();
  const focusScript = buildPreviewFocusScript(options.focusSelector, options.focusIndex);

  if (/<head\b[^>]*>/i.test(withRootedAttributes)) {
    return withRootedAttributes
      .replace(/<head\b[^>]*>/i, (headTag) => `${headTag}${baseTag}`)
      .replace(/<\/body>/i, `${bridgeScript}${focusScript}</body>`);
  }

  return `<!doctype html><html><head>${baseTag}</head><body>${withRootedAttributes}${bridgeScript}${focusScript}</body></html>`;
}

function rewriteCss(css: string, projectId: string, resolvedPath: string, sourceConfig: Record<string, unknown>) {
  const siteRoot = getSiteRoot(sourceConfig, resolvedPath);

  return css
    .replace(/url\(\s*(["']?)([^"')]+)\1\s*\)/gi, (_match, quote: string, value: string) => {
      const rewritten = rewritePreviewUrl(value, projectId, siteRoot);
      return `url(${quote}${rewritten}${quote})`;
    })
    .replace(/@import\s+(["'])([^"']+)\1/gi, (_match, quote: string, value: string) => {
      return `@import ${quote}${rewritePreviewUrl(value, projectId, siteRoot)}${quote}`;
    });
}

function textFromAsset(asset: ResolvedAsset) {
  return new TextDecoder().decode(asset.body);
}

function sendAsset(response: Response, asset: ResolvedAsset, body: string | Uint8Array, contentType?: string) {
  response.setHeader("Cache-Control", "no-store");
  response.type(contentType ?? asset.contentType ?? getMimeType(asset.path));
  response.send(typeof body === "string" ? body : Buffer.from(body));
}

previewRouter.get("/projects/:projectId/pages/:pageId", async (request, response) => {
  try {
    const page = await getLatestPageSource(request.params.projectId, request.params.pageId);

    if (!page) {
      sendNotFound(response);
      return;
    }

    response.redirect(302, buildPreviewSiteUrl(request.params.projectId, page.source_path));
  } catch (error) {
    console.error(error);
    response.status(500).type("text/plain").send("Preview page failed.");
  }
});

previewRouter.get(/^\/projects\/([^/]+)\/snapshot\/?(.*)$/i, async (request, response) => {
  try {
    const params = request.params as Record<string, string | undefined>;
    const projectId = params[0] ?? "";
    const rawAssetPath = params[1] ?? "";
    const cacheVersion = getQueryString(request.query.cacheVersion) ?? getQueryString(request.query.scanId) ?? "latest";
    const focusSelector = getQueryString(request.query.focus);
    const focusIndex = getQueryInteger(request.query.focusIndex);
    const viewportWidth = getQueryDimension(request.query.viewportWidth, 1920);
    const viewportHeight = getQueryDimension(request.query.viewportHeight, 1080);
    const origin = `${request.protocol}://${request.get("host")}`;
    const snapshot = await getPreviewSnapshot({
      focusIndex,
      focusSelector,
      origin,
      projectId,
      cacheVersion,
      sourcePath: rawAssetPath || "index.html",
      viewportHeight,
      viewportWidth
    });

    response
      .status(200)
      .setHeader("Cache-Control", "public, max-age=31536000, immutable")
      .setHeader("X-SEO-Mode-Preview-Cache", snapshot.status)
      .type("image/png")
      .send(Buffer.from(snapshot.body));
  } catch (error) {
    console.error(error);
    response.status(503).type("text/plain").send("Preview snapshot failed.");
  }
});

previewRouter.get(/^\/projects\/([^/]+)\/site\/?(.*)$/i, async (request, response) => {
  try {
    const params = request.params as Record<string, string | undefined>;
    const projectId = params[0] ?? "";
    const rawAssetPath = params[1] ?? "";
    const focusSelector = getQueryString(request.query.focus);
    const focusIndex = getQueryInteger(request.query.focusIndex);
    const source = await getLatestProjectSource(projectId);

    if (!source) {
      sendNotFound(response);
      return;
    }

    const asset = await readProjectAsset(source, rawAssetPath || "index.html");

    if (!asset) {
      sendNotFound(response);
      return;
    }

    const contentType = asset.contentType ?? getMimeType(asset.path);

    if (isHtml(asset.path, contentType)) {
      sendAsset(
        response,
        asset,
        rewriteHtml(textFromAsset(asset), projectId, asset.path, source.config, { focusIndex, focusSelector }),
        "text/html; charset=utf-8"
      );
      return;
    }

    if (isCss(asset.path, contentType)) {
      sendAsset(response, asset, rewriteCss(textFromAsset(asset), projectId, asset.path, source.config), "text/css; charset=utf-8");
      return;
    }

    sendAsset(response, asset, isTextLike(asset.path, contentType) ? textFromAsset(asset) : asset.body, contentType);
  } catch (error) {
    console.error(error);
    response.status(500).type("text/plain").send("Preview asset failed.");
  }
});
