#!/usr/bin/env npx tsx

/**
 * Authentication handler for web automation
 * Supports generic form login and Microsoft SSO (MSAL)
 *
 * Usage:
 *   npx tsx auth.ts --url "https://example.com/login" --type form
 *   npx tsx auth.ts --url "https://example.com" --type msal
 *   npx tsx auth.ts --url "https://example.com" --type auto
 */

import { getPage, launchBrowser } from './browse.js';
import parseArgs from 'minimist';
import type { Page, BrowserContext } from 'playwright-core';
import { createInterface } from 'readline';

// Types
type AuthType = 'auto' | 'form' | 'msal';

interface AuthOptions {
  url: string;
  authType: AuthType;
  credentials?: {
    username: string;
    password: string;
  };
  headless?: boolean;
  timeout?: number;
}

interface AuthResult {
  success: boolean;
  finalUrl: string;
  authType: AuthType;
  message: string;
}

// Get credentials from environment or options
function getCredentials(options?: {
  username?: string;
  password?: string;
}): { username: string; password: string } | null {
  const username = options?.username || process.env.CLOAKBROWSER_USERNAME || process.env.CAMOUFOX_USERNAME;
  const password = options?.password || process.env.CLOAKBROWSER_PASSWORD || process.env.CAMOUFOX_PASSWORD;

  if (!username || !password) {
    return null;
  }

  return { username, password };
}

// Prompt user for input (for MFA or credentials)
async function promptUser(question: string, hidden = false): Promise<string> {
  const rl = createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  return new Promise((resolve) => {
    if (hidden) {
      process.stdout.write(question);
      // Note: This is a simple implementation. For production, use a proper hidden input library
    }
    rl.question(question, (answer) => {
      rl.close();
      resolve(answer);
    });
  });
}

// Detect authentication type from page
async function detectAuthType(page: Page): Promise<AuthType> {
  const url = page.url();

  // Check for Microsoft login
  if (
    url.includes('login.microsoftonline.com') ||
    url.includes('login.live.com') ||
    url.includes('login.windows.net')
  ) {
    return 'msal';
  }

  // Check for common form login patterns
  const hasLoginForm = await page.evaluate(() => {
    const passwordField = document.querySelector(
      'input[type="password"], input[name*="password"], input[id*="password"]'
    );
    const usernameField = document.querySelector(
      'input[type="email"], input[type="text"][name*="user"], input[type="text"][name*="email"], input[id*="user"], input[id*="email"]'
    );
    return !!(passwordField && usernameField);
  });

  if (hasLoginForm) {
    return 'form';
  }

  return 'auto';
}

// Handle generic form login
async function handleFormLogin(
  page: Page,
  credentials: { username: string; password: string },
  timeout: number
): Promise<boolean> {
  console.log('Attempting form login...');

  // Find and fill username/email field
  const usernameSelectors = [
    'input[type="email"]',
    'input[name*="user" i]',
    'input[name*="email" i]',
    'input[id*="user" i]',
    'input[id*="email" i]',
    'input[autocomplete="username"]',
    'input[type="text"]:first-of-type',
  ];

  let usernameField = null;
  for (const selector of usernameSelectors) {
    usernameField = await page.$(selector);
    if (usernameField) break;
  }

  if (!usernameField) {
    console.error('Could not find username/email field');
    return false;
  }

  await usernameField.fill(credentials.username);
  console.log('Filled username field');

  // Find and fill password field
  const passwordSelectors = [
    'input[type="password"]',
    'input[name*="password" i]',
    'input[id*="password" i]',
    'input[autocomplete="current-password"]',
  ];

  let passwordField = null;
  for (const selector of passwordSelectors) {
    passwordField = await page.$(selector);
    if (passwordField) break;
  }

  if (!passwordField) {
    console.error('Could not find password field');
    return false;
  }

  await passwordField.fill(credentials.password);
  console.log('Filled password field');

  // Check for "Remember me" checkbox and check it
  const rememberCheckbox = await page.$(
    'input[type="checkbox"][name*="remember" i], input[type="checkbox"][id*="remember" i]'
  );
  if (rememberCheckbox) {
    await rememberCheckbox.check();
    console.log('Checked "Remember me" checkbox');
  }

  // Find and click submit button
  const submitSelectors = [
    'button[type="submit"]',
    'input[type="submit"]',
    'button:has-text("Sign in")',
    'button:has-text("Log in")',
    'button:has-text("Login")',
    'button:has-text("Submit")',
    '[role="button"]:has-text("Sign in")',
  ];

  let submitButton = null;
  for (const selector of submitSelectors) {
    submitButton = await page.$(selector);
    if (submitButton) break;
  }

  if (!submitButton) {
    // Try pressing Enter as fallback
    await passwordField.press('Enter');
  } else {
    await submitButton.click();
  }

  console.log('Submitted login form');

  // Wait for navigation or error
  try {
    await page.waitForNavigation({ timeout, waitUntil: 'domcontentloaded' });
    return true;
  } catch {
    // Check if we're still on login page with error
    const errorMessages = await page.$$eval(
      '.error, .alert-danger, [role="alert"], .login-error',
      (els) => els.map((el) => el.textContent?.trim()).filter(Boolean)
    );

    if (errorMessages.length > 0) {
      console.error('Login error:', errorMessages.join(', '));
      return false;
    }

    return true; // Might have succeeded without navigation
  }
}

// Handle Microsoft SSO login
async function handleMsalLogin(
  page: Page,
  credentials: { username: string; password: string },
  timeout: number
): Promise<boolean> {
  console.log('Attempting Microsoft SSO login...');

  const currentUrl = page.url();

  // If not already on Microsoft login, wait for redirect
  if (!currentUrl.includes('login.microsoftonline.com')) {
    try {
      await page.waitForURL('**/login.microsoftonline.com/**', { timeout: 10000 });
    } catch {
      console.log('Not redirected to Microsoft login');
      return false;
    }
  }

  // Wait for email input
  const emailInput = await page.waitForSelector(
    'input[type="email"], input[name="loginfmt"]',
    { timeout }
  );

  if (!emailInput) {
    console.error('Could not find email input on Microsoft login');
    return false;
  }

  // Fill email and submit
  await emailInput.fill(credentials.username);
  console.log('Filled email field');

  const nextButton = await page.$('input[type="submit"], button[type="submit"]');
  if (nextButton) {
    await nextButton.click();
  } else {
    await emailInput.press('Enter');
  }

  // Wait for password page
  try {
    await page.waitForSelector(
      'input[type="password"], input[name="passwd"]',
      { timeout }
    );
  } catch {
    // Might be using passwordless auth or different flow
    console.log('Password field not found - might be using different auth flow');
    return false;
  }

  // Fill password
  const passwordInput = await page.$('input[type="password"], input[name="passwd"]');
  if (!passwordInput) {
    console.error('Could not find password input');
    return false;
  }

  await passwordInput.fill(credentials.password);
  console.log('Filled password field');

  // Submit
  const signInButton = await page.$('input[type="submit"], button[type="submit"]');
  if (signInButton) {
    await signInButton.click();
  } else {
    await passwordInput.press('Enter');
  }

  // Handle "Stay signed in?" prompt
  try {
    const staySignedInButton = await page.waitForSelector(
      'input[value="Yes"], button:has-text("Yes")',
      { timeout: 5000 }
    );
    if (staySignedInButton) {
      await staySignedInButton.click();
      console.log('Clicked "Stay signed in" button');
    }
  } catch {
    // Prompt might not appear
  }

  // Check for Conditional Access Policy error
  const caError = await page.$('text=Conditional Access policy');
  if (caError) {
    console.error('Blocked by Conditional Access Policy');
    // Take screenshot for debugging
    await page.screenshot({ path: 'ca-policy-error.png' });
    console.log('Screenshot saved: ca-policy-error.png');
    return false;
  }

  // Wait for redirect away from Microsoft login
  try {
    await page.waitForURL(
      (url) => !url.href.includes('login.microsoftonline.com'),
      { timeout }
    );
    return true;
  } catch {
    return false;
  }
}

// Check if user is already authenticated
async function isAuthenticated(page: Page, targetUrl: string): Promise<boolean> {
  const currentUrl = page.url();

  // If we're on the target URL (not a login page), we're likely authenticated
  if (currentUrl.startsWith(targetUrl)) {
    // Check for common login page indicators
    const isLoginPage = await page.evaluate(() => {
      const loginIndicators = [
        'input[type="password"]',
        'form[action*="login"]',
        'form[action*="signin"]',
        '.login-form',
        '#login',
      ];
      return loginIndicators.some((sel) => document.querySelector(sel) !== null);
    });

    return !isLoginPage;
  }

  return false;
}

// Main authentication function
export async function authenticate(options: AuthOptions): Promise<AuthResult> {
  const browser = await launchBrowser({ headless: options.headless ?? true });
  const page = await browser.newPage();
  const timeout = options.timeout ?? 30000;

  try {
    // Navigate to URL
    console.log(`Navigating to: ${options.url}`);
    await page.goto(options.url, { timeout: 60000, waitUntil: 'domcontentloaded' });

    // Check if already authenticated
    if (await isAuthenticated(page, options.url)) {
      return {
        success: true,
        finalUrl: page.url(),
        authType: 'auto',
        message: 'Already authenticated (session persisted from profile)',
      };
    }

    // Get credentials
    const credentials = options.credentials
      ? options.credentials
      : getCredentials();

    if (!credentials) {
      // No credentials - open interactive browser
      console.log('\nNo credentials provided. Opening browser for manual login...');
      console.log('Please complete the login process manually.');
      console.log('The session will be saved to your profile.');

      // Switch to headed mode for manual login
      await browser.close();
      const interactiveBrowser = await launchBrowser({ headless: false });
      const interactivePage = await interactiveBrowser.newPage();
      await interactivePage.goto(options.url);

      await promptUser('\nPress Enter when you have completed login...');

      const finalUrl = interactivePage.url();
      await interactiveBrowser.close();

      return {
        success: true,
        finalUrl,
        authType: 'auto',
        message: 'Manual login completed - session saved to profile',
      };
    }

    // Detect auth type if auto
    let authType = options.authType;
    if (authType === 'auto') {
      authType = await detectAuthType(page);
      console.log(`Detected auth type: ${authType}`);
    }

    // Handle authentication based on type
    let success = false;
    switch (authType) {
      case 'msal':
        success = await handleMsalLogin(page, credentials, timeout);
        break;
      case 'form':
      default:
        success = await handleFormLogin(page, credentials, timeout);
        break;
    }

    const finalUrl = page.url();

    return {
      success,
      finalUrl,
      authType,
      message: success
        ? `Authentication successful - session saved to profile`
        : 'Authentication failed',
    };
  } finally {
    await browser.close();
  }
}

// Navigate to authenticated page (handles auth if needed)
export async function navigateAuthenticated(
  url: string,
  options?: {
    credentials?: { username: string; password: string };
    headless?: boolean;
  }
): Promise<{ page: Page; browser: BrowserContext }> {
  const { page, browser } = await getPage({ headless: options?.headless ?? true });

  await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' });

  // Check if we need to authenticate
  if (!(await isAuthenticated(page, url))) {
    console.log('Session expired or not authenticated. Attempting login...');

    // Get credentials
    const credentials = options?.credentials ?? getCredentials();

    if (!credentials) {
      throw new Error(
        'Authentication required but no credentials provided. ' +
          'Set CLOAKBROWSER_USERNAME and CLOAKBROWSER_PASSWORD environment variables. Legacy aliases CAMOUFOX_USERNAME and CAMOUFOX_PASSWORD are also supported.'
      );
    }

    // Detect and handle auth
    const authType = await detectAuthType(page);

    let success = false;
    if (authType === 'msal') {
      success = await handleMsalLogin(page, credentials, 30000);
    } else {
      success = await handleFormLogin(page, credentials, 30000);
    }

    if (!success) {
      await browser.close();
      throw new Error('Authentication failed');
    }

    // Navigate back to original URL if we were redirected
    if (!page.url().startsWith(url)) {
      await page.goto(url, { timeout: 60000, waitUntil: 'domcontentloaded' });
    }
  }

  return { page, browser };
}

// CLI entry point
async function main() {
  const args = parseArgs(process.argv.slice(2), {
    string: ['url', 'type', 'username', 'password'],
    boolean: ['headless', 'help'],
    default: {
      type: 'auto',
      headless: false, // Default to headed for auth so user can see/interact
    },
    alias: {
      u: 'url',
      t: 'type',
      h: 'help',
    },
  });

  if (args.help || !args.url) {
    console.log(`
Web Authentication Handler

Usage:
  npx tsx auth.ts --url <url> [options]

Options:
  -u, --url <url>         URL to authenticate (required)
  -t, --type <type>       Auth type: auto, form, or msal (default: auto)
  --username <user>       Username/email (or set CLOAKBROWSER_USERNAME env var)
  --password <pass>       Password (or set CLOAKBROWSER_PASSWORD env var)
  --headless <bool>       Run in headless mode (default: false for auth)
  -h, --help              Show this help message

Auth Types:
  auto    Auto-detect authentication type
  form    Generic username/password form
  msal    Microsoft SSO (login.microsoftonline.com)

Environment Variables:
  CLOAKBROWSER_USERNAME       Default username/email for authentication
  CLOAKBROWSER_PASSWORD       Default password for authentication

Compatibility Aliases:
  CAMOUFOX_USERNAME          Legacy alias for CLOAKBROWSER_USERNAME
  CAMOUFOX_PASSWORD          Legacy alias for CLOAKBROWSER_PASSWORD

Examples:
  # Interactive login (no credentials, opens browser)
  npx tsx auth.ts --url "https://example.com/login"

  # Form login with credentials
  npx tsx auth.ts --url "https://example.com/login" --type form \\
    --username "user@example.com" --password "secret"

  # Microsoft SSO login
  CLOAKBROWSER_USERNAME=user@company.com CLOAKBROWSER_PASSWORD=secret \\
  npx tsx auth.ts --url "https://internal.company.com" --type msal

Notes:
  - Session is saved to ~/.cloakbrowser-profile/ for persistence
  - After successful auth, subsequent browses will be authenticated
  - Use --headless false if you need to handle MFA manually
`);
    process.exit(args.help ? 0 : 1);
  }

  const authType = args.type as AuthType;
  if (!['auto', 'form', 'msal'].includes(authType)) {
    console.error(`Invalid auth type: ${authType}. Must be auto, form, or msal.`);
    process.exit(1);
  }

  try {
    const result = await authenticate({
      url: args.url,
      authType,
      credentials:
        args.username && args.password
          ? { username: args.username, password: args.password }
          : undefined,
      headless: args.headless,
    });

    console.log(`\nAuthentication result:`);
    console.log(`  Success: ${result.success}`);
    console.log(`  Auth type: ${result.authType}`);
    console.log(`  Final URL: ${result.finalUrl}`);
    console.log(`  Message: ${result.message}`);

    process.exit(result.success ? 0 : 1);
  } catch (error) {
    console.error('Error:', error instanceof Error ? error.message : error);
    process.exit(1);
  }
}

// Run if executed directly
const isMainModule = process.argv[1]?.includes('auth.ts');
if (isMainModule) {
  main();
}
