/**
 * Selector Generator - selector generator
 * Generate multiple candidate selectors for DOM elements
 */

import type {
  NonEmptyArray,
  NormalizedSelectorGenerationOptions,
  SelectorCandidate,
  SelectorGenerationOptions,
  SelectorStrategy,
  SelectorStrategyContext,
  SelectorTarget,
  ExtendedSelectorTarget,
} from './types';
import { compareSelectorCandidates, withStability } from './stability';
import { DEFAULT_SELECTOR_STRATEGIES } from './strategies';
import { computeDomPath } from './dom-path';
import { computeFingerprint } from './fingerprint';

const DEFAULT_MAX_CANDIDATES = 8;
const DEFAULT_TEXT_MAX_LENGTH = 64;

const DEFAULT_TEXT_TAGS = ['button', 'a', 'summary'] as const;

const DEFAULT_TESTID_ATTRS = [
  'data-testid',
  'data-test-id',
  'data-testId',
  'data-test',
  'data-qa',
  'data-cy',
  'name',
  'title',
  'alt',
] as const;

function clampInt(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) return min;
  return Math.min(max, Math.max(min, Math.floor(value)));
}

/**
 * Standardized selector generation options
 */
export function normalizeSelectorGenerationOptions(
  options: SelectorGenerationOptions | undefined,
): NormalizedSelectorGenerationOptions {
  return {
    maxCandidates: clampInt(options?.maxCandidates ?? DEFAULT_MAX_CANDIDATES, 1, 50),
    includeText: options?.includeText ?? true,
    includeAria: options?.includeAria ?? true,
    includeCssUnique: options?.includeCssUnique ?? true,
    includeCssPath: options?.includeCssPath ?? true,
    testIdAttributes: options?.testIdAttributes ?? DEFAULT_TESTID_ATTRS,
    textMaxLength: clampInt(options?.textMaxLength ?? DEFAULT_TEXT_MAX_LENGTH, 1, 256),
    textTags: options?.textTags ?? DEFAULT_TEXT_TAGS,
  };
}

/**
 * CSS string escape
 * Uses native CSS.escape when available; otherwise falls back to a spec-inspired polyfill.
 */
export function cssEscape(value: string): string {
  if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') return CSS.escape(value);

  const str = String(value);
  const len = str.length;
  if (len === 0) return '';

  let result = '';
  const firstCodeUnit = str.charCodeAt(0);

  for (let i = 0; i < len; i++) {
    const codeUnit = str.charCodeAt(i);

    if (codeUnit === 0x0000) {
      result += '\uFFFD';
      continue;
    }

    if (
      (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||
      codeUnit === 0x007f ||
      (i === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
      (i === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && firstCodeUnit === 0x002d)
    ) {
      result += `\\${codeUnit.toString(16)} `;
      continue;
    }

    if (i === 0 && len === 1 && codeUnit === 0x002d) {
      result += `\\${str.charAt(i)}`;
      continue;
    }

    const isAsciiAlnum =
      (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
      (codeUnit >= 0x0041 && codeUnit <= 0x005a) ||
      (codeUnit >= 0x0061 && codeUnit <= 0x007a);

    const isSafe = isAsciiAlnum || codeUnit === 0x002d || codeUnit === 0x005f;

    if (isSafe) result += str.charAt(i);
    else result += `\\${str.charAt(i)}`;
  }

  return result;
}

function getQueryRoot(element: Element): ParentNode {
  const root = element.getRootNode?.();
  if (root instanceof ShadowRoot) return root;
  if (typeof document !== 'undefined') return document;
  throw new Error('Selector generator requires a DOM-like environment');
}

function safeQueryAll(root: ParentNode, selector: string): ReadonlyArray<Element> {
  try {
    return Array.from(root.querySelectorAll(selector));
  } catch {
    return [];
  }
}

function isUnique(root: ParentNode, selector: string): boolean {
  try {
    return root.querySelectorAll(selector).length === 1;
  } catch {
    return false;
  }
}

function candidateKey(c: SelectorCandidate): string {
  switch (c.type) {
    case 'text':
      return `text:${c.value}:${c.tagNameHint ?? ''}:${c.match ?? ''}`;
    case 'aria':
      return `aria:${c.role ?? ''}:${c.name ?? ''}:${c.value}`;
    default:
      return `${c.type}:${c.value}`;
  }
}

export interface GenerateSelectorTargetOptions extends SelectorGenerationOptions {
  root?: ParentNode;
  strategies?: ReadonlyArray<SelectorStrategy>;
}

/**
 * Generate selector targets for DOM elements
 */
export function generateSelectorTarget(
  element: Element,
  options: GenerateSelectorTargetOptions = {},
): SelectorTarget {
  const normalized = normalizeSelectorGenerationOptions(options);
  const root = options.root ?? getQueryRoot(element);

  const helpers = {
    cssEscape,
    isUnique: (selector: string) => isUnique(root, selector),
    safeQueryAll: (selector: string) => safeQueryAll(root, selector),
  };

  const ctx: SelectorStrategyContext = {
    element,
    root,
    options: normalized,
    helpers,
  };

  const strategies = options.strategies ?? DEFAULT_SELECTOR_STRATEGIES;

  const raw: SelectorCandidate[] = [];
  for (const strategy of strategies) {
    const produced = strategy.generate(ctx);
    for (const c0 of produced) {
      raw.push({
        ...c0,
        source: c0.source ?? 'generated',
        strategy: c0.strategy ?? strategy.id,
      });
    }
  }

  // Dedupe (keep first occurrence)
  const seen = new Set<string>();
  const deduped: SelectorCandidate[] = [];
  for (const c of raw) {
    const key = candidateKey(c);
    if (seen.has(key)) continue;
    seen.add(key);
    deduped.push(withStability(c));
  }

  // If strategies produced nothing (shouldn't happen), create a minimal fallback.
  if (deduped.length === 0) {
    const fallback: SelectorCandidate = withStability({
      type: 'css',
      value: 'body',
      source: 'generated',
      strategy: 'fallback',
    });
    const candidates: NonEmptyArray<SelectorCandidate> = [fallback];
    return {
      selector: fallback.value,
      candidates,
      tagName: element.tagName?.toLowerCase?.() ?? undefined,
    };
  }

  // Sort and truncate
  const sorted = [...deduped].sort(compareSelectorCandidates).slice(0, normalized.maxCandidates);

  // Primary selector should be directly usable by locator (prefer CSS/attr)
  const primary = sorted.find((c) => c.type === 'css' || c.type === 'attr') ?? sorted[0];

  const reordered = (() => {
    const idx = sorted.indexOf(primary);
    if (idx <= 0) return sorted;
    return [primary, ...sorted.slice(0, idx), ...sorted.slice(idx + 1)];
  })();

  const tagName = element.tagName?.toLowerCase?.() ?? undefined;

  return {
    selector: primary.value,
    candidates: reordered as NonEmptyArray<SelectorCandidate>,
    tagName,
  };
}

// =============================================================================
// Extended Selector Target (Phase 1.2)
// =============================================================================

function safeMatches(element: Element, selector: string): boolean {
  try {
    return element.matches(selector);
  } catch {
    return false;
  }
}

/**
 * Pick the best selector for a shadow host element.
 * Prefers unique CSS/attr selectors from the generated candidates.
 */
function pickShadowHostSelector(
  host: Element,
  hostRoot: ParentNode,
  options: GenerateSelectorTargetOptions,
): string | null {
  const hostTarget = generateSelectorTarget(host, { ...options, root: hostRoot });

  let fallback: string | null = null;

  // Try to find a unique selector from candidates
  for (const candidate of hostTarget.candidates) {
    if (candidate.type !== 'css' && candidate.type !== 'attr') continue;

    const selector = String(candidate.value || '').trim();
    if (!selector) continue;

    // Verify the selector actually matches the host
    if (!safeMatches(host, selector)) continue;

    // Check uniqueness in the host's root
    if (isUnique(hostRoot, selector)) {
      return selector;
    }

    // Keep first matching selector as fallback
    if (!fallback) {
      fallback = selector;
    }
  }

  // Try the primary selector
  const primary = typeof hostTarget.selector === 'string' ? hostTarget.selector.trim() : '';
  if (primary && safeMatches(host, primary)) {
    return primary;
  }

  return fallback;
}

/**
 * Compute shadow host selector chain (outer -> inner).
 *
 * Returns an empty array when:
 * - Element is not inside Shadow DOM
 * - A host selector cannot be generated for any boundary
 */
function computeShadowHostChain(
  element: Element,
  options: GenerateSelectorTargetOptions,
): string[] {
  const chain: string[] = [];
  let current: Element = element;

  while (true) {
    const rootNode = current.getRootNode?.();
    if (!(rootNode instanceof ShadowRoot)) {
      break;
    }

    const host = rootNode.host;
    if (!(host instanceof Element)) {
      break;
    }

    const hostRoot = getQueryRoot(host);
    const hostSelector = pickShadowHostSelector(host, hostRoot, options);

    if (!hostSelector) {
      // Cannot generate selector for this host, return empty chain
      return [];
    }

    chain.unshift(hostSelector);
    current = host;
  }

  return chain;
}

/**
 * Generate selector target with additional metadata (Phase 1.2).
 *
 * This function generates a complete ElementLocator-like structure including:
 * - fingerprint: for fuzzy element matching
 * - domPath: for fast element recovery
 * - shadowHostChain: for Shadow DOM traversal
 *
 * @example
 * ```ts
 * const target = generateExtendedSelectorTarget(buttonElement);
 * // target.fingerprint = "button|id=submit|class=btn.primary"
 * // target.domPath = [0, 2, 1]
 * // target.shadowHostChain = ["my-component"] or []
 * ```
 */
export function generateExtendedSelectorTarget(
  element: Element,
  options: GenerateSelectorTargetOptions = {},
): ExtendedSelectorTarget {
  const base = generateSelectorTarget(element, options);

  return {
    ...base,
    fingerprint: computeFingerprint(element),
    domPath: computeDomPath(element),
    shadowHostChain: computeShadowHostChain(element, options),
  };
}
