import type { Page, BrowserContext } from '@playwright/test';
import { writeFileSync, mkdirSync, readFileSync, existsSync } from 'fs';
import { join } from 'path';

// Global snapshot state for chain resets - persisted to disk
const SNAPSHOT_FILE = join(process.cwd(), 'tmp', '.chain-snapshot-id');
let initialSnapshotId: string | null = null;
let currentSnapshotId: string | null = null;

// Load snapshot ID from disk if it exists
function loadSnapshotId(): string | null {
  try {
    if (existsSync(SNAPSHOT_FILE)) {
      return readFileSync(SNAPSHOT_FILE, 'utf-8').trim();
    }
  } catch (e) {
    console.warn(`[CHAIN] Could not read snapshot file: ${e}`);
  }
  return null;
}

// Save snapshot ID to disk
function saveSnapshotId(id: string): void {
  try {
    mkdirSync(join(process.cwd(), 'tmp'), { recursive: true });
    writeFileSync(SNAPSHOT_FILE, id, 'utf-8');
  } catch (e) {
    console.warn(`[CHAIN] Could not write snapshot file: ${e}`);
  }
}

/**
 * Reset chain state using evm_snapshot/evm_revert
 * On first call: takes the initial snapshot (clean state) and saves to disk
 * On subsequent calls: reverts to initial snapshot, then takes a new snapshot
 * This preserves deployed contracts but resets balances and pool state to initial conditions
 * 
 * Snapshot ID is persisted to disk so it survives module reloads between tests
 */
export async function resetChainState(rpcUrl: string): Promise<void> {
  // Try to load from disk first (in case module was reloaded)
  if (!initialSnapshotId) {
    initialSnapshotId = loadSnapshotId();
    if (initialSnapshotId) {
      console.log(`[CHAIN] Loaded initial snapshot from disk: ${initialSnapshotId}`);
    }
  }

  if (initialSnapshotId) {
    // Revert to the initial snapshot
    console.log(`[CHAIN] Reverting to initial snapshot ${initialSnapshotId}...`);
    const revertRes = await fetch(rpcUrl, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({
        jsonrpc: '2.0',
        method: 'evm_revert',
        params: [initialSnapshotId],
        id: 1
      })
    });
    const revertData = await revertRes.json();
    if (!revertData.result) {
      // Revert failed - clear snapshot file and take a fresh one
      console.error(`[CHAIN] Revert FAILED: ${JSON.stringify(revertData)}`);
      console.log(`[CHAIN] Clearing snapshot file and taking fresh snapshot...`);
      initialSnapshotId = null;
      // Fall through to take fresh snapshot below
    } else {
      console.log(`[CHAIN] Reverted successfully to initial state`);
      
      // After successful revert, take a new snapshot (anvil consumes the old one)
      console.log('[CHAIN] Taking new snapshot after successful revert...');
      const newSnapshotRes = await fetch(rpcUrl, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({
          jsonrpc: '2.0',
          method: 'evm_snapshot',
          params: [],
          id: 1
        })
      });
      const newSnapshotData = await newSnapshotRes.json();
      currentSnapshotId = newSnapshotData.result;
      
      // CRITICAL: Update initialSnapshotId because anvil consumed it during revert
      initialSnapshotId = currentSnapshotId;
      saveSnapshotId(initialSnapshotId);
      console.log(`[CHAIN] New initial snapshot taken (replaces consumed one): ${initialSnapshotId}`);
      return;
    }
  }
  
  // First call OR revert failed: take initial snapshot of CURRENT state
  console.log('[CHAIN] Taking FIRST initial snapshot...');
  const snapshotRes = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'evm_snapshot',
      params: [],
      id: 1
    })
  });
  const snapshotData = await snapshotRes.json();
  initialSnapshotId = snapshotData.result;
  currentSnapshotId = initialSnapshotId;
  saveSnapshotId(initialSnapshotId);
  console.log(`[CHAIN] Initial snapshot taken and saved to disk: ${initialSnapshotId}`);
}

export interface TestReport {
  personaName: string;
  testDate: string;
  pagesVisited: Array<{
    page: string;
    url: string;
    timeSpent: number; // milliseconds
    timestamp: string;
  }>;
  actionsAttempted: Array<{
    action: string;
    success: boolean;
    error?: string;
    timestamp: string;
  }>;
  screenshots: string[];
  uiObservations: string[];
  copyFeedback: string[];
  tokenomicsQuestions: string[];
  overallSentiment: string;
}

/**
 * Connect wallet using the injected test provider
 */
export async function connectWallet(page: Page): Promise<void> {
  console.log('[HELPER] Connecting wallet...');

  // Wait for Vue app to mount (increased timeout for post-chain-reset scenarios)
  const navbarTitle = page.locator('.navbar-title').first();
  await navbarTitle.waitFor({ state: 'visible', timeout: 60_000 });

  // Trigger resize event for mobile detection; connectButton.isVisible below waits for layout
  await page.evaluate(() => {
    window.dispatchEvent(new Event('resize'));
  });

  // Wait for wagmi to settle — the connect button or connected display must appear.
  // After the wallet-provider fix, eth_accounts returns [] when not connected,
  // so wagmi should land on 'disconnected' status and render the connect button.
  const connectButton = page.locator('.connect-button--disconnected').first();
  const connectedButton = page.locator('.connect-button--connected').first();

  // Wait for either the disconnect button (normal case) or connected button (auto-reconnect)
  const desktopButton = page.locator('.connect-button--disconnected, .connect-button--connected').first();

  if (await desktopButton.isVisible({ timeout: 10_000 })) {
    if (await connectedButton.isVisible({ timeout: 500 }).catch(() => false)) {
      // Wallet already connected (e.g. wagmi reconnected from storage) — skip connect flow
      console.log('[HELPER] Wallet already connected (auto-reconnect)');
    } else {
      // Desktop connect button found — click to open connector panel
      console.log('[HELPER] Found desktop Connect button');
      await connectButton.click();
      // Wait for the connector panel to open — .connectors-element appearing is the observable event
      const injectedConnector = page.locator('.connectors-element').first();
      await injectedConnector.waitFor({ state: 'visible', timeout: 10_000 });
      console.log('[HELPER] Clicking wallet connector...');
      await injectedConnector.click();
    }
  } else {
    // Try mobile fallback
    const mobileLoginIcon = page.locator('.navbar-end svg').first();
    if (await mobileLoginIcon.isVisible({ timeout: 2_000 })) {
      console.log('[HELPER] Using mobile login icon');
      await mobileLoginIcon.click();
      const injectedConnector = page.locator('.connectors-element').first();
      await injectedConnector.waitFor({ state: 'visible', timeout: 10_000 });
      await injectedConnector.click();
    }
  }

  // Verify wallet is connected
  const walletDisplay = page.getByText(/0x[a-fA-F0-9]{4}/i).first();
  await walletDisplay.waitFor({ state: 'visible', timeout: 15_000 });
  console.log('[HELPER] Wallet connected successfully');
}

/**
 * Mint ETH on the local Anvil fork (via RPC, not UI)
 * This is a direct RPC call to anvil_setBalance
 */
export async function mintEth(
  page: Page,
  rpcUrl: string,
  recipientAddress: string,
  amount: string = '10'
): Promise<void> {
  console.log(`[HELPER] Minting ${amount} ETH to ${recipientAddress} via RPC...`);
  
  const amountWei = BigInt(parseFloat(amount) * 1e18).toString(16);
  const paddedAmount = '0x' + amountWei.padStart(64, '0');
  
  const response = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      jsonrpc: '2.0',
      method: 'anvil_setBalance',
      params: [recipientAddress, paddedAmount],
      id: 1
    })
  });
  
  const result = await response.json();
  if (result.error) {
    throw new Error(`Failed to mint ETH: ${result.error.message}`);
  }
  
  console.log(`[HELPER] ETH minted successfully via RPC`);
}

// Helper: send RPC call and return result
async function sendRpc(rpcUrl: string, method: string, params: unknown[]): Promise<string> {
  const resp = await fetch(rpcUrl, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params })
  });
  const data = await resp.json();
  if (data.error) throw new Error(`RPC ${method} failed: ${data.error.message}`);
  return data.result;
}

// Helper: wait for transaction receipt
async function waitForReceipt(rpcUrl: string, txHash: string, timeoutMs = 15000): Promise<any> {
  const start = Date.now();
  while (Date.now() - start < timeoutMs) {
    const resp = await fetch(rpcUrl, {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getTransactionReceipt', params: [txHash] })
    });
    const data = await resp.json();
    if (data.result) return data.result;
    // eslint-disable-next-line no-restricted-syntax -- Polling with timeout: no event source for transaction receipt over HTTP RPC (eth_subscribe not available). See AGENTS.md #Engineering Principles.
    await new Promise(r => setTimeout(r, 500));
  }
  throw new Error(`Transaction ${txHash} not mined within ${timeoutMs}ms`);
}

/**
 * Fund a wallet with KRK tokens by transferring from the deployer (Anvil #0).
 * On the local fork, the deployer holds the initial KRK supply.
 * The ethAmount parameter is kept for API compatibility but controls KRK amount
 * (1 ETH ≈ 1000 KRK at the ~0.01 initialization price).
 */
export async function buyKrk(
  page: Page,
  ethAmount: string,
  rpcUrl: string = 'http://localhost:8545',
  privateKey?: string
): Promise<void> {
  const deployments = JSON.parse(readFileSync(join(process.cwd(), 'onchain', 'deployments-local.json'), 'utf-8'));
  const krkAddress = deployments.contracts.Kraiken;
  
  // Determine recipient address
  let walletAddr: string;
  if (privateKey) {
    const { Wallet } = await import('ethers');
    walletAddr = new Wallet(privateKey).address;
  } else {
    walletAddr = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'; // default Anvil #0
  }
  
  // Transfer KRK from deployer (Anvil #0) to recipient
  // Give 100 KRK per "ETH" parameter (deployer has ~2K KRK after bootstrap)
  const krkAmount = Math.min(parseFloat(ethAmount) * 100, 500);
  const { ethers } = await import('ethers');
  const provider = new ethers.JsonRpcProvider(rpcUrl);
  const DEPLOYER_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
  const deployer = new ethers.Wallet(DEPLOYER_KEY, provider);
  
  const krk = new ethers.Contract(krkAddress, [
    'function transfer(address,uint256) returns (bool)',
    'function balanceOf(address) view returns (uint256)'
  ], deployer);
  
  const amount = ethers.parseEther(krkAmount.toString());
  console.log(`[HELPER] Transferring ${krkAmount} KRK to ${walletAddr}...`);
  
  const tx = await krk.transfer(walletAddr, amount);
  await tx.wait();
  
  const balance = await krk.balanceOf(walletAddr);
  console.log(`[HELPER] KRK balance: ${ethers.formatEther(balance)} KRK`);
}

/**
 * Take an annotated screenshot with a description
 */
export async function takeScreenshot(
  page: Page,
  personaName: string,
  moment: string,
  report: TestReport
): Promise<void> {
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
  const filename = `${personaName.toLowerCase().replace(/\s+/g, '-')}-${moment.toLowerCase().replace(/\s+/g, '-')}-${timestamp}.png`;
  const dirPath = join('test-results', 'usertest', personaName.toLowerCase().replace(/\s+/g, '-'));
  
  try {
    mkdirSync(dirPath, { recursive: true });
  } catch (e) {
    // Directory may already exist
  }
  
  const filepath = join(dirPath, filename);
  await page.screenshot({ path: filepath, fullPage: true });
  
  report.screenshots.push(filepath);
  console.log(`[SCREENSHOT] ${moment}: ${filepath}`);
}

/**
 * Log a persona observation (what they think/feel)
 */
export function logObservation(personaName: string, observation: string, report: TestReport): void {
  const message = `[${personaName}] ${observation}`;
  console.log(message);
  report.uiObservations.push(observation);
}

/**
 * Log copy/messaging feedback
 */
export function logCopyFeedback(personaName: string, feedback: string, report: TestReport): void {
  const message = `[${personaName} - COPY] ${feedback}`;
  console.log(message);
  report.copyFeedback.push(feedback);
}

/**
 * Log a tokenomics question the persona would have
 */
export function logTokenomicsQuestion(personaName: string, question: string, report: TestReport): void {
  const message = `[${personaName} - TOKENOMICS] ${question}`;
  console.log(message);
  report.tokenomicsQuestions.push(question);
}

/**
 * Record a page visit
 */
export function recordPageVisit(
  pageName: string,
  url: string,
  startTime: number,
  report: TestReport
): void {
  const timeSpent = Date.now() - startTime;
  report.pagesVisited.push({
    page: pageName,
    url,
    timeSpent,
    timestamp: new Date().toISOString(),
  });
}

/**
 * Record an action attempt
 */
export function recordAction(
  action: string,
  success: boolean,
  error: string | undefined,
  report: TestReport
): void {
  report.actionsAttempted.push({
    action,
    success,
    error,
    timestamp: new Date().toISOString(),
  });
}

/**
 * Write the final report to JSON
 */
export function writeReport(personaName: string, report: TestReport): void {
  const dirPath = join(process.cwd(), 'tmp', 'usertest-results');
  
  try {
    mkdirSync(dirPath, { recursive: true });
  } catch (e) {
    // Directory may already exist
  }
  
  const filename = `${personaName.toLowerCase().replace(/\s+/g, '-')}.json`;
  const filepath = join(dirPath, filename);
  
  writeFileSync(filepath, JSON.stringify(report, null, 2), 'utf-8');
  console.log(`[REPORT] Written to ${filepath}`);
}

/**
 * Create a new test report
 */
export function createReport(personaName: string): TestReport {
  return {
    personaName,
    testDate: new Date().toISOString(),
    pagesVisited: [],
    actionsAttempted: [],
    screenshots: [],
    uiObservations: [],
    copyFeedback: [],
    tokenomicsQuestions: [],
    overallSentiment: '',
  };
}

/**
 * New feedback structure for redesigned tests
 */
export interface PersonaFeedback {
  persona: string;
  test: 'A' | 'B';
  timestamp: string;
  journey: 'passive-holder' | 'staker';
  steps: Array<{
    step: string;
    screenshot?: string;
    feedback: string[];
  }>;
  overall: {
    wouldBuy?: boolean;
    wouldReturn?: boolean;
    wouldStake?: boolean;
    friction: string[];
  };
}

/**
 * Create new feedback structure
 */
export function createPersonaFeedback(
  persona: string,
  test: 'A' | 'B',
  journey: 'passive-holder' | 'staker'
): PersonaFeedback {
  return {
    persona,
    test,
    timestamp: new Date().toISOString(),
    journey,
    steps: [],
    overall: {
      friction: []
    }
  };
}

/**
 * Add a step to persona feedback
 */
export function addFeedbackStep(
  feedback: PersonaFeedback,
  step: string,
  observations: string[],
  screenshot?: string
): void {
  feedback.steps.push({
    step,
    screenshot,
    feedback: observations
  });
}

/**
 * Write persona feedback to JSON
 */
export function writePersonaFeedback(feedback: PersonaFeedback): void {
  const dirPath = join(process.cwd(), 'tmp', 'usertest-results');
  
  try {
    mkdirSync(dirPath, { recursive: true });
  } catch (e) {
    // Directory may already exist
  }
  
  const filename = `${feedback.persona.toLowerCase()}-test-${feedback.test.toLowerCase()}.json`;
  const filepath = join(dirPath, filename);
  
  writeFileSync(filepath, JSON.stringify(feedback, null, 2), 'utf-8');
  console.log(`[FEEDBACK] Written to ${filepath}`);
}

/**
 * Navigate to stake page and attempt to stake
 */
export async function attemptStake(
  page: Page,
  amount: string,
  taxRateIndex: string,
  personaName: string,
  report: TestReport
): Promise<void> {
  console.log(`[${personaName}] Attempting to stake ${amount} KRK at tax rate ${taxRateIndex}%...`);
  
  const baseUrl = page.url().split('#')[0];
  await page.goto(`${baseUrl}stake`);

  try {
    // Wait for stake form to fully load
    const tokenAmountSlider = page.getByRole('slider', { name: 'Token Amount' });
    await tokenAmountSlider.waitFor({ state: 'visible', timeout: 15_000 });
    
    // Wait for KRK balance to load in UI (critical — without this, button shows "Insufficient Balance")
    console.log(`[${personaName}] Waiting for KRK balance to load in UI...`);
    try {
      await page.waitForFunction(() => {
        const balEl = document.querySelector('.balance');
        if (!balEl) return false;
        const text = balEl.textContent || '';
        const match = text.match(/([\d,.]+)/);
        return match && parseFloat(match[1].replace(/,/g, '')) > 0;
      }, { timeout: 150_000 });
      const balText = await page.locator('.balance').first().textContent();
      console.log(`[${personaName}] Balance loaded: ${balText}`);
    } catch (e) {
      console.log(`[${personaName}] WARNING: Balance did not load within 90s — staking may fail`);
    }
    
    // Fill amount
    const stakeAmountInput = page.getByLabel('Staking Amount');
    await stakeAmountInput.waitFor({ state: 'visible', timeout: 10_000 });
    await stakeAmountInput.fill(amount);

    // Select tax rate
    const taxSelect = page.getByRole('combobox', { name: 'Tax' });
    await taxSelect.selectOption({ value: taxRateIndex });

    // Take screenshot before attempting to click
    const screenshotDir = join('test-results', 'usertest', personaName.toLowerCase().replace(/\s+/g, '-'));
    mkdirSync(screenshotDir, { recursive: true });
    
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const screenshotPath = join(screenshotDir, `stake-form-filled-${timestamp}.png`);
    await page.screenshot({ path: screenshotPath, fullPage: true });
    report.screenshots.push(screenshotPath);
    console.log(`[${personaName}] Screenshot: ${screenshotPath}`);
    
    // Find ALL buttons in the stake form to see actual state
    const allButtons = await page.getByRole('main').getByRole('button').all();
    const buttonTexts = await Promise.all(
      allButtons.map(async (btn) => {
        try {
          return await btn.textContent();
        } catch {
          return null;
        }
      })
    );
    console.log(`[${personaName}] Available buttons: ${buttonTexts.filter(Boolean).join(', ')}`);
    
    // Check for error state buttons
    const buttonText = buttonTexts.join(' ');
    if (buttonText.includes('Insufficient Balance')) {
      const errorMsg = 'Cannot stake: Insufficient KRK balance. Buy more KRK first.';
      console.log(`[${personaName}] ${errorMsg}`);
      recordAction(`Stake ${amount} KRK at ${taxRateIndex}% tax`, false, errorMsg, report);
      throw new Error(errorMsg);
    }
    
    if (buttonText.includes('Stake Amount Too Low')) {
      const errorMsg = 'Cannot stake: Amount is below minimum stake requirement.';
      console.log(`[${personaName}] ${errorMsg}`);
      recordAction(`Stake ${amount} KRK at ${taxRateIndex}% tax`, false, errorMsg, report);
      throw new Error(errorMsg);
    }
    
    if (buttonText.includes('Tax Rate Too Low')) {
      const errorMsg = 'Cannot stake: No open positions at this tax rate. Increase tax rate.';
      console.log(`[${personaName}] ${errorMsg}`);
      recordAction(`Stake ${amount} KRK at ${taxRateIndex}% tax`, false, errorMsg, report);
      throw new Error(errorMsg);
    }
    
    // Wait for stake button with longer timeout
    const stakeButton = page.getByRole('main').getByRole('button', { name: /^(Stake|Snatch and Stake)$/i });
    await stakeButton.waitFor({ state: 'visible', timeout: 15_000 });
    
    const finalButtonText = await stakeButton.textContent();
    console.log(`[${personaName}] Clicking button: "${finalButtonText}"`);
    
    await stakeButton.click();
    
    // Wait for transaction
    try {
      await page.getByRole('button', { name: /Sign Transaction|Waiting/i }).waitFor({ state: 'visible', timeout: 5_000 });
      await page.getByRole('button', { name: /^(Stake|Snatch and Stake)$/i }).waitFor({ state: 'visible', timeout: 60_000 });
    } catch (e) {
      // May complete instantly
    }
    
    recordAction(`Stake ${amount} KRK at ${taxRateIndex}% tax`, true, undefined, report);
    console.log(`[${personaName}] Stake successful`);
  } catch (error: any) {
    recordAction(`Stake ${amount} KRK at ${taxRateIndex}% tax`, false, error.message, report);
    console.log(`[${personaName}] Stake failed: ${error.message}`);
    throw error;
  }
}
