import type { ElementContext } from "@ltt/shared";

const TEST_ATTRS = ["data-testid", "data-test", "data-qa", "data-cy"];

/**
 * Builds a CSS selector that (when possible) matches exactly one element.
 * Preference: test attributes → #id → aria-label → nth-of-type path (depth ≤ 6).
 */
export function buildUniqueSelector(el: Element): { selector: string; unique: boolean } {
  const candidates: string[] = [];

  for (const attr of TEST_ATTRS) {
    const value = el.getAttribute(attr);
    if (value) candidates.push(`[data-testid="${cssEscape(value)}"]`.replace("data-testid", attr));
  }
  if (el.id) candidates.push(`#${cssEscape(el.id)}`);
  const aria = el.getAttribute("aria-label");
  if (aria) candidates.push(`${el.tagName.toLowerCase()}[aria-label="${cssEscape(aria)}"]`);

  for (const candidate of candidates) {
    if (countMatches(candidate) === 1) return { selector: candidate, unique: true };
  }

  // nth-of-type path, capped at depth 6
  const parts: string[] = [];
  let node: Element | null = el;
  let depth = 0;
  while (node && node.nodeType === 1 && depth < 6) {
    const tag = node.tagName.toLowerCase();
    if (tag === "html" || tag === "body") {
      parts.unshift(tag);
      break;
    }
    let index = 1;
    let sibling = node.previousElementSibling;
    while (sibling) {
      if (sibling.tagName === node!.tagName) index++;
      sibling = sibling.previousElementSibling;
    }
    const siblingsSameTag = node.parentElement
      ? Array.from(node.parentElement.children).filter((c) => c.tagName === node!.tagName).length
      : 1;
    parts.unshift(siblingsSameTag > 1 ? `${tag}:nth-of-type(${index})` : tag);
    // early exit if the path so far is unique
    const candidate = parts.join(" > ");
    if (parts.length >= 2 && countMatches(candidate) === 1) {
      return { selector: candidate, unique: true };
    }
    node = node.parentElement;
    depth++;
  }
  const selector = parts.join(" > ");
  return { selector, unique: countMatches(selector) === 1 };
}

function countMatches(selector: string): number {
  try {
    return document.querySelectorAll(selector).length;
  } catch {
    return 0;
  }
}

function cssEscape(value: string): string {
  if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(value);
  return value.replace(/["\\\]]/g, "\\$&");
}

/** Extracts structured context about an element for the report payload. */
export function buildElementContext(el: Element): ElementContext {
  const rect = el.getBoundingClientRect();
  const { selector, unique } = buildUniqueSelector(el);
  const testAttributes: Record<string, string> = {};
  for (const attr of TEST_ATTRS) {
    const value = el.getAttribute(attr);
    if (value) testAttributes[attr] = value;
  }

  const classes = Array.from(el.classList).slice(0, 10);
  const text = (el.textContent ?? "").trim().replace(/\s+/g, " ");

  return {
    selector,
    unique_selector: unique ? selector : null,
    tag: el.tagName.toLowerCase(),
    id: el.id || null,
    classes,
    text_snippet: text.slice(0, 120) || null,
    aria_label: el.getAttribute("aria-label"),
    test_attributes: Object.keys(testAttributes).length ? testAttributes : null,
    rect: { x: rect.x, y: rect.y, w: rect.width, h: rect.height },
    screenshot_rel: null, // filled by the caller once the screenshot size is known
  };
}

/**
 * Fills screenshot_rel: element rect as fractions of the visible viewport,
 * which matches captureVisibleTab output regardless of devicePixelRatio.
 */
export function withScreenshotRel(
  context: ElementContext,
  viewportWidth: number,
  viewportHeight: number
): ElementContext {
  const rect = context.rect;
  if (!rect || !viewportWidth || !viewportHeight) return context;
  return {
    ...context,
    screenshot_rel: {
      x: rect.x / viewportWidth,
      y: rect.y / viewportHeight,
      w: rect.w / viewportWidth,
      h: rect.h / viewportHeight,
    },
  };
}

export interface BrowserEnvironment {
  user_agent: string;
  browser: string;
  browser_version: string | null;
  os: string | null;
  viewport: { w: number; h: number };
  dpr: number;
  language: string;
  captured_at: string;
}

export function collectEnvironment(): BrowserEnvironment {
  const ua = navigator.userAgent;
  let browser = "Unknown";
  let version: string | null = null;
  const firefox = /Firefox\/([\d.]+)/.exec(ua);
  const chrome = /Chrome\/([\d.]+)/.exec(ua);
  const safari = /Version\/([\d.]+).*Safari/.exec(ua);
  if (firefox) {
    browser = "Firefox";
    version = firefox[1];
  } else if (chrome) {
    browser = "Chrome";
    version = chrome[1];
  } else if (safari) {
    browser = "Safari";
    version = safari[1];
  }
  let os: string | null = null;
  if (/Windows/.test(ua)) os = "Windows";
  else if (/Mac OS X/.test(ua)) os = "macOS";
  else if (/Linux/.test(ua)) os = "Linux";
  else if (/Android/.test(ua)) os = "Android";
  else if (/iPhone|iPad/.test(ua)) os = "iOS";

  return {
    user_agent: ua,
    browser,
    browser_version: version,
    os,
    viewport: { w: window.innerWidth, h: window.innerHeight },
    dpr: window.devicePixelRatio,
    language: navigator.language,
    captured_at: new Date().toISOString(),
  };
}