import { Page, expect } from '@playwright/test';
import { BasePage } from './BasePage';
import { Ware } from '../types';

/**
 * Bargaining Panel Page Object
 * Handles all interactions for the bargaining/shop feature.
 *
 * Real DOM structure (MapPage + MapCharacterPanel):
 *   .map-character-panel                   — outer wrapper (visible after "Start Bargain")
 *     motion.aside (renders as <aside>)    — inner component
 *       [data-testid="dialogue-panel"]     — scrollable chat messages area
 *         div.text-left                   — each NPC/seller message bubble
 *         div.text-right                  — each user message bubble
 *       textarea                          — message draft input
 *       button "Send"                     — send button
 *
 * Catalog message (textContent, markdown rendered as plain text):
 *   "Available wares to bargain for:
 *    Item Name — ask NNN Gold (id: #ID)
 *    ..."
 */
export class BargainingPanel extends BasePage {
  // Outer wrapper div for the MapCharacterPanel (only present when seller panel is open)
  private readonly panelWrapper = () => this.page.locator('.map-character-panel').first();

  // Scrollable chat messages area
  private readonly dialoguePanel = () => this.page.locator('[data-testid="dialogue-panel"]').first();

  // Seller/NPC messages — rendered with text-left alignment
  private readonly sellerMessages = () => this.dialoguePanel().locator('> .text-left');

  // Message input (textarea — no data-testid in real component)
  private readonly messageInput = () => this.panelWrapper().locator('textarea').first();

  // Send button (text "Send" — no data-testid in real component)
  private readonly sendButton = () =>
    this.panelWrapper().getByRole('button', { name: /^Send$/i }).first();

  constructor(page: Page, baseUrl?: string) {
    super(page, baseUrl);
  }

  /**
   * Check if bargaining panel is visible
   */
  async isVisible(): Promise<boolean> {
    return this.panelWrapper().isVisible({ timeout: 3000 }).catch(() => false);
  }

  /**
   * Wait for the dialogue panel to become visible (after "Start Bargain" is clicked)
   */
  async waitForPanel(timeout = 10000): Promise<void> {
    await this.dialoguePanel().waitFor({ state: 'visible', timeout });
  }

  /**
   * Check if catalog is displayed as the first seller message.
   * The catalog contains item names with "(id: #N)" entries.
   */
  async isCatalogFirstMessage(): Promise<boolean> {
    try {
      await this.waitForPanel(10000);
      await this.pollUntil(
        async () => (await this.sellerMessages().count()) > 0,
        10000,
        500,
      );
      const text = await this.sellerMessages().first().textContent();
      return !!(text?.match(/available wares|wares to bargain|id:\s*#\d+/i));
    } catch {
      return false;
    }
  }

  /**
   * Get catalog wares by parsing the catalog assistant message text.
   * Catalog line format (after markdown rendering):
   *   "Item Name — ask NNN Gold (id: #ID)"
   */
  async getCatalogWares(): Promise<Ware[]> {
    try {
      await this.waitForPanel(10000);
      await this.pollUntil(
        async () => {
          const count = await this.sellerMessages().count();
          if (count === 0) return false;
          for (let i = 0; i < count; i++) {
            const t = await this.sellerMessages().nth(i).textContent();
            if (t?.match(/id:\s*#\d+/i)) return true;
          }
          return false;
        },
        10000,
        500,
      );
    } catch {
      return [];
    }

    const count = await this.sellerMessages().count();
    for (let i = 0; i < count; i++) {
      const textRaw = await this.sellerMessages().nth(i).textContent();
      if (textRaw?.match(/id:\s*#\d+/i)) {
        // Use li elements for parsing since textContent collapses newlines
        const liElements = this.sellerMessages().nth(i).locator('li');
        const liCount = await liElements.count();
        if (liCount > 0) {
          const wares: Ware[] = [];
          for (let j = 0; j < liCount; j++) {
            const liText = await liElements.nth(j).textContent();
            if (liText) {
              const parsed = this.parseWaresFromText(liText);
              wares.push(...parsed);
            }
          }
          if (wares.length > 0) return wares;
        }
        // Fallback: use innerText which preserves newlines from block elements
        const innerText = await this.sellerMessages().nth(i).evaluate(
          (el: Element) => (el as HTMLElement).innerText || el.textContent || ''
        );
        return this.parseWaresFromText(innerText);
      }
    }
    return [];
  }

  /**
   * Parse wares from catalog message textContent.
   * Expected line: "Name — ask NNN Gold (id: #ID)"
   * Note: markdown bold (**) is stripped by the renderer, so plain text is used.
   */
  private parseWaresFromText(text: string): Ware[] {
    const wares: Ware[] = [];
    for (const line of text.split('\n')) {
      const match = line.match(/^\s*([^—–\n]+?)\s*[—–]\s*ask\s*([\d,]+)\s*Gold\s*\(id:\s*#(\d+)\)/i);
      if (match) {
        const name = match[1].trim();
        const price = parseInt(match[2].replace(/,/g, ''), 10);
        const id = match[3];
        if (name && id) wares.push({ id, name, description: '', price });
      }
    }
    return wares;
  }

  /**
   * Select a ware by sending its name in the bargaining chat
   */
  async selectWareByName(wareName: string): Promise<void> {
    await this.sendMessage(`I want the ${wareName}`);
  }

  /**
   * Select a ware by ID (sends "#ID" in chat)
   */
  async selectWareById(wareId: string): Promise<void> {
    await this.sendMessage(`#${wareId}`);
  }

  /**
   * Select a ware by partial name (fuzzy — types partial name in chat)
   */
  async selectWareByPartialName(partialName: string): Promise<void> {
    await this.sendMessage(`I want ${partialName}`);
  }

  /**
   * Send a message and wait for the seller to respond (new message appears).
   * Uses smart polling instead of a fixed wait — faster when the API is quick.
   */
  async sendMessage(message: string): Promise<void> {
    const countBefore = await this.sellerMessages().count();
    await this.messageInput().fill(message);
    await this.sendButton().click();
    // Wait for a new seller message to appear (up to 15 s)
    await this.pollUntil(
      async () => (await this.sellerMessages().count()) > countBefore,
      15000,
      300,
    ).catch(() => {
      // Tolerate timeout — test assertions will catch real failures
    });
  }

  /**
   * Make a price offer
   */
  async makeOffer(amount: number): Promise<void> {
    await this.sendMessage(`${amount}`);
  }

  /**
   * Accept the deal by typing "deal"
   */
  async acceptDeal(): Promise<void> {
    await this.sendMessage('deal');
  }

  /**
   * Get the text of the last seller message
   */
  async getLastSellerMessage(): Promise<string> {
    try {
      await this.pollUntil(
        async () => (await this.sellerMessages().count()) > 0,
        10000,
        500,
      );
    } catch {
      return '';
    }
    const count = await this.sellerMessages().count();
    if (count === 0) return '';
    const text = await this.sellerMessages().nth(count - 1).textContent();
    return text?.trim() || '';
  }

  /**
   * Check if seller is asking for clarification (ambiguous selection)
   */
  async isAskingForClarification(): Promise<boolean> {
    const msg = await this.getLastSellerMessage();
    return /which|clarif|confirm|did you mean/i.test(msg);
  }

  /**
   * Get gold balance from the nav GoldCounter ("Gold: NNN")
   */
  async getGoldBalance(): Promise<number> {
    try {
      // GoldCounter renders: <div ...>Gold: {gold}</div>
      const el = this.page.locator('text=/Gold: \\d+/').first();
      const text = await el.textContent({ timeout: 5000 });
      const match = text?.match(/(\d+)/);
      return parseInt(match?.[1] || '0', 10);
    } catch {
      return 0;
    }
  }

  /**
   * Check if stats section is visible on the inventory page.
   * Stats grid uses data-tour="inventory-stats".
   * Uses waitFor because InventoryPage has a loading state before stats render.
   */
  async isStatsVisible(): Promise<boolean> {
    try {
      await this.page
        .locator('[data-tour="inventory-stats"]')
        .waitFor({ state: 'visible', timeout: 15000 });
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Get best bargain percentage from inventory stats grid
   */
  async getBestBargainPercent(): Promise<number> {
    const panel = this.page.locator('[data-tour="inventory-stats"]');
    const text = await panel.textContent();
    const matches = [...(text?.matchAll(/([\d.]+)%/g) || [])];
    return parseFloat(matches[0]?.[1] || '0');
  }

  /**
   * Get average savings percentage from inventory stats grid
   */
  async getAverageSavingsPercent(): Promise<number> {
    const panel = this.page.locator('[data-tour="inventory-stats"]');
    const text = await panel.textContent();
    const matches = [...(text?.matchAll(/([\d.]+)%/g) || [])];
    return parseFloat(matches[matches.length - 1]?.[1] || '0');
  }

  /**
   * Navigate to inventory page using SPA navigation (click nav link).
   * A full page.goto('/inventory') resets React auth state causing a redirect to /login.
   * Clicking the nav link preserves the currentUser state from login.
   */
  async goToInventory(): Promise<void> {
    // Try SPA-safe navigation via nav link first
    const navLink = this.page.getByRole('link', { name: /^Inventory$/i }).first();
    const isNavVisible = await navLink.isVisible({ timeout: 2000 }).catch(() => false);

    if (isNavVisible) {
      await navLink.click();
      await this.page.waitForURL('**/inventory', { timeout: 10000 });
    } else {
      // Fallback: full page load — wait for React auth check to complete before checking content
      await this.page.goto(`${this.baseUrl}/inventory`);
      // Give the SPA time to check auth (/api/auth/me) and re-render the protected route
      await this.page.waitForTimeout(3000);
      // If auth succeeded, we may be on /inventory; if not re-navigate once auth check is done
      if (!this.page.url().includes('/inventory')) {
        const inventoryLink = this.page.getByRole('link', { name: /^Inventory$/i }).first();
        const visible = await inventoryLink.isVisible({ timeout: 3000 }).catch(() => false);
        if (visible) {
          await inventoryLink.click();
          await this.page.waitForURL('**/inventory', { timeout: 10000 });
        }
      }
    }
    await this.page.waitForTimeout(500);
  }

  /**
   * Get inventory items from the table rows.
   * InventoryPage renders items in <table><tbody><tr> rows (no data-testid).
   */
  async getInventoryItems(): Promise<string[]> {
    await this.page
      .locator('table tbody tr')
      .first()
      .waitFor({ state: 'visible', timeout: 10000 })
      .catch(() => null);
    const rows = this.page.locator('table tbody tr');
    const count = await rows.count();
    const results: string[] = [];
    for (let i = 0; i < count; i++) {
      const text = await rows.nth(i).textContent();
      if (text) results.push(text.trim());
    }
    return results;
  }

  /**
   * Expect a purchase confirmation in the last seller message
   */
  async expectPurchaseConfirmation(): Promise<void> {
    await this.page.waitForTimeout(2000);
    const msg = await this.getLastSellerMessage();
    expect(msg).toMatch(/purchase|success|bought|congratulations|thank|sold|deal/i);
  }

  /**
   * Check if panel has a generic NPC opener message
   */
  async hasGenericOpener(): Promise<boolean> {
    try {
      const messages = await this.getMessages();
      return messages.some(m => /^hello|^hi|^greetings|welcome/i.test(m.trim()));
    } catch {
      return false;
    }
  }

  /**
   * Get all messages from the bargaining panel
   */
  async getMessages(): Promise<string[]> {
    try {
      await this.waitForPanel(10000);
      const allMessages = this.dialoguePanel().locator('> div');
      const count = await allMessages.count();
      const messages: string[] = [];
      for (let i = 0; i < count; i++) {
        const text = await allMessages.nth(i).textContent();
        if (text) messages.push(text.trim());
      }
      return messages;
    } catch {
      return [];
    }
  }

  /**
   * Check if the bargaining panel is loaded and ready for interaction
   */
  async isLoaded(): Promise<boolean> {
    try {
      await this.waitForPanel(10000);
      const inputVisible = await this.messageInput().isVisible({ timeout: 3000 });
      const buttonVisible = await this.sendButton().isVisible({ timeout: 3000 });
      return inputVisible && buttonVisible;
    } catch {
      return false;
    }
  }
}
