import type { Page } from 'playwright';
import type { LoggerPort } from 'apps/companion/main/automation/application/ports/LoggerPort';
import { IRACING_SELECTORS, BLOCKED_KEYWORDS } from './IRacingSelectors';
import type { PlaywrightConfig } from '../core/PlaywrightAutomationAdapter';
import { PlaywrightBrowserSession } from '../core/PlaywrightBrowserSession';

export class SafeClickService {
  constructor(
    private readonly config: Required<PlaywrightConfig>,
    private readonly browserSession: PlaywrightBrowserSession,
    private readonly logger?: LoggerPort,
  ) {}

  private log(level: 'debug' | 'info' | 'warn' | 'error', message: string, context?: Record<string, unknown>): void {
    if (!this.logger) {
      return;
    }
    const logger = this.logger as Record<
      'debug' | 'info' | 'warn' | 'error',
      (msg: string, ctx?: Record<string, unknown>) => void
    >;
    logger[level](message, context);
  }

  private isRealMode(): boolean {
    return this.config.mode === 'real';
  }

  private getPage(): Page {
    const page = this.browserSession.getPage();
    if (!page) {
      throw new Error('Browser not connected');
    }
    return page;
  }

  /**
   * Check if a selector or element text matches blocked patterns (checkout/payment buttons).
   * SAFETY CRITICAL: This prevents accidental purchases during automation.
   *
   * @param selector The CSS selector being clicked
   * @param elementText Optional text content of the element (should be direct text only)
   * @returns true if the selector/text matches a blocked pattern
   */
  private isBlockedSelector(_selector: string, elementText?: string): boolean {
    const selectorLower = selector.toLowerCase();
    const textLower = elementText?.toLowerCase().trim() ?? '';

    // Check if selector contains any blocked keywords
    for (const keyword of BLOCKED_KEYWORDS) {
      if (selectorLower.includes(keyword) || textLower.includes(keyword)) {
        return true;
      }
    }

    // Check for price indicators (e.g., "$0.50", "$19.99")
    // IMPORTANT: Only block if the price is combined with a checkout-related action word
    // This prevents false positives when price is merely displayed on the page
    const pricePattern = /\$\d+\.\d{2}/;
    const hasPrice = pricePattern.test(textLower) || pricePattern.test(selector);
    if (hasPrice) {
      // Only block if text also contains checkout-related words
      const checkoutActionWords = ['check', 'out', 'buy', 'purchase', 'pay', 'cart'];
      const hasCheckoutWord = checkoutActionWords.some(word => textLower.includes(word));
      if (hasCheckoutWord) {
        return true;
      }
    }

    // Check for cart icon class
    if (selectorLower.includes('icon-cart') || selectorLower.includes('cart-icon')) {
      return true;
    }

    return false;
  }

  /**
   * Verify an element is not a blocked checkout/payment button before clicking.
   * SAFETY CRITICAL: Throws error if element matches blocked patterns.
   *
   * This method checks:
   * 1. The selector string itself for blocked patterns
   * 2. The element's DIRECT text content (not children/siblings)
   * 3. The element's class, id, and href attributes for checkout indicators
   * 4. Whether the element matches any blocked CSS selectors
   *
   * @param selector The CSS selector of the element to verify
   * @throws Error if element is a blocked checkout/payment button
   */
  async verifyNotBlockedElement(_selector: string): Promise<void> {
    const page = this.browserSession.getPage();
    if (!page) return;

    // In mock mode we bypass safety blocking to allow tests to exercise checkout flows
    // without risking real-world purchases. Safety checks remain active in 'real' mode.
    if (!this.isRealMode()) {
      this.log('debug', 'Mock mode detected - skipping checkout blocking checks', { selector });
      return;
    }

    // First check the selector itself
    if (this.isBlockedSelector(selector)) {
      const errorMsg = `🚫 BLOCKED: Selector "${selector}" matches checkout/payment pattern. Automation stopped for safety.`;
      this.log('error', errorMsg);
      throw new Error(errorMsg);
    }

    // Try to get the element's attributes and direct text for verification
    try {
      const element = page.locator(selector).first();
      const isVisible = await element.isVisible().catch(() => false);

      if (isVisible) {
        // Get element attributes for checking
        const elementClass = (await element.getAttribute('class').catch(() => '')) ?? '';
        const elementId = (await element.getAttribute('id').catch(() => '')) ?? '';
        const elementHref = (await element.getAttribute('href').catch(() => '')) ?? '';

        // Check class/id/href for checkout indicators
        const attributeText = `${elementClass} ${elementId} ${elementHref}`.toLowerCase();
        if (
          attributeText.includes('checkout') ||
          attributeText.includes('cart') ||
          attributeText.includes('purchase') ||
          attributeText.includes('payment')
        ) {
          const errorMsg = `🚫 BLOCKED: Element attributes contain checkout pattern. Class="${elementClass}", ID="${elementId}", Href="${elementHref}". Automation stopped for safety.`;
          this.log('error', errorMsg);
          throw new Error(errorMsg);
        }

        // Get ONLY the direct text of this element, excluding child element text
        // This prevents false positives when a checkout button exists elsewhere on the page
        const directText = await element
          .evaluate((el) => {
            let text = '';
            const childNodes = Array.from(el.childNodes);
            for (const child of childNodes) {
              if (child.nodeType === Node.TEXT_NODE) {
                text += child.textContent || '';
              }
            }
            return text.trim();
          })
          .catch(() => '');

        // Also get innerText as fallback (for buttons with icon + text structure)
        // But only check if directText is empty or very short
        let textToCheck = directText;
        if (directText.length < 3) {
          const innerText = await element.innerText().catch(() => '');
          if (innerText.length < 100) {
            textToCheck = innerText.trim();
          }
        }

        this.log('debug', 'Checking element text for blocked patterns', {
          selector,
          directText,
          textToCheck,
          elementClass,
        });

        if (textToCheck && this.isBlockedSelector('', textToCheck)) {
          const errorMsg = `🚫 BLOCKED: Element text "${textToCheck}" matches checkout/payment pattern. Automation stopped for safety.`;
          this.log('error', errorMsg);
          throw new Error(errorMsg);
        }

        // Check if element matches any of the blocked selectors directly
        for (const blockedSelector of Object.values(IRACING_SELECTORS.BLOCKED_SELECTORS)) {
          const matchesBlocked = await element
            .evaluate((el, sel) => {
              try {
                return el.matches(sel) || el.closest(sel) !== null;
              } catch {
                return false;
              }
            }, blockedSelector)
            .catch(() => false);

          if (matchesBlocked) {
            const errorMsg = `🚫 BLOCKED: Element matches blocked selector "${blockedSelector}". Automation stopped for safety.`;
            this.log('error', errorMsg);
            throw new Error(errorMsg);
          }
        }
      }
    } catch (error) {
      if (error instanceof Error && error.message.includes('BLOCKED')) {
        throw error;
      }
      this.log('debug', 'Could not verify element (may not exist yet)', { _selector, error: String(error) });
    }
  }

  /**
   * Dismiss any visible Chakra UI modal popups that might block interactions.
   * This handles various modal dismiss patterns including close buttons and overlay clicks.
   * Optimized for speed - uses instant visibility checks and minimal waits.
   */
  async dismissModals(): Promise<void> {
    const page = this.browserSession.getPage();
    if (!page) return;

    try {
      const modalContainer = page.locator('.chakra-modal__content-container, .modal-content');
      const isModalVisible = await modalContainer.isVisible().catch(() => false);

      if (!isModalVisible) {
        this.log('debug', 'No modal visible, continuing');
        return;
      }

      this.log('info', 'Modal detected, dismissing immediately');

      const dismissButton = page
        .locator(
          '.chakra-modal__content-container button[aria-label="Continue"], ' +
            '.chakra-modal__content-container button:has-text("Continue"), ' +
            '.chakra-modal__content-container button:has-text("Close"), ' +
            '.chakra-modal__content-container button:has-text("OK"), ' +
            '.chakra-modal__close-btn, ' +
            '[aria-label="Close"]',
        )
        .first();

      if (await dismissButton.isVisible().catch(() => false)) {
        this.log('info', 'Clicking modal dismiss button');
        await dismissButton.click({ force: true, timeout: 1000 });
        await page.waitForTimeout(100);
        return;
      }

      this.log('debug', 'No dismiss button found, skipping Escape to avoid closing wizard');
      await page.waitForTimeout(100);
    } catch (error) {
      this.log('debug', 'Modal dismiss error (non-critical)', { error: String(error) });
    }
  }

  /**
   * Dismiss any open React DateTime pickers (rdt component).
   * These pickers can intercept pointer events and block clicks on other elements.
   * Used specifically before navigating away from steps that have datetime pickers.
   *
   * IMPORTANT: Do NOT use Escape key as it closes the entire wizard modal in iRacing.
   */
  async dismissDatetimePickers(): Promise<void> {
    const page = this.browserSession.getPage();
    if (!page) return;

    try {
      const initialCount = await page.locator('.rdt.rdtOpen').count();

      if (initialCount === 0) {
        this.log('debug', 'No datetime picker open');
        return;
      }

      this.log('info', `Closing ${initialCount} open datetime picker(s)`);

      // Strategy 1: remove rdtOpen class via JS
      await page.evaluate(() => {
        const openPickers = document.querySelectorAll('.rdt.rdtOpen');
        openPickers.forEach((picker) => {
          picker.classList.remove('rdtOpen');
        });
        const activeEl = document.activeElement as HTMLElement;
        if (activeEl && activeEl.blur && activeEl.closest('.rdt')) {
          activeEl.blur();
        }
      });
      await page.waitForTimeout(50);

      let stillOpenCount = await page.locator('.rdt.rdtOpen').count();
      if (stillOpenCount === 0) {
        this.log('debug', 'Datetime pickers closed via JavaScript');
        return;
      }

      // Strategy 2: click outside
      this.log('debug', `${stillOpenCount} picker(s) still open, clicking outside`);
      const modalBody = page.locator(IRACING_SELECTORS.wizard.modalContent).first();
      if (await modalBody.isVisible().catch(() => false)) {
        const cardHeader = page.locator(`${IRACING_SELECTORS.wizard.stepContainers.timeOfDay} .card-header`).first();
        if (await cardHeader.isVisible().catch(() => false)) {
          await cardHeader.click({ force: true, timeout: 1000 }).catch(() => {});
          await page.waitForTimeout(100);
        }
      }

      stillOpenCount = await page.locator('.rdt.rdtOpen').count();
      if (stillOpenCount === 0) {
        this.log('debug', 'Datetime pickers closed via click outside');
        return;
      }

      // Strategy 3: blur inputs and force-remove rdtOpen
      this.log('debug', `${stillOpenCount} picker(s) still open, force blur`);
      await page.evaluate(() => {
        const rdtInputs = document.querySelectorAll('.rdt input');
        rdtInputs.forEach((input) => {
          (input as HTMLElement).blur();
        });
        const openPickers = document.querySelectorAll('.rdt.rdtOpen');
        openPickers.forEach((picker) => {
          picker.classList.remove('rdtOpen');
          const pickerDropdown = picker.querySelector('.rdtPicker') as HTMLElement;
          if (pickerDropdown) {
            pickerDropdown.style.display = 'none';
          }
        });
      });
      await page.waitForTimeout(50);

      const finalCount = await page.locator('.rdt.rdtOpen').count();
      if (finalCount > 0) {
        this.log('warn', `Could not close ${finalCount} datetime picker(s), will attempt click with force`);
      } else {
        this.log('debug', 'Datetime picker dismiss complete');
      }
    } catch (error) {
      this.log('debug', 'Datetime picker dismiss error (non-critical)', { error: String(error) });
    }
  }

  /**
   * Safe click wrapper that handles modal interception errors with auto-retry.
   * If a click fails because a modal is intercepting pointer events, this method
   * will dismiss the modal and retry the click operation.
   *
   * SAFETY: Before any click, verifies the target is not a checkout/payment button.
   *
   * @param selector The CSS selector of the element to click
   * @param options Click options including timeout and force
   * @returns Promise that resolves when click succeeds or throws after max retries
   */
  async safeClick(
    selector: string,
    options?: { timeout?: number; force?: boolean },
  ): Promise<void> {
    const page = this.getPage();

    // In mock mode, ensure mock fixtures are visible (remove 'hidden' flags)
    if (!this.isRealMode()) {
      try {
        await page.evaluate(() => {
          document
            .querySelectorAll<HTMLElement>('.wizard-step.hidden, .modal.hidden, .wizard-step[hidden]')
            .forEach((el) => {
              el.classList.remove('hidden');
              el.removeAttribute('hidden');
            });
        });
      } catch {
        // ignore any evaluation errors in test environments
      }
    }

    // SAFETY CHECK: Verify this is not a checkout/payment button
    await this.verifyNotBlockedElement(selector);

    const maxRetries = 3;
    const timeout = options?.timeout ?? this.config.timeout;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        const useForce = options?.force || attempt === maxRetries;
        await page.click(_selector, { timeout, force: useForce });
        return;
      } catch (error) {
        if (error instanceof Error && error.message.includes('BLOCKED')) {
          throw error;
        }

        const errorMessage = String(error);

        if (
          errorMessage.includes('intercepts pointer events') ||
          errorMessage.includes('chakra-modal') ||
          errorMessage.includes('chakra-portal') ||
          errorMessage.includes('rdtDay') ||
          errorMessage.includes('rdtPicker') ||
          errorMessage.includes('rdt')
        ) {
          this.log('info', `Element intercepting click (attempt ${attempt}/${maxRetries}), dismissing...`, {
            selector,
            attempt,
            maxRetries,
          });

          await this.dismissDatetimePickers();
          await this.dismissModals();
          await page.waitForTimeout(200);

          if (attempt === maxRetries) {
            this.log('warn', 'Max retries reached, attempting JS click fallback', { selector });

            try {
              const clicked = await page.evaluate((sel) => {
                try {
                  const el = document.querySelector(sel) as HTMLElement | null;
                  if (!el) return false;
                  el.scrollIntoView({ block: 'center', inline: 'center' });
                  el.click();
                  return true;
                } catch {
                  return false;
                }
              }, selector);

              if (clicked) {
                this.log('info', 'JS fallback click succeeded', { selector });
                return;
              } else {
                this.log('debug', 'JS fallback click did not find element or failed', { selector });
              }
            } catch (e) {
              this.log('debug', 'JS fallback click error', { _selector, error: String(e) });
            }

            this.log('error', 'Max retries reached, click still blocked', { selector });
            throw error;
          }
        } else {
          throw error;
        }
      }
    }
  }
}