/* eslint-disable no-alert */
/* eslint-disable no-param-reassign */
/* eslint-env browser */
import path from 'path';
import { getDomain } from 'tldts';
import { runAxeScript } from '../commonCrawlerFunc.js';
import { capturePageData } from '../pageCapture.js';
import { consoleLogger, guiInfoLog, silentLogger } from '../../logs.js';
import { guiInfoStatusTypes, STATUS_CODE_METADATA } from '../../constants/constants.js';
import { isSkippedUrl, validateCustomFlowLabel } from '../../constants/common.js';

declare global {
  interface Window {
    handleOnScanClick?: () => Promise<void> | void;
    handleOnStopClick?: () => Promise<void> | void;
    oobeeSetCollapsed?: (val: boolean) => void;
    oobeeShowStopModal?: () => Promise<{ confirmed: boolean; label: string }>;
    oobeeHideStopModal?: () => void;
    updateMenuPos?: (pos: 'LEFT' | 'RIGHT') => void;
  }
}

const sameRegistrableDomain = (hostA: string, hostB: string) => {
  const domainA = getDomain(hostA);
  const domainB = getDomain(hostB);

  if (!domainA || !domainB) return hostA === hostB;

  return domainA === domainB;
};

const parseBoolEnv = (val: string | undefined, defaultVal: boolean) => {
  if (val == null) return defaultVal;
  const v = String(val).trim().toLowerCase();
  if (['1', 'true', 'yes', 'y', 'on'].includes(v)) return true;
  if (['0', 'false', 'no', 'n', 'off'].includes(v)) return false;
  return defaultVal;
};

const RESTRICT_OVERLAY_TO_ENTRY_DOMAIN = parseBoolEnv(
  process.env.RESTRICT_OVERLAY_TO_ENTRY_DOMAIN,
  false,
);
const OVERLAY_OPERATION_TIMEOUT_MS = 5000;

const isOverlayAllowed = (currentUrl: string, entryUrl: string) => {
  try {
    const cur = new URL(currentUrl);

    if (cur.protocol !== 'http:' && cur.protocol !== 'https:') return false;

    if (!RESTRICT_OVERLAY_TO_ENTRY_DOMAIN) return true;

    const base = new URL(entryUrl);

    return sameRegistrableDomain(cur.hostname, base.hostname);
  } catch {
    return false;
  }
};

//! For Cypress Test
// env to check if Cypress test is running
const isCypressTest = process.env.IS_CYPRESS_TEST === 'true';

export const DEBUG = false;
export const log = str => {
  if (DEBUG) {
    console.log(str);
  }
};

export const screenshotFullPage = async (page, screenshotsDir: string, screenshotIdx) => {
  const imgName = `PHScan-screenshot${screenshotIdx}.png`;
  const imgPath = path.join(screenshotsDir, imgName);
  const originalSize = page.viewportSize();

  try {
    const fullPageSize = await page.evaluate(() => ({
      width: Math.max(
        document.body.scrollWidth,
        document.documentElement.scrollWidth,
        document.body.offsetWidth,
        document.documentElement.offsetWidth,
        document.body.clientWidth,
        document.documentElement.clientWidth,
      ),
      height: Math.max(
        document.body.scrollHeight,
        document.documentElement.scrollHeight,
        document.body.offsetHeight,
        document.documentElement.offsetHeight,
        document.body.clientHeight,
        document.documentElement.clientHeight,
      ),
    }));

    const usesInfiniteScroll = async () => {
      const prevHeight = await page.evaluate(() => document.body.scrollHeight);

      await page.evaluate(() => {
        window.scrollTo(0, document.body.scrollHeight);
      });

      const isLoadMoreContent = async () => {
        await new Promise(resolve => setTimeout(resolve, 2500));
        if (page.isClosed()) return false;
        try {
          await page.waitForLoadState('domcontentloaded');
          const newHeight = await page.evaluate(() => document.body.scrollHeight);
          return newHeight > prevHeight;
        } catch {
          return false;
        }
      };

      const result = await isLoadMoreContent();
      return result;
    };

    await usesInfiniteScroll();

    // scroll back to top of page for screenshot
    await page.evaluate(() => {
      window.scrollTo(0, 0);
    });

    consoleLogger.info(`Screenshot page at: ${page.url()}`);

    await page.screenshot({
      timeout: 5000,
      path: imgPath,
      clip: {
        x: 0,
        y: 0,
        width: fullPageSize.width,
        height: 5400,
      },
      fullPage: true,
      scale: 'css',
    });

    if (originalSize) await page.setViewportSize(originalSize);
  } catch {
    consoleLogger.error('Unable to take screenshot');
    // Do not return screenshot path if screenshot fails
    return '';
  }

  return `screenshots/${imgName}`; // relative path from reports folder
};

export const runAxeScan = async (
  page,
  includeScreenshots,
  randomToken,
  customFlowDetails,
  dataset,
  urlsCrawled,
) => {
  const result = await runAxeScript({ includeScreenshots, page, randomToken, customFlowDetails });

  if (result.axeScanFailed) {
    guiInfoLog(guiInfoStatusTypes.ERROR, {
      numScanned: urlsCrawled.scanned.length,
      urlScanned: page.url(),
    });
    urlsCrawled.error.push({
      url: page.url(),
      pageTitle: result.pageTitle,
      actualUrl: page.url(),
      metadata: STATUS_CODE_METADATA[2],
      httpStatusCode: 2,
    });
    return;
  }

  await capturePageData(page, page.url(), randomToken);

  await dataset.pushData(result);

  const rawTitle = result.pageTitle ?? '';
  let pageTitleTextOnly = rawTitle; // Note: The original pageTitle contains the index and is being used in top 10 issues

  if (typeof result.pageIndex === 'number') {
    const re = new RegExp(`^\\s*${result.pageIndex}\\s*:\\s*`);
    pageTitleTextOnly = rawTitle.replace(re, '');
  } else {
    pageTitleTextOnly = rawTitle.replace(/^\s*\d+\s*:\s*/, '');
  }

  urlsCrawled.scanned.push({
    url: page.url(),
    pageTitle: pageTitleTextOnly,
    pageImagePath: customFlowDetails.pageImagePath,
  });
};

export const processPage = async (page, processPageParams) => {
  // make sure to update processPageParams' scannedIdx
  processPageParams.scannedIdx += 1;

  let { includeScreenshots } = processPageParams;

  const {
    scannedIdx,
    blacklistedPatterns,
    dataset,
    intermediateScreenshotsPath,
    urlsCrawled,
    randomToken,
  } = processPageParams;

  try {
    await page.waitForLoadState('domcontentloaded', { timeout: 10000 });
  } catch {
    consoleLogger.info('Unable to detect page load state');
  }

  consoleLogger.info(`Attempting to scan: ${page.url()}`);

  const pageUrl = page.url();

  if (blacklistedPatterns && isSkippedUrl(pageUrl, blacklistedPatterns)) {
    const continueScan = await page.evaluate(() =>
      window.confirm('Page has been excluded, would you still like to proceed with the scan?'),
    );
    if (!continueScan) {
      urlsCrawled.userExcluded.push({
        url: pageUrl,
        pageTitle: pageUrl,
        actualUrl: pageUrl,
      });

      return;
    }
  }

  // TODO: Check if necessary
  // To skip already scanned pages
  // if (urlsCrawled.scanned.some(scan => scan.url === pageUrl)) {
  //   page.evaluate(() => {
  //     window.alert('Page has already been scanned, skipping scan.');
  //   });
  //   return;
  // }

  try {
    const initialScrollPos = await page.evaluate(() => ({
      x: window.scrollX,
      y: window.scrollY,
    }));

    const pageImagePath = await screenshotFullPage(page, intermediateScreenshotsPath, scannedIdx);

    // TODO: This is a temporary fix to not take element screenshots on pages when errors out at full page screenshot
    if (pageImagePath === '') {
      includeScreenshots = false;
    }

    await runAxeScan(
      page,
      includeScreenshots,
      randomToken,
      {
        pageIndex: scannedIdx,
        pageImagePath,
      },
      dataset,
      urlsCrawled,
    );

    if (includeScreenshots) {
      consoleLogger.info(`Successfully screenshot page at: ${page.url()}`);
    }

    guiInfoLog(guiInfoStatusTypes.SCANNED, {
      numScanned: urlsCrawled.scanned.length,
      urlScanned: pageUrl,
    });

    await page.evaluate(pos => {
      window.scrollTo(pos.x, pos.y);
    }, initialScrollPos);
  } catch {
    consoleLogger.error(`Error in scanning page: ${pageUrl}`);
  }
};

export const MENU_POSITION = {
  left: 'LEFT',
  right: 'RIGHT',
};

type OverlayOpts = {
  inProgress?: boolean;
  collapsed?: boolean;
  hideStopInput?: boolean;
};

export const updateMenu = async (page, urlsCrawled) => {
  log(`Overlay menu: updating: ${page.url()}`);
  await page.evaluate(
    vars => {
      const shadowHost = document.querySelector('#oobeeShadowHost');
      if (shadowHost) {
        const p = shadowHost.shadowRoot.querySelector('#oobee-p-pages-scanned');
        if (p) {
          p.textContent = `Pages Scanned: ${vars.urlsCrawled.scanned.length || 0}`;
        }
      }
    },
    { urlsCrawled },
  );

  consoleLogger.info(`Overlay menu updated`);
};

export const addOverlayMenu = async (
  page,
  urlsCrawled,
  menuPos,
  opts: OverlayOpts = {
    inProgress: false,
    collapsed: false,
  },
) => {
  try {
    await page.waitForLoadState('domcontentloaded', { timeout: 2000 });
  } catch {
    // In CDP mode the load state may not resolve after script injection (e.g. axe-core).
    // Proceed with injection anyway — the DOM is accessible if evaluate() succeeds.
  }
  consoleLogger.info(`Overlay menu: adding to ${menuPos}...`);

  // Add the overlay menu with initial styling
  return page
    .evaluate(
      async vars => {
        const customWindow: Window = window as unknown as Window;
        const inProgress = !!(vars?.opts && vars.opts.inProgress);
        const collapsedOption = !!(vars?.opts && vars.opts.collapsed);

        const panel = document.createElement('aside');
        panel.className = 'oobee-panel';

        const minBtn = document.createElement('button');
        minBtn.type = 'button';
        minBtn.className = 'oobee-minbtn';
        minBtn.setAttribute('aria-label', 'Minimize/expand panel');

        const MINBTN_SVG = `
          <svg class="oobee-minbtn__icon" xmlns="http://www.w3.org/2000/svg"
              width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
            <g clip-path="url(#clip0_59_3691)">
              <path d="M6.41 6L5 7.41L9.58 12L5 16.59L6.41 18L12.41 12L6.41 6Z" fill="#9021A6"/>
              <path d="M14.41 6L13 7.41L17.58 12L13 16.59L14.41 18L20.41 12L14.41 6Z" fill="#9021A6"/>
            </g>
            <defs>
              <clipPath id="clip0_59_3691">
                <rect width="24" height="24" fill="white"/>
              </clipPath>
            </defs>
          </svg>
        `;
        minBtn.innerHTML = MINBTN_SVG;

        let currentPos: 'LEFT' | 'RIGHT' = vars.menuPos || 'RIGHT';
        const isCollapsed = () => panel.classList.contains('collapsed');

        const setPosClass = (pos: 'LEFT' | 'RIGHT') => {
          panel.classList.remove('pos-left', 'pos-right');
          minBtn.classList.remove('pos-left', 'pos-right');
          if (pos === 'LEFT') {
            panel.classList.add('pos-left');
            minBtn.classList.add('pos-left');
          } else {
            panel.classList.add('pos-right');
            minBtn.classList.add('pos-right');
          }
          positionMinimizeBtn();
          setDraggableSidebarMenu();
        };

        const toggleCollapsed = (force?: boolean) => {
          const willCollapse = typeof force === 'boolean' ? force : !isCollapsed();
          if (willCollapse) {
            panel.classList.add('collapsed');
            localStorage.setItem('oobee:overlay-collapsed', '1');
            customWindow.oobeeSetCollapsed?.(true);
          } else {
            panel.classList.remove('collapsed');
            localStorage.setItem('oobee:overlay-collapsed', '0');
            customWindow.oobeeSetCollapsed?.(false);
          }
          positionMinimizeBtn();
          setDraggableSidebarMenu();
        };

        setPosClass(currentPos);
        const persisted = localStorage.getItem('oobee:overlay-collapsed');
        const startCollapsed = persisted != null ? persisted === '1' : collapsedOption;
        if (startCollapsed) panel.classList.add('collapsed');

        const header = document.createElement('div');
        header.className = 'oobee-header';

        const grip = document.createElement('button');
        grip.type = 'button';
        grip.className = 'oobee-grip';
        grip.setAttribute('aria-label', 'Drag to move panel left or right');

        const GRIP_SVG = `
          <svg class="oobee-grip__icon" xmlns="http://www.w3.org/2000/svg"
              width="24" height="24" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false">
            <path d="M6 11C4.9 11 4 10.1 4 9C4 7.9 4.9 7 6 7C7.1 7 8 7.9 8 9C8 10.1 7.1 11 6 11ZM14 9C14 7.9 13.1 7 12 7C10.9 7 10 7.9 10 9C10 10.1 10.9 11 12 11C13.1 11 14 10.1 14 9ZM20 9C20 7.9 19.1 7 18 7C16.9 7 16 7.9 16 9C16 10.1 16.9 11 18 11C19.1 11 20 10.1 20 9ZM16 15C16 16.1 16.9 17 18 17C19.1 17 20 16.1 20 15C20 13.9 19.1 13 18 13C16.9 13 16 13.9 16 15ZM14 15C14 13.9 13.1 13 12 13C10.9 13 10 13.9 10 15C10 16.1 10.9 17 12 17C13.1 17 14 16.1 14 15ZM8 15C8 13.9 7.1 13 6 13C4.9 13 4 13.9 4 15C4 16.1 4.9 17 6 17C7.1 17 8 16.1 8 15Z" fill="#AFAFB0"/>
          </svg>
        `;
        grip.innerHTML = GRIP_SVG;

        const leftSpacer = document.createElement('div');
        leftSpacer.className = 'oobee-spacer';
        const rightSpacer = document.createElement('div');
        rightSpacer.className = 'oobee-spacer';

        header.appendChild(leftSpacer);
        header.appendChild(grip);
        header.appendChild(rightSpacer);

        const body = document.createElement('div');
        body.className = 'oobee-body';

        const h2 = document.createElement('h2');
        h2.id = 'oobeeHPagesScanned';
        h2.className = 'oobee-section-title';
        h2.textContent = `Pages Scanned (${vars.urlsCrawled.scanned.length || 0})`;

        const scanIcon = document.createElement('span');
        scanIcon.className = 'oobee-btn-icon';

        const SCAN_SVG = `
          <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
            <g clip-path="url(#clip0_1421_431)">
              <path d="M12.5763 11.5472L12.2958 11.2857L12.1037 11.1005C12.776 10.3183 12.9194 9.56432 12.9194 8.45969C12.9194 5.99657 10.9228 4 8.45969 4C5.99657 4 4 5.99657 4 8.45969C4 10.9228 5.99657 12.9194 8.45969 12.9194C9.56432 12.9194 10.3183 12.776 11.1005 12.1037L11.2857 12.2958L11.5472 12.5763L14.9777 16L16 14.9777L12.5763 11.5472ZM8.45969 11.5472C6.75129 11.5472 5.37221 10.1681 5.37221 8.45969C5.37221 6.75129 6.75129 5.37221 8.45969 5.37221C10.1681 5.37221 11.5472 6.75129 11.5472 8.45969C11.5472 10.1681 10.1681 11.5472 8.45969 11.5472Z" fill="white"/>
              <path d="M18.5 0H19.5C19.7761 0 20 0.223858 20 0.5V5H18.5V0Z" fill="white"/>
              <path d="M19.5 2.18552e-08L19.5 1.5L15 1.5L15 -2.18556e-07L19.5 2.18552e-08Z" fill="white"/>
              <path d="M1.5 0H0.5C0.223858 0 0 0.223858 0 0.5V5H1.5V0Z" fill="white"/>
              <path d="M0.5 2.18552e-08L0.5 1.5L5 1.5L5 -2.18556e-07L0.5 2.18552e-08Z" fill="white"/>
              <path d="M1.5 20H0.5C0.223858 20 0 19.7761 0 19.5V15H1.5V20Z" fill="white"/>
              <path d="M0.5 20L0.5 18.5L5 18.5L5 20L0.5 20Z" fill="white"/>
              <path d="M18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V15H18.5V20Z" fill="white"/>
              <path d="M19.5 20L19.5 18.5L15 18.5L15 20L19.5 20Z" fill="white"/>
            </g>
            <defs>
              <clipPath id="clip0_1421_431">
                <rect width="20" height="20" fill="white"/>
              </clipPath>
            </defs>
          </svg>
        `;
        
        scanIcon.innerHTML = SCAN_SVG; 
        const scanBtn = document.createElement('button');
        scanBtn.id = 'oobeeBtnScan';
        scanBtn.className = 'oobee-btn oobee-btn-primary';
        scanBtn.disabled = inProgress;
        scanBtn.appendChild(scanIcon);

        const scanText = document.createElement('span');
        scanText.className = 'oobee-btn-text';
        scanText.innerText = 'Scan page';
        scanBtn.appendChild(scanText);

        scanBtn.addEventListener('click', async () => customWindow.handleOnScanClick?.());

        const endScanIcon = document.createElement('span');
        endScanIcon.className = 'oobee-btn-icon';

        const ENDSCAN_SVG = 
          `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
            <path d="M10 0C4.47 0 0 4.47 0 10C0 15.53 4.47 20 10 20C15.53 20 20 15.53 20 10C20 4.47 15.53 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18ZM13.59 5L10 8.59L6.41 5L5 6.41L8.59 10L5 13.59L6.41 15L10 11.41L13.59 15L15 13.59L11.41 10L15 6.41L13.59 5Z" fill="#9021A6"/>
          </svg>
        `;

        endScanIcon.innerHTML = ENDSCAN_SVG;
        const endScanBtn = document.createElement('button');
        endScanBtn.id = 'oobeeBtnEndScan';
        endScanBtn.className = 'oobee-btn oobee-btn-secondary';
        endScanBtn.appendChild(endScanIcon);
        
        const endScanText = document.createElement('span');
        endScanText.className = 'oobee-btn-text';
        endScanText.innerText = 'End scan';
        endScanBtn.appendChild(endScanText);

        endScanBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());

        const btnGroup = document.createElement('div');
        btnGroup.className = 'oobee-actions';
        btnGroup.appendChild(scanBtn);
        btnGroup.appendChild(endScanBtn);

        const listWrap = document.createElement('div');
        listWrap.id = 'oobeeList';
        listWrap.className = 'oobee-list';

        const renderList = () => {
          const scanned = vars.urlsCrawled.scanned || [];
          listWrap.innerHTML = '';

          if (scanned.length === 0) {
            const empty = document.createElement('div');
            empty.className = 'oobee-empty';
            empty.textContent = 'Scan a page to start';
            listWrap.appendChild(empty);
            return;
          }

          const ol = document.createElement('ol');
          ol.className = 'oobee-ol';

          scanned.forEach(item => {
            const li = document.createElement('li');
            li.className = 'oobee-li';

            const title = document.createElement('div');
            title.className = 'oobee-item-title';
            title.textContent = item.pageTitle && item.pageTitle.trim() ? item.pageTitle : item.url;

            const url = document.createElement('div');
            url.className = 'oobee-item-url';
            url.textContent = item.url;

            li.appendChild(title);
            li.appendChild(url);
            ol.appendChild(li);
          });

          listWrap.appendChild(ol);
        };
        renderList();

        body.appendChild(btnGroup);
        body.appendChild(h2);
        body.appendChild(listWrap);

        panel.appendChild(header);
        panel.appendChild(body);

        const sheet = new CSSStyleSheet();
        // TODO: separate out into css file if this gets too big
        sheet.replaceSync(`
          .oobee-panel{
            position: fixed;
            top: 0;
            height: 100vh;
            width: 320px;
            box-sizing: border-box;
            background: #fff;
            color: #111;
            z-index: 2147483647;
            display: flex;
            flex-direction: column;
            border: 1px solid rgba(0,0,0,.08);border-left: none;border-right: none;
            box-shadow: 0 6px 24px rgba(0,0,0,.08);
            transition: width .16s ease,left .16s ease,right .16s ease
          }
          .oobee-panel.pos-right {
            right: 0;
            border-left: 1px solid rgba(0,0,0,.08)
          }
          .oobee-panel.pos-left {
            left: 0;
            border-right: 1px solid rgba(0,0,0,.08)
          }
          .oobee-panel.collapsed {
            width: 58px;
            overflow: hidden
          }

          :host {
            --oobee-gap: 8px;                 /* distance from panel edge */
            --oobee-panel-offset: 320px;      /* overwritten by JS to actual width */
          }

          /* external minimize button (always OUTSIDE the panel) */
          .oobee-minbtn {
            position: fixed;
            top: 0;
            z-index: 2147483647;
            width: 32px;
            height: 32px;
            border: none;
            background: #fff;
            cursor: pointer;
          }

          /* right-docked: button sits to the LEFT of the panel */
          .oobee-minbtn.pos-right{
            right: calc(var(--oobee-panel-offset) + var(--oobee-gap));
          }
          /* left-docked: button sits to the RIGHT of the panel */
          .oobee-minbtn.pos-left{
            left: calc(var(--oobee-panel-offset) + var(--oobee-gap));
          }
          .oobee-minbtn:hover {
            box-shadow:0 4px 12px rgba(0,0,0,.12);
          }
          .oobee-minbtn:active {
            transform:translateY(1px);
          }
          .oobee-minbtn:focus-visible {
            outline: 2px solid #7b4dff;
            outline-offset: 2px;
          }

          .oobee-header {
            position: relative;
            display: flex;
            align-items: center;
            justify-content: space-between;
          }

          .oobee-spacer {
            width:28px;
            height:28px;
          }

          .oobee-grip{
            border: 0;
            background: #FFFFFF;
            cursor: grab;
            margin-top: 0.4rem;
          }
          .oobee-grip:active {
            cursor:grabbing;
          }

          .oobee-body {
            display: flex;
            flex-direction: column;
            flex: 1;
            min-height: 0;
            overflow: hidden;
          }

          .oobee-actions {
            display: flex;
            flex-direction: column;
            gap: 12px;
            padding: 1rem;
          }

          .oobee-panel.collapsed .oobee-actions {
            display: flex;
            justify-content: center;
            padding: 1rem 0.7rem;
          }

          /* Base button */
          .oobee-btn {
            width: 100%;
            min-height: 44px;
            border-radius: 999px;
            padding: 12px 16px;
            font-size: 16px;
            line-height: 1.2;
            font-weight: 400;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 10px;
            transition: {
              box-shadow .12s ease,
              transform .02s ease,
              background-color .12s ease,
              color .12s ease,
              border-color .12s ease;
            }
          }
          .oobee-btn:disabled {
            opacity:.6;
            cursor:not-allowed
          }

          .oobee-panel.collapsed .oobee-btn {
            width: 44px !important;
            height: 44px !important;
            min-width: 44px !important;
            min-height: 44px !important;
            max-width: 44px !important;
            max-height: 44px !important;
            border-radius: 50% !important;
            padding: 0 !important;
            justify-content: center;
            gap: 0;
          }

          /* Primary (filled) */
          .oobee-btn-primary {
            background: #9021a6;
            color: #fff;
            border: 1px solid transparent;
          }
          .oobee-btn-primary:hover:not(:disabled) {
            box-shadow:0 2px 10px rgba(0,0,0,.12);
          }
          .oobee-btn-primary:active:not(:disabled) {
            transform:translateY(1px);
          }
          .oobee-btn-primary:focus-visible {
            outline:2px solid #7b4dff;
            outline-offset:2px;
          }

          /* Stop button */
          .oobee-btn-secondary{
            background: #fff;
            color: #9021A6;
            border: 1px solid #9021A6;
          }
          .oobee-btn-secondary:active:not(:disabled) {
            transform:translateY(1px);
          }
          .oobee-btn-secondary:focus-visible{
            outline: 2px solid #7b4dff;
            outline-offset:2px;
          }

          /* Text for empty scans */
          .oobee-empty{
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100%;
            font-size: 14px;
            color: #555555;
          }

          .oobee-list {
            flex: 1;
            min-height: 0;
            overflow-y: auto;
            padding-left: 1rem;
            padding-right: 1rem;
            padding-bottom: 1rem;
            padding-top: 0;
          }

          .oobee-panel.collapsed .oobee-list {
            display: none;
          }

          .oobee-btn-icon {
            display: inline-flex;
            align-items: center;
            justify-content: center;
            width: 20px;
            height: 20px;
            vertical-align: middle;
          }

          .oobee-btn-text {
            display: inline;
            white-space: nowrap;
            vertical-align: middle;
          }
          
          .oobee-panel.collapsed .oobee-btn-text {
            display: none;
          }

          #oobeeStopOverlay[hidden] {
            display:none !important;
          }
          #oobeeStopOverlay {
            display:grid;
          }

          .oobee-section-title {
            font-size: 16px;
            font-weight: 700;
            color: #161616;
            border-top: 1px solid rgba(0, 0, 0, 0.08);
            padding: 1rem;
            margin: 0;
          }

          .oobee-panel.collapsed .oobee-section-title {
            font-size: 14px;
            display: flex;
            justify-content: center;
            text-align: center;
          }

          .oobee-ol {
            margin: 0;
            padding-left: 1.25rem;
            display: flex;
            flex-direction: column;
            gap: 10px;
          }

          .oobee-li {
            list-style: decimal;
            font-size: 14px;
          }

          .oobee-item-title {
            font-size: 14px;
            color: #161616;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
          }

          .oobee-item-url {
            font-size: 12px;
            color: #6b7280;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            direction: rtl;
            text-align: left;
          }

          .oobee-minbtn__icon {
            transition: transform .18s ease;
            transform: rotate(0deg);
          }
          .oobee-minbtn__icon.is-left {
            transform: rotate(180deg);
          }

          :host-context(.oobee-snap) .oobee-panel,
          :host-context(.oobee-snap) .oobee-minbtn { display:none !important; }

          @media (max-width:1024px) {
            .oobee-panel {
              width:280px
            }
          }
          @media (max-width:768px) {
            .oobee-panel {
              width: 92vw;
              height: 100vh;
              top: 0;
              bottom: 0;
              border-radius: 0;
            }
            .oobee-panel.collapsed {
              width: auto;
              height: auto;
              padding: 0;
              display: flex;
              align-items: center;
              justify-content: center;
              border-radius: 999px;
              box-shadow: 0 6px 24px rgba(0,0,0,.4);
              top: auto;
              bottom: max(16px, env(safe-area-inset-bottom,0px))
            }
          }
        `);

        document.documentElement.classList.remove('oobee-snap');
        const shadowHost = document.createElement('div');
        shadowHost.id = 'oobeeShadowHost';
        const shadowRoot = shadowHost.attachShadow({ mode: 'open' });

        shadowRoot.adoptedStyleSheets = [sheet];

        shadowRoot.appendChild(panel);
        shadowRoot.appendChild(minBtn);

        function setDraggableSidebarMenu() {
          const icon = minBtn.querySelector<SVGElement>('.oobee-minbtn__icon');
          if (!icon) return;

          const closed = isCollapsed();
          const arrowPointsRight =
            (currentPos === 'RIGHT' && !closed) || (currentPos === 'LEFT' && closed);

          icon.classList.toggle('is-left', !arrowPointsRight);
          minBtn.setAttribute('aria-label', closed ? 'Expand panel' : 'Collapse panel');
        }

        function positionMinimizeBtn() {
          const OPEN_OFFSET = 318;
          const COLLAPSED_OFFSET = 55;
          const offset = isCollapsed() ? COLLAPSED_OFFSET : OPEN_OFFSET;

          minBtn.style.left = '';
          minBtn.style.right = '';

          if (currentPos === 'RIGHT') {
            minBtn.style.right = `${offset}px`;
          } else {
            minBtn.style.left = `${offset}px`;
          }
        }
        positionMinimizeBtn();
        setDraggableSidebarMenu();

        minBtn.addEventListener('click', () => toggleCollapsed());

        let startX = 0;
        const THRESH = 40;

        grip.addEventListener('pointerdown', (e: PointerEvent) => {
          startX = e.clientX;
          grip.setPointerCapture(e.pointerId); // <-- use the button
        });

        grip.addEventListener('pointermove', (e: PointerEvent) => {
          if (!grip.hasPointerCapture?.(e.pointerId)) return; // <-- check the button
          const dx = e.clientX - startX;
          if (Math.abs(dx) >= THRESH) {
            const nextPos: 'LEFT' | 'RIGHT' = dx < 0 ? 'LEFT' : 'RIGHT';
            if (nextPos !== currentPos) {
              currentPos = nextPos;
              setPosClass(currentPos);
              window.updateMenuPos?.(currentPos);
            }
            startX = e.clientX;
          }
        });

        grip.addEventListener('pointerup', (e: PointerEvent) => {
          try {
            grip.releasePointerCapture(e.pointerId);
          } catch {}
        });

        const stopDialog = document.createElement('dialog');
        stopDialog.id = 'oobeeStopDialog';
        Object.assign(stopDialog.style, {
          width: 'min(560px, calc(100vw - 32px))',
          border: 'none',
          padding: '0',
          borderRadius: '16px',
          overflow: 'hidden',
          boxShadow: '0 10px 40px rgba(0,0,0,.35)',
          fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
        });
        const dialogSheet = new CSSStyleSheet();
        dialogSheet.replaceSync(`
          #oobeeStopDialog::backdrop {
            background: rgba(0,0,0,.55);
          }

          /* primary button hover/focus */
          .oobee-stop-primary:hover {
            filter: brightness(0.95);
          }
          .oobee-stop-primary:focus-visible {
            outline: 2px solid #7b4dff; outline-offset: 2px;
          }

          /* cancel link hover */
          .oobee-stop-cancel {
            color: #9021A6;
            text-decoration: underline;
          }
          .oobee-stop-cancel:hover {
            filter: brightness(0.95);
          }

          /* close “X” hover ring */
          .oobee-stop-close:hover {
            background: #f3f4f6;
          }
        `);
        shadowRoot.adoptedStyleSheets = [sheet, dialogSheet];

        const head = document.createElement('div');
        Object.assign(head.style, {
          padding: '20px 20px 8px 20px',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'space-between',
          gap: '8px',
        });

        const title = document.createElement('h2');
        title.id = 'oobee-stop-title';
        title.textContent = 'Are you sure you want to stop this scan?';
        Object.assign(title.style, {
          margin: '0',
          fontSize: '22px',
          fontWeight: '700',
          lineHeight: '1.25',
        });

        const closeX = document.createElement('button');
        closeX.type = 'button';
        closeX.setAttribute('aria-label', 'Close');
        closeX.textContent = '×';
        closeX.className = 'oobee-stop-close';
        Object.assign(closeX.style, {
          border: 'none',
          background: 'transparent',
          fontSize: '28px',
          lineHeight: '1',
          cursor: 'pointer',
          color: '#4b5563',
          width: '36px',
          height: '36px',
          borderRadius: '12px',
          display: 'grid',
          placeItems: 'center',
        });
        head.appendChild(title);
        head.appendChild(closeX);

        const bodyWrap = document.createElement('div');
        Object.assign(bodyWrap.style, {
          padding: '12px 20px 20px 20px',
        });

        const form = document.createElement('form');
        form.noValidate = true;
        form.autocomplete = 'off';
        Object.assign(form.style, {
          display: 'grid',
          gridTemplateColumns: '1fr',
          rowGap: '12px',
        });

        const label = document.createElement('label');
        label.setAttribute('for', 'oobee-stop-input');
        label.textContent = 'Enter a name for this scan';
        Object.assign(label.style, { fontSize: '15px', fontWeight: '600' });

        const input = document.createElement('input');
        input.id = 'oobeeStopInput';
        input.type = 'text';
        Object.assign(input.style, {
          width: '100%',
          borderRadius: '5px',
          border: '1px solid #e5e7eb',
          padding: '12px 14px',
          fontSize: '14px',
          outline: 'none',
          boxSizing: 'border-box',
        });
        input.addEventListener('focus', () => {
          input.style.borderColor = '#7b4dff';
          input.style.boxShadow = '0 0 0 3px rgba(123,77,255,.25)';
        });
        input.addEventListener('blur', () => {
          input.style.borderColor = '#e5e7eb';
          input.style.boxShadow = 'none';
        });

        const actions = document.createElement('div');
        Object.assign(actions.style, { display: 'grid', gap: '12px', marginTop: '4px' });

        const primary = document.createElement('button');
        primary.type = 'submit';
        primary.textContent = 'Stop scan';
        primary.className = 'oobee-stop-primary';
        Object.assign(primary.style, {
          border: 'none',
          borderRadius: '999px',
          padding: '12px 16px',
          fontSize: '15px',
          fontWeight: '600',
          color: '#fff',
          background: '#9021A6',
          cursor: 'pointer',
        });

        const cancel = document.createElement('button');
        cancel.type = 'button';
        cancel.textContent = 'No, continue scan';
        cancel.className = 'oobee-stop-cancel';
        Object.assign(cancel.style, {
          border: 'none',
          background: 'transparent',
          fontSize: '14px',
          justifySelf: 'center',
          cursor: 'pointer',
          padding: '6px',
        });

        actions.appendChild(primary);
        actions.appendChild(cancel);
        const shouldHideInput = !!(vars?.opts && vars.opts.hideStopInput);
        if (!shouldHideInput) {
          form.appendChild(label);
          form.appendChild(input);
        }
        form.appendChild(actions);
        bodyWrap.appendChild(form);

        stopDialog.appendChild(head);
        stopDialog.appendChild(bodyWrap);
        shadowRoot.appendChild(stopDialog);

        let stopResolver: null | ((v: { confirmed: boolean; label: string }) => void) = null;
        const hideStop = () => {
          try {
            stopDialog.close();
          } catch {}
          stopResolver = null;
        };
        const showStop = () => {
          if (!shouldHideInput) input.value = '';
          try {
            stopDialog.showModal();
          } catch {}
          if (!shouldHideInput) {
            requestAnimationFrame(() => {
              try {
                input.focus({ preventScroll: true });
                input.select();
              } catch {}
            });
          }
        };
        form.addEventListener('submit', e => {
          e.preventDefault();
          const v = (input.value || '').trim();
          if (stopResolver) stopResolver({ confirmed: true, label: v });
          hideStop();
        });
        closeX.addEventListener('click', () => {
          if (stopResolver) stopResolver({ confirmed: false, label: '' });
          hideStop();
        });
        cancel.addEventListener('click', () => {
          if (stopResolver) stopResolver({ confirmed: false, label: '' });
          hideStop();
        });
        stopDialog.addEventListener('cancel', e => {
          e.preventDefault();
          if (stopResolver) stopResolver({ confirmed: false, label: '' });
          hideStop();
        });
        (customWindow as Window).oobeeShowStopModal = () =>
          new Promise<{ confirmed: boolean; label: string }>(resolve => {
            stopResolver = resolve;
            showStop();
          });
        (customWindow as Window).oobeeHideStopModal = hideStop;

        if (document.body) {
          document.body.appendChild(shadowHost);
        } else if (document.head) {
          // The <head> element exists
          // Append the variable below the head
          document.head.insertAdjacentElement('afterend', shadowHost);
        } else {
          // Neither <body> nor <head> nor <html> exists
          // Append the variable to the document
          document.documentElement.appendChild(shadowHost);
        }
        positionMinimizeBtn();
        setDraggableSidebarMenu();
      },
      { menuPos, MENU_POSITION, urlsCrawled, opts },
    )
    .then(() => {
      log('Overlay menu: successfully added');
    })
    .catch(error => {
      consoleLogger.error('Overlay menu: failed to add', error);
      throw error;
    });
};

export const removeOverlayMenu = async page => {
  await page
    .evaluate(() => {
      const existingOverlay = document.querySelector('#oobeeShadowHost');
      if (existingOverlay) {
        existingOverlay.remove();
        return true;
      }
      return false;
    })
    .then(removed => {
      if (removed) {
        consoleLogger.info('Overlay Menu: successfully removed');
      }
    });
};

export const initNewPage = async (page, pageClosePromises, processPageParams, pagesDict) => {
  let menuPos = MENU_POSITION.right;
  let overlayRefreshSeq = 0;
  let overlayRefreshChain = Promise.resolve();

  // eslint-disable-next-line no-underscore-dangle
  const pageId = page._guid;

  page.on('dialog', async dialog => {
    try {
      await dialog.dismiss();
    } catch {
      // dialog may already be closed
    }
  });

  const pageClosePromise = new Promise(resolve => {
    page.on('close', () => {
      log(`Page: close detected: ${page.url()}`);
      delete pagesDict[pageId];
      resolve(true);
    });
  });
  pageClosePromises.push(pageClosePromise);

  if (!pagesDict[pageId]) {
    pagesDict[pageId] = {
      page,
      isScanning: false,
      collapsed: false,
    };
  }

  const reconcileOverlayMenu = async (trigger: string) => {
    // Mark this as the latest refresh so older ones can stop.
    const refreshSeq = ++overlayRefreshSeq;

    // Serialize overlay updates so multiple navigation events do not add/remove concurrently.
    overlayRefreshChain = overlayRefreshChain
      .catch(() => {})
      .then(async () => {
        if (refreshSeq !== overlayRefreshSeq || page.isClosed()) return;

        // Skip overlay work on schemes we can't inject into — about:blank (no origin,
        // blocks localStorage), chrome://new-tab-page and chrome-error:// (Trusted
        // Types policy blocks innerHTML), devtools://, view-source://, chrome-extension://,
        // etc. The overlay cannot inject on any of these and the attempt produces
        // noisy errors on every new tab / failed navigation. Allow http(s) and file://
        // (used for local-file scans). Subsequent triggers will reconcile the overlay
        // once the tab navigates to a supported scheme.
        const currentUrl = page.url();
        const isInjectable =
          currentUrl.startsWith('http://') ||
          currentUrl.startsWith('https://') ||
          currentUrl.startsWith('file://');
        if (!isInjectable) return;

        // During an active scan, navigation events (framenavigated/domcontentloaded) can fire
        // due to axe-core injection or page resource loading. In CDP mode, concurrent
        // page.evaluate() calls conflict with the running scan. Skip overlay injection
        // for non-scan triggers while scanning — the overlay will be re-added by the
        // 'scan-click' trigger after the scan completes.
        if (pagesDict[pageId]?.isScanning && trigger !== 'scan-click') return;

        try {
          // `framenavigated` can fire before the new document is ready for DOM inspection/injection.
          // Use a short timeout — in CDP mode, waitForLoadState can hang after script injection.
          await page.waitForLoadState('domcontentloaded', { timeout: 2000 });
        } catch {
          // Best effort only. The page may still be mid-navigation or state tracking confused.
        }

        try {
          // Give fast redirect chains a brief chance to advance before we inject/remove the overlay.
          await page.waitForTimeout(300);
        } catch {
          // Best effort only. The page may already be closing.
        }

        // Re-check staleness after waiting because a newer navigation may have happened meanwhile.
        if (refreshSeq !== overlayRefreshSeq || page.isClosed()) return;

        const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);

        if (!allowed) {
          // On macOS and Windows the custom flow always runs headful.
          // The URL guard (urlGuard.ts) intercepts non-http/https navigations
          // and calls page.goto(safeUrl). Do NOT remove the overlay here —
          // removing it causes it to stay permanently disabled if the redirect
          // races ahead of the next reconcile cycle.
          // Instead, fall through to the hasOverlay / addOverlayMenu block so
          // the overlay is (re-)injected even on transient non-http/https URLs
          // (e.g. file://, about:blank) and again after the guard's redirect.
          const isDesktopHost = process.platform === 'darwin' || process.platform === 'win32';
          if (!isDesktopHost) {
            // On Linux / Docker: remove overlay for non-http/https URLs and stop.
            await Promise.race([
              removeOverlayMenu(page),
              new Promise((_, reject) => {
                setTimeout(() => {
                  reject(
                    new Error(
                      `removeOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`,
                    ),
                  );
                }, OVERLAY_OPERATION_TIMEOUT_MS);
              }),
            ]);
            return;
          }
          // Desktop hosts: skip removal and fall through to re-add overlay.
        }

        const hasOverlay = await page.evaluate(() =>
          Boolean(document.querySelector('#oobeeShadowHost')),
        );

        consoleLogger.info(`Overlay state (${trigger}): ${hasOverlay}`);

        if (!hasOverlay) {
          // Recreate the overlay after allowed redirects while preserving current UI state.
          consoleLogger.info(`Adding overlay menu to page (${trigger}): ${page.url()}`);
          await Promise.race([
            addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
              inProgress: !!pagesDict[pageId]?.isScanning,
              collapsed: !!pagesDict[pageId]?.collapsed,
              hideStopInput: !!processPageParams.customFlowLabel,
            }),
            new Promise((_, reject) => {
              setTimeout(() => {
                reject(
                  new Error(`addOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`),
                );
              }, OVERLAY_OPERATION_TIMEOUT_MS);
            }),
          ]);
        }
      })
      .catch(() => {
        consoleLogger.info('Error in adding overlay menu to page');
      });

    await overlayRefreshChain;
  };

  type handleOnScanClickFunction = () => void;

  // Window functions exposed in browser
  const handleOnScanClick: handleOnScanClickFunction = async () => {
    consoleLogger.info('Scan: click detected');
    log('Scan: click detected');
    try {
      pagesDict[pageId].isScanning = true;
      await removeOverlayMenu(page);
      await processPage(page, processPageParams);
      log('Scan: success');
      pagesDict[pageId].isScanning = false;

      if (page.isClosed()) return;
      await reconcileOverlayMenu('scan-click');

      // If the overlay still isn't present after the first attempt (can happen in
      // CDP mode where waitForLoadState tracking is unreliable), retry once.
      if (!page.isClosed()) {
        const overlayPresent = await page.evaluate(() =>
          Boolean(document.querySelector('#oobeeShadowHost')),
        ).catch(() => false);
        if (!overlayPresent) {
          log('Overlay missing after scan-click reconcile, retrying...');
          await reconcileOverlayMenu('scan-click');
        }
      }
    } catch (error) {
      log(`Scan failed ${error}`);
    }
  };

  const handleOnStopClick = async () => {
    const scannedCount = processPageParams?.urlsCrawled?.scanned?.length ?? 0;
    if (scannedCount === 0) {
      if (typeof processPageParams.stopAll === 'function') {
        try {
          await processPageParams.stopAll();
        } catch (e) {
          // ignore invalid; continue without label
        }
      }
      return;
    }

    try {
      const inputValue = await page.evaluate(async () => {
        const win = window as Window;
        if (typeof win.oobeeShowStopModal === 'function') {
          return await win.oobeeShowStopModal();
        }
        const ok = window.confirm('Are you sure you want to stop this scan?');
        return { confirmed: ok, label: '' };
      });

      if (!inputValue?.confirmed) {
        await page.evaluate(() => {
          const endScanBtn = document.getElementById('oobeeBtnEndScan') as HTMLButtonElement | null;
          if (endScanBtn) {
            endScanBtn.disabled = false;
            endScanBtn.textContent = 'Stop';
          }
        });
        return;
      }

      const label = (inputValue.label || '').trim();
      try {
        const { isValid } = validateCustomFlowLabel(label);
        if (isValid && label) {
          processPageParams.customFlowLabel = label;
        }
      } catch {
        // ignore invalid; continue without label
      }

      if (typeof processPageParams.stopAll === 'function') {
        try {
          await processPageParams.stopAll();
        } catch (e) {
          // any console log will be on user browser, do not need to log
        }
      }
    } catch (e) {
      // any console log will be on user browser, do not need to log
    }
  };

  page.on('domcontentloaded', async () => {
    if (page.isClosed()) return;
    await reconcileOverlayMenu('domcontentloaded');

    if (isCypressTest) {
      try {
        await handleOnScanClick();
        page.close();
      } catch {
        consoleLogger.info(
          `Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`,
        );
      }
    }
  });

  page.on('framenavigated', async (frame: any) => {
    if (frame !== page.mainFrame() || page.isClosed()) return;
    await reconcileOverlayMenu('framenavigated');
  });

  try {
    if (page.isClosed()) return page;
    await page.exposeFunction('handleOnScanClick', handleOnScanClick);
    await page.exposeFunction('handleOnStopClick', handleOnStopClick);

    type UpdateMenuPosFunction = (newPos: any) => void;

    // Define the updateMenuPos function
    const updateMenuPos: UpdateMenuPosFunction = newPos => {
      const prevPos = menuPos;
      if (prevPos !== newPos) {
        menuPos = newPos;
      }
    };
    await page.exposeFunction('updateMenuPos', updateMenuPos);
  } catch (e) {
    log(`Error exposing functions on page: ${e}`);
  }

  await reconcileOverlayMenu('init');

  return page;
};
