import { Router, Request, Response } from 'express';
import rateLimit from 'express-rate-limit';
import crypto from 'crypto';
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import {
  findUserByEmail, createUser, findUserById, getPickBalance,
  setVerificationToken, verifyEmail, findByVerificationToken, ensureDailyGrant,
  updateAffiliateAttribution, markAffiliateSignupTracked, recordLoginIp,
  isInGracePeriod, needsGraceVerification, updateCredentials, getProfile,
  setPasswordResetToken, findByPasswordResetToken, resetPassword,
  findUserByGoogleSub, linkGoogleAccount,
  reserveVerificationEmailCooldown, releaseVerificationEmailCooldown,
  DAILY_FORECAST_GRANT, NEW_ACCOUNT_FORECASTS, SURVEY_BONUS_FORECASTS,
} from '../models/user';
import { signToken, authMiddleware, clearAuthCookies, setAuthCookies } from '../middleware/auth';
import { sendPasswordResetEmail, sendVerificationEmail, sendWelcomeEmail } from '../services/email';
import { trackAffiliateConversion } from '../services/affiliate';
import { trackTikTokLead, trackTikTokLeadSubmission, trackTikTokSubmitForm } from '../services/tiktok';

const AFFILIATE_COOKIE = 'rm_affiliate_tracking';

function getClientIp(req: Request): string {
  const forwarded = req.headers['x-forwarded-for'];
  if (forwarded) {
    const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded).split(',')[0].trim();
    return first;
  }
  return req.socket.remoteAddress || '0.0.0.0';
}

const router = Router();

const FRONTEND_URL = process.env.FRONTEND_URL || 'https://rainmakersports.app';
const API_URL = process.env.API_URL || `${FRONTEND_URL.replace(/\/$/, '')}/api`;
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID || '';
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET || '';
const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI || `${API_URL}/auth/google/callback`;
const GOOGLE_SCOPE = 'openid email profile';
const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
const GOOGLE_TOKEN_URL = 'https://oauth2.googleapis.com/token';
const GOOGLE_USERINFO_URL = 'https://openidconnect.googleapis.com/v1/userinfo';

function normalizeAccountKey(value: unknown): string | null {
  if (typeof value !== 'string') return null;
  const normalized = value.trim().toLowerCase();
  return isValidEmail(normalized) ? normalized : null;
}

function buildAccountRateLimitKey(req: Request): string {
  return normalizeAccountKey(req.body?.email) || `ip:${req.ip || req.socket.remoteAddress || 'unknown'}`;
}

function buildAuthenticatedRateLimitKey(req: Request): string {
  return req.user?.userId || `ip:${req.ip || req.socket.remoteAddress || 'unknown'}`;
}

// Strict rate limits for auth endpoints.
const loginIpLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  skipSuccessfulRequests: true,
  message: { error: 'Too many login attempts, try again in 15 minutes' },
});
const loginAccountLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: buildAccountRateLimitKey,
  skipSuccessfulRequests: true,
  message: { error: 'Too many login attempts for that account, try again in 15 minutes' },
});
const signupLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: buildAccountRateLimitKey,
  skipSuccessfulRequests: true,
  message: { error: 'Too many signup attempts, try again in 15 minutes' },
});
const forgotPasswordLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: buildAccountRateLimitKey,
  message: { error: 'Too many reset attempts for that account, try again in 15 minutes' },
});
const resetPasswordLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 10, message: { error: 'Too many reset attempts, try again in 15 minutes' } });

const RESEND_VERIFICATION_COOLDOWN_SECONDS = 60;
const resendVerificationPublicLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: buildAccountRateLimitKey,
  message: { error: 'Too many verification emails requested, try again in 15 minutes' },
});
const resendVerificationLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  keyGenerator: buildAuthenticatedRateLimitKey,
  message: { error: 'Too many verification emails requested, try again in 15 minutes' },
});

// Valid ISO 3166-1 alpha-2 codes (subset of most common; accepts any 2-letter uppercase)
function isValidCountryCode(code: string): boolean {
  return /^[A-Z]{2}$/.test(code);
}

// Email format validation
function isValidEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email) && email.length <= 254;
}

async function buildAuthUserResponse(user: any) {
  const balance = await getPickBalance(user.id);
  return {
    id: user.id,
    email: user.email,
    is_weatherman: user.is_weatherman,
    monthly_pass_expires: user.monthly_pass_expires || null,
    monthlyPassExpiresAt: user.monthly_pass_expires || null,
    ...balance,
    grace_expires_at: user.grace_expires_at,
    in_grace_period: isInGracePeriod(user),
    needs_grace_verification: needsGraceVerification(user),
  };
}

function getAuthTokenVersion(user: { auth_token_version?: number | null }): number {
  const version = Number(user.auth_token_version);
  return Number.isInteger(version) && version >= 0 ? version : 0;
}

type GoogleStatePayload = {
  mode: 'login' | 'signup';
  returnTo: string;
  adultConfirmed: boolean;
  tracking?: TikTokLeadTrackingPayload | null;
};

const GOOGLE_STATE_COOKIE = 'rm_google_oauth_state';
const ADULT_CONFIRMATION_ERROR = 'You must confirm you are 18 or older to create an account.';

type TikTokLeadTrackingPayload = {
  consent?: boolean;
  eventId?: string | null;
  method?: string | null;
  placement?: string | null;
  referrer?: string | null;
  ttclid?: string | null;
  ttp?: string | null;
  url?: string | null;
};

function getDefaultGoogleReturnTo(mode: 'login' | 'signup'): string {
  return mode === 'signup' ? '/forecasts?welcome=true&survey=1' : '/';
}

function sanitizeReturnTo(value: unknown, mode: 'login' | 'signup'): string {
  if (typeof value !== 'string' || !value.startsWith('/') || value.startsWith('//')) {
    return getDefaultGoogleReturnTo(mode);
  }
  return value;
}

function hasAdultConfirmation(value: unknown): boolean {
  if (typeof value === 'boolean') return value;
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    return normalized === 'true' || normalized === '1' || normalized === 'yes' || normalized === 'on';
  }
  return false;
}

function sanitizeOptionalText(value: unknown, maxLength: number): string | null {
  if (typeof value !== 'string') return null;
  const normalized = value.trim();
  if (!normalized) return null;
  return normalized.slice(0, maxLength);
}

function parseTikTokLeadTracking(value: unknown): TikTokLeadTrackingPayload | null {
  if (!value || typeof value !== 'object') return null;
  const record = value as Record<string, unknown>;

  return {
    consent: record.consent === true,
    eventId: sanitizeOptionalText(record.eventId, 128),
    method: sanitizeOptionalText(record.method, 64),
    placement: sanitizeOptionalText(record.placement, 64),
    referrer: sanitizeOptionalText(record.referrer, 2048),
    ttclid: sanitizeOptionalText(record.ttclid, 512),
    ttp: sanitizeOptionalText(record.ttp, 512),
    url: sanitizeOptionalText(record.url, 2048),
  };
}

function getTikTokLeadTrackingFromQuery(req: Request): TikTokLeadTrackingPayload | null {
  return parseTikTokLeadTracking({
    consent: hasAdultConfirmation(req.query.trackingConsent),
    eventId: req.query.eventId,
    method: req.query.trackingMethod,
    placement: req.query.trackingPlacement,
    referrer: req.query.trackingReferrer,
    ttclid: req.query.ttclid,
    ttp: req.query.ttp,
    url: req.query.trackingUrl,
  });
}

function hasTikTokTrackingConsent(req: Request, tracking?: TikTokLeadTrackingPayload | null): boolean {
  return tracking?.consent === true || req.cookies?.rm_cookie_consent === 'accepted';
}

function getTikTokLeadIp(req: Request): string | null {
  const ip = getClientIp(req);
  return ip && ip !== '0.0.0.0' ? ip : null;
}

function buildTikTokEventId(base: string | null | undefined, suffix: string): string | null {
  const normalized = sanitizeOptionalText(base, 128);
  if (!normalized) return null;
  return `${normalized}_${suffix}`.slice(0, 128);
}

function maybeTrackTikTokSignup(req: Request, user: { id: string; email: string }, tracking?: TikTokLeadTrackingPayload | null): void {
  if (!hasTikTokTrackingConsent(req, tracking)) return;

  trackTikTokLead({
    email: user.email,
    eventId: tracking?.eventId,
    externalId: user.id,
    ip: getTikTokLeadIp(req),
    referrer: tracking?.referrer || sanitizeOptionalText(req.headers.referer, 2048),
    description: tracking?.placement || tracking?.method || 'signup',
    ttclid: tracking?.ttclid || sanitizeOptionalText(req.cookies?.rm_ttclid, 512),
    ttp: tracking?.ttp || sanitizeOptionalText(req.cookies?._ttp, 512),
    url: tracking?.url,
    userAgent: sanitizeOptionalText(req.headers['user-agent'], 512),
  }).catch((err) => {
    console.error('TikTok signup tracking failed (non-blocking):', err);
  });
}

function maybeTrackTikTokLeadSubmit(req: Request, user: { id: string; email: string }, tracking?: TikTokLeadTrackingPayload | null): void {
  if (!hasTikTokTrackingConsent(req, tracking)) return;

  trackTikTokLeadSubmission({
    email: user.email,
    eventId: buildTikTokEventId(tracking?.eventId, 'lead'),
    externalId: user.id,
    ip: getTikTokLeadIp(req),
    referrer: tracking?.referrer || sanitizeOptionalText(req.headers.referer, 2048),
    description: tracking?.placement || tracking?.method || 'quick_signup',
    ttclid: tracking?.ttclid || sanitizeOptionalText(req.cookies?.rm_ttclid, 512),
    ttp: tracking?.ttp || sanitizeOptionalText(req.cookies?._ttp, 512),
    url: tracking?.url,
    userAgent: sanitizeOptionalText(req.headers['user-agent'], 512),
  }).catch((err) => {
    console.error('TikTok lead submit tracking failed (non-blocking):', err);
  });

  trackTikTokSubmitForm({
    email: user.email,
    eventId: buildTikTokEventId(tracking?.eventId, 'submit_form'),
    externalId: user.id,
    ip: getTikTokLeadIp(req),
    referrer: tracking?.referrer || sanitizeOptionalText(req.headers.referer, 2048),
    description: tracking?.placement || tracking?.method || 'quick_signup',
    ttclid: tracking?.ttclid || sanitizeOptionalText(req.cookies?.rm_ttclid, 512),
    ttp: tracking?.ttp || sanitizeOptionalText(req.cookies?._ttp, 512),
    url: tracking?.url,
    userAgent: sanitizeOptionalText(req.headers['user-agent'], 512),
  }).catch((err) => {
    console.error('TikTok submit form tracking failed (non-blocking):', err);
  });
}

function isGoogleConfigured(): boolean {
  return Boolean(GOOGLE_CLIENT_ID && GOOGLE_CLIENT_SECRET);
}

function signGoogleState(payload: GoogleStatePayload): string {
  return jwt.sign(payload, process.env.JWT_SECRET!, {
    algorithm: 'HS256',
    expiresIn: '10m',
  });
}

function verifyGoogleState(rawState: string | undefined): GoogleStatePayload | null {
  if (!rawState) return null;
  try {
    const decoded = jwt.verify(rawState, process.env.JWT_SECRET!, { algorithms: ['HS256'] }) as Partial<GoogleStatePayload>;
    if (
      (decoded.mode !== 'login' && decoded.mode !== 'signup')
      || typeof decoded.returnTo !== 'string'
      || typeof decoded.adultConfirmed !== 'boolean'
    ) {
      return null;
    }
    return {
      mode: decoded.mode,
      returnTo: sanitizeReturnTo(decoded.returnTo, decoded.mode),
      adultConfirmed: decoded.adultConfirmed,
      tracking: parseTikTokLeadTracking(decoded.tracking || null),
    };
  } catch {
    return null;
  }
}

function buildGoogleFrontendRedirect(params: {
  success?: boolean;
  mode: 'login' | 'signup';
  returnTo?: string;
  error?: string;
}): string {
  const url = new URL(`${FRONTEND_URL.replace(/\/$/, '')}/auth/google`);
  url.searchParams.set('mode', params.mode);
  if (params.success) url.searchParams.set('success', 'true');
  if (params.returnTo) url.searchParams.set('returnTo', params.returnTo);
  if (params.error) url.searchParams.set('error', params.error);
  return url.toString();
}

function buildFrontendReturnUrl(returnTo: string): string {
  return new URL(sanitizeReturnTo(returnTo, 'login'), `${FRONTEND_URL.replace(/\/$/, '')}/`).toString();
}

function setGoogleStateCookie(res: Response, state: string): void {
  res.cookie(GOOGLE_STATE_COOKIE, state, {
    httpOnly: true,
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
    maxAge: 10 * 60 * 1000,
    path: '/api/auth/google',
  });
}

function clearGoogleStateCookie(res: Response): void {
  res.clearCookie(GOOGLE_STATE_COOKIE, {
    httpOnly: true,
    sameSite: 'lax',
    secure: process.env.NODE_ENV === 'production',
    path: '/api/auth/google',
  });
}

async function exchangeGoogleCodeForTokens(code: string) {
  const tokenRes = await fetch(GOOGLE_TOKEN_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      code,
      client_id: GOOGLE_CLIENT_ID,
      client_secret: GOOGLE_CLIENT_SECRET,
      redirect_uri: GOOGLE_REDIRECT_URI,
      grant_type: 'authorization_code',
    }),
  });

  if (!tokenRes.ok) {
    throw new Error(`Google token exchange failed with ${tokenRes.status}`);
  }

  return tokenRes.json() as Promise<{ access_token?: string }>;
}

async function fetchGoogleProfile(accessToken: string) {
  const profileRes = await fetch(GOOGLE_USERINFO_URL, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });

  if (!profileRes.ok) {
    throw new Error(`Google userinfo failed with ${profileRes.status}`);
  }

  return profileRes.json() as Promise<{
    sub?: string;
    email?: string;
    email_verified?: boolean;
    name?: string;
  }>;
}

async function maybeTrackAffiliateSignup(req: Request, userId: string): Promise<void> {
  const affCookie = req.cookies?.[AFFILIATE_COOKIE];
  if (!affCookie) return;

  let aff: any = null;
  try {
    aff = typeof affCookie === 'string' ? JSON.parse(affCookie) : affCookie;
  } catch {
    return;
  }

  if (!aff?.code || !aff?.trackingId) return;

  await updateAffiliateAttribution(userId, aff.code, aff.trackingId);
  await trackAffiliateConversion(aff.trackingId, 'signup');
  await markAffiliateSignupTracked(userId);
}

// POST /api/auth/signup
router.post('/signup', signupLimiter, async (req: Request, res: Response) => {
  try {
    const { email, password, marketingConsent, country_code, adultConfirmed, tracking } = req.body;
    const leadTracking = parseTikTokLeadTracking(tracking);
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password required' });
      return;
    }
    if (!hasAdultConfirmation(adultConfirmed)) {
      res.status(400).json({ error: ADULT_CONFIRMATION_ERROR });
      return;
    }
    if (typeof email !== 'string' || !isValidEmail(email)) {
      res.status(400).json({ error: 'Valid email address required' });
      return;
    }
    if (typeof password !== 'string' || password.length < 8 || password.length > 72) {
      res.status(400).json({ error: 'Password must be 8-72 characters' });
      return;
    }

    // Validate country_code if provided
    let finalCountryCode: string | null = null;
    let countrySource = 'unknown';
    if (country_code && typeof country_code === 'string') {
      const upper = country_code.toUpperCase();
      if (isValidCountryCode(upper)) {
        finalCountryCode = upper;
        countrySource = 'user'; // User confirmed/selected
      }
    }

    const existing = await findUserByEmail(email);
    if (existing) {
      res.status(409).json({ error: 'Email already registered' });
      return;
    }

    const passwordHash = await bcrypt.hash(password, 12);
    const user = await createUser(email, passwordHash, !!marketingConsent, finalCountryCode, countrySource);
    const jwtToken = signToken({ userId: user.id, email: user.email, tokenVersion: getAuthTokenVersion(user) });
    setAuthCookies(res, jwtToken);

    // Capture signup IP
    recordLoginIp(user.id, getClientIp(req), true).catch(err => console.error('IP capture failed:', err));

    // Generate verification token
    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hours
    await setVerificationToken(user.id, verificationToken, expires);

    // Send verification email (fire and forget — don't block signup)
    sendVerificationEmail(email, verificationToken).catch((err) =>
      console.error('Failed to send verification email:', err)
    );
    maybeTrackTikTokSignup(req, user, leadTracking);

    // Affiliate attribution (never blocks signup)
    try {
      const affCookie = req.cookies?.[AFFILIATE_COOKIE];
      if (affCookie) {
        let aff: any = null;
        try {
          aff = typeof affCookie === 'string' ? JSON.parse(affCookie) : affCookie;
        } catch { /* malformed cookie — ignore */ }
        if (aff?.code && aff?.trackingId) {
          await updateAffiliateAttribution(user.id, aff.code, aff.trackingId);
          await trackAffiliateConversion(aff.trackingId, 'signup');
          await markAffiliateSignupTracked(user.id);
          console.log(`Affiliate signup tracked: user=${user.id} code=${aff.code}`);
        }
      }
    } catch (affErr) {
      console.error('Affiliate signup tracking failed (non-blocking):', affErr);
    }

    res.status(201).json({
      user: await buildAuthUserResponse(user),
      message: `Account created. You received ${NEW_ACCOUNT_FORECASTS} free forecasts on your new account. Verify your email and complete a survey for another ${SURVEY_BONUS_FORECASTS} bonus forecasts. After both bonus buckets are gone, you reset to ${DAILY_FORECAST_GRANT} per day.`,
    });
  } catch (err) {
    console.error('Signup error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// POST /api/auth/login
router.post('/login', loginIpLimiter, loginAccountLimiter, async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password required' });
      return;
    }

    const user = await findUserByEmail(email);
    if (!user) {
      res.status(401).json({ error: 'Invalid credentials' });
      return;
    }

    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) {
      res.status(401).json({ error: 'Invalid credentials' });
      return;
    }

    const jwtToken = signToken({ userId: user.id, email: user.email, tokenVersion: getAuthTokenVersion(user) });
    const balance = await getPickBalance(user.id);
    setAuthCookies(res, jwtToken);

    // Capture login IP
    recordLoginIp(user.id, getClientIp(req)).catch(err => console.error('IP capture failed:', err));

    res.json({
      user: {
        id: user.id,
        email: user.email,
        is_weatherman: user.is_weatherman,
        ...balance,
      },
    });
  } catch (err) {
    console.error('Login error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET /api/auth/google/start
router.get('/google/start', async (req: Request, res: Response) => {
  const mode = req.query.mode === 'signup' ? 'signup' : 'login';
  const returnTo = sanitizeReturnTo(req.query.returnTo, mode);
  const adultConfirmed = hasAdultConfirmation(req.query.adultConfirmed);
  const tracking = mode === 'signup' ? getTikTokLeadTrackingFromQuery(req) : null;

  if (!isGoogleConfigured()) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'not_configured' }));
    return;
  }

  if (mode === 'signup' && !adultConfirmed) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'adult_confirmation_required' }));
    return;
  }

  const state = signGoogleState({
    mode,
    returnTo,
    adultConfirmed: mode === 'signup' ? adultConfirmed : false,
    tracking,
  });
  setGoogleStateCookie(res, state);
  const params = new URLSearchParams({
    client_id: GOOGLE_CLIENT_ID,
    redirect_uri: GOOGLE_REDIRECT_URI,
    response_type: 'code',
    scope: GOOGLE_SCOPE,
    state,
    access_type: 'online',
    include_granted_scopes: 'true',
    prompt: 'select_account',
  });

  res.redirect(`${GOOGLE_AUTH_URL}?${params.toString()}`);
});

// GET /api/auth/google/callback
router.get('/google/callback', async (req: Request, res: Response) => {
  const rawState = typeof req.query.state === 'string' ? req.query.state : undefined;
  const cookieState = typeof req.cookies?.[GOOGLE_STATE_COOKIE] === 'string' ? req.cookies[GOOGLE_STATE_COOKIE] : undefined;
  const state = verifyGoogleState(rawState);
  const mode = state?.mode || 'login';
  const returnTo = state?.returnTo || getDefaultGoogleReturnTo(mode);
  const tracking = state?.tracking || null;

  clearGoogleStateCookie(res);

  if (!isGoogleConfigured()) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'not_configured' }));
    return;
  }

  if (!state) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'invalid_state' }));
    return;
  }

  if (mode === 'signup' && !state.adultConfirmed) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'adult_confirmation_required' }));
    return;
  }

  if (!cookieState || rawState !== cookieState) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'invalid_state' }));
    return;
  }

  if (typeof req.query.error === 'string') {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: req.query.error }));
    return;
  }

  const code = typeof req.query.code === 'string' ? req.query.code : '';
  if (!code) {
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'missing_code' }));
    return;
  }

  try {
    const tokenPayload = await exchangeGoogleCodeForTokens(code);
    const accessToken = tokenPayload.access_token;
    if (!accessToken) {
      throw new Error('Google access token missing');
    }

    const googleProfile = await fetchGoogleProfile(accessToken);
    const email = googleProfile.email?.toLowerCase();
    const googleSub = googleProfile.sub;

    if (!email || !isValidEmail(email) || !googleSub) {
      throw new Error('Google profile missing required identity fields');
    }

    if (googleProfile.email_verified === false) {
      res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'email_not_verified' }));
      return;
    }

    let user = await findUserByGoogleSub(googleSub);
    let isNewUser = false;

    if (!user) {
      const existingByEmail = await findUserByEmail(email);
      if (existingByEmail) {
        await linkGoogleAccount(existingByEmail.id, googleSub);
        await ensureDailyGrant(existingByEmail.id);
        user = await findUserById(existingByEmail.id);
      } else {
        const randomPass = crypto.randomBytes(16).toString('hex');
        const passwordHash = await bcrypt.hash(randomPass, 12);
        const created = await createUser(email, passwordHash, false);
        await linkGoogleAccount(created.id, googleSub);
        await ensureDailyGrant(created.id);
        recordLoginIp(created.id, getClientIp(req), true).catch(err => console.error('IP capture failed:', err));
        maybeTrackAffiliateSignup(req, created.id).catch((err) =>
          console.error('Affiliate Google signup tracking failed (non-blocking):', err),
        );
        maybeTrackTikTokSignup(req, created, tracking);
        sendWelcomeEmail(email).catch((err) =>
          console.error('Failed to send welcome email:', err),
        );
        user = await findUserById(created.id);
        isNewUser = true;
      }
    }

    if (!user) {
      throw new Error('Failed to load user after Google auth');
    }

    if (!isNewUser) {
      recordLoginIp(user.id, getClientIp(req)).catch(err => console.error('IP capture failed:', err));
    }

    const jwtToken = signToken({
      userId: user.id,
      email: user.email,
      tokenVersion: getAuthTokenVersion(user),
    });
    setAuthCookies(res, jwtToken);

    res.redirect(buildFrontendReturnUrl(returnTo));
  } catch (err) {
    console.error('Google auth callback error:', err);
    res.redirect(buildGoogleFrontendRedirect({ mode, returnTo, error: 'google_auth_failed' }));
  }
});

// POST /api/auth/forgot-password
router.post('/forgot-password', forgotPasswordLimiter, async (req: Request, res: Response) => {
  try {
    const { email } = req.body;
    if (!email || typeof email !== 'string' || !isValidEmail(email)) {
      res.status(400).json({ error: 'Valid email address required' });
      return;
    }

    const user = await findUserByEmail(email);
    if (user) {
      const resetToken = crypto.randomBytes(32).toString('hex');
      const expires = new Date(Date.now() + 60 * 60 * 1000);
      await setPasswordResetToken(user.id, resetToken, expires);
      sendPasswordResetEmail(user.email, resetToken).catch((err) =>
        console.error('Failed to send password reset email:', err),
      );
    }

    res.json({ message: 'If that account exists, a password reset email has been sent.' });
  } catch (err) {
    console.error('Forgot password error:', err);
    res.status(500).json({ error: 'Failed to process password reset request' });
  }
});

// POST /api/auth/reset-password
router.post('/reset-password', resetPasswordLimiter, async (req: Request, res: Response) => {
  try {
    const { token, password } = req.body;
    if (!token || typeof token !== 'string') {
      res.status(400).json({ error: 'Reset token required' });
      return;
    }
    if (typeof password !== 'string' || password.length < 8 || password.length > 72) {
      res.status(400).json({ error: 'Password must be 8-72 characters' });
      return;
    }

    const user = await findByPasswordResetToken(token);
    if (!user) {
      res.status(400).json({ error: 'This reset link is invalid or has expired.' });
      return;
    }

    const passwordHash = await bcrypt.hash(password, 12);
    await resetPassword(user.id, passwordHash);

    res.json({ message: 'Password updated. You can log in now.' });
  } catch (err) {
    console.error('Reset password error:', err);
    res.status(500).json({ error: 'Failed to reset password' });
  }
});

// POST /api/auth/logout
router.post('/logout', (_req: Request, res: Response) => {
  clearAuthCookies(res);
  res.status(204).end();
});

// GET /api/auth/me
router.get('/me', authMiddleware, async (req: Request, res: Response) => {
  try {
    const user = await findUserById(req.user!.userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const balance = await getPickBalance(user.id);
    const profile = await getProfile(user.id);

    res.json({
      id: user.id,
      email: user.email,
      is_weatherman: user.is_weatherman,
      ...balance,
      created_at: user.created_at,
      grace_expires_at: user.grace_expires_at,
      in_grace_period: isInGracePeriod(user),
      needs_grace_verification: needsGraceVerification(user),
      profile: profile ? {
        favorite_sports_ranked: profile.favorite_sports_ranked,
        interest_buckets: profile.interest_buckets,
        market_participant: profile.market_participant,
      } : null,
    });
  } catch (err) {
    console.error('Me error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// POST /api/auth/resend-verification-public — no auth required, takes email+password
router.post('/resend-verification-public', signupLimiter, resendVerificationPublicLimiter, async (req: Request, res: Response) => {
  try {
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password are required' });
      return;
    }

    const user = await findUserByEmail(email);
    if (!user) {
      // Don't reveal whether account exists
      res.json({ message: 'If that account exists, a verification email has been sent.' });
      return;
    }

    // Verify password
    const valid = await bcrypt.compare(password, user.password_hash);
    if (!valid) {
      res.status(401).json({ error: 'Invalid email or password' });
      return;
    }

    if (user.email_verified) {
      res.json({ message: 'Email already verified. You can log in.', alreadyVerified: true });
      return;
    }

    const reservedAt = await reserveVerificationEmailCooldown(user.id, RESEND_VERIFICATION_COOLDOWN_SECONDS);
    if (!reservedAt) {
      res.status(429).json({ error: 'Please wait 60 seconds before resending' });
      return;
    }

    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
    try {
      await setVerificationToken(user.id, verificationToken, expires);
      await sendVerificationEmail(user.email, verificationToken);
    } catch (error) {
      await releaseVerificationEmailCooldown(user.id, reservedAt).catch(() => undefined);
      throw error;
    }

    res.json({ message: 'Verification email sent! Check your inbox.' });
  } catch (err) {
    console.error('Resend verification public error:', err);
    res.status(500).json({ error: 'Failed to send verification email' });
  }
});

// POST /api/auth/resend-verification
router.post('/resend-verification', authMiddleware, resendVerificationLimiter, async (req: Request, res: Response) => {
  try {
    const userId = req.user!.userId;
    const user = await findUserById(userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    if (user.email_verified) {
      res.json({ message: 'Email already verified' });
      return;
    }

    const reservedAt = await reserveVerificationEmailCooldown(userId, RESEND_VERIFICATION_COOLDOWN_SECONDS);
    if (!reservedAt) {
      res.status(429).json({ error: 'Please wait 60 seconds before resending' });
      return;
    }

    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
    try {
      await setVerificationToken(userId, verificationToken, expires);
      await sendVerificationEmail(user.email, verificationToken);
    } catch (error) {
      await releaseVerificationEmailCooldown(userId, reservedAt).catch(() => undefined);
      throw error;
    }

    res.json({ message: 'Verification email sent' });
  } catch (err) {
    console.error('Resend verification error:', err);
    res.status(500).json({ error: 'Failed to send verification email' });
  }
});

// POST /api/auth/quick-signup — email-only signup for gating modal
router.post('/quick-signup', signupLimiter, async (req: Request, res: Response) => {
  try {
    const { email, adultConfirmed, tracking } = req.body;
    const leadTracking = parseTikTokLeadTracking(tracking);
    if (!email || typeof email !== 'string' || !isValidEmail(email)) {
      res.status(400).json({ error: 'Valid email address required' });
      return;
    }

    const existing = await findUserByEmail(email);
    if (existing) {
      // Never mint a session for an existing account from email-only input.
      if (!existing.email_verified) {
        const verificationToken = crypto.randomBytes(32).toString('hex');
        const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
        await setVerificationToken(existing.id, verificationToken, expires);
        sendVerificationEmail(email, verificationToken).catch((err) =>
          console.error('Failed to send verification email:', err)
        );
        res.json({
          authenticated: false,
          verificationSent: true,
          message: 'Verification email sent. Verify your email to continue.',
        });
        return;
      }
      res.status(409).json({ error: 'This email already has an account. Please log in.' });
      return;
    }

    if (!hasAdultConfirmation(adultConfirmed)) {
      res.status(400).json({ error: ADULT_CONFIRMATION_ERROR });
      return;
    }

    // Create user with random password
    const randomPass = crypto.randomBytes(16).toString('hex');
    const passwordHash = await bcrypt.hash(randomPass, 12);
    const user = await createUser(email, passwordHash, false);

    // Capture signup IP
    recordLoginIp(user.id, getClientIp(req), true).catch(err => console.error('IP capture failed:', err));

    // Generate verification token
    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
    await setVerificationToken(user.id, verificationToken, expires);

    sendVerificationEmail(email, verificationToken).catch((err) =>
      console.error('Failed to send verification email:', err)
    );
    maybeTrackTikTokLeadSubmit(req, user, leadTracking);

    // Affiliate attribution (never blocks signup)
    try {
      const affCookie = req.cookies?.[AFFILIATE_COOKIE];
      if (affCookie) {
        let aff: any = null;
        try {
          aff = typeof affCookie === 'string' ? JSON.parse(affCookie) : affCookie;
        } catch { /* malformed cookie — ignore */ }
        if (aff?.code && aff?.trackingId) {
          await updateAffiliateAttribution(user.id, aff.code, aff.trackingId);
          await trackAffiliateConversion(aff.trackingId, 'signup');
          await markAffiliateSignupTracked(user.id);
          console.log(`Affiliate quick-signup tracked: user=${user.id} code=${aff.code}`);
        }
      }
    } catch (affErr) {
      console.error('Affiliate quick-signup tracking failed (non-blocking):', affErr);
    }

    const jwtToken = signToken({ userId: user.id, email: user.email, tokenVersion: getAuthTokenVersion(user) });
    setAuthCookies(res, jwtToken);

    res.status(201).json({
      user: await buildAuthUserResponse(user),
      message: `Your new account received ${NEW_ACCOUNT_FORECASTS} free forecasts. Verify your email and complete one survey for another ${SURVEY_BONUS_FORECASTS} bonus forecasts. After both bonus buckets are gone, you reset to ${DAILY_FORECAST_GRANT} per day.`,
    });
  } catch (err) {
    console.error('Quick signup error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// POST /api/auth/update-credentials-and-verify
router.post('/update-credentials-and-verify', authMiddleware, async (req: Request, res: Response) => {
  try {
    const userId = req.user!.userId;
    const { email, password } = req.body;
    if (!email || !password) {
      res.status(400).json({ error: 'Email and password required' });
      return;
    }
    if (typeof email !== 'string' || !isValidEmail(email)) {
      res.status(400).json({ error: 'Valid email address required' });
      return;
    }
    if (typeof password !== 'string' || password.length < 8 || password.length > 72) {
      res.status(400).json({ error: 'Password must be 8-72 characters' });
      return;
    }

    const user = await findUserById(userId);
    if (!user) {
      res.status(404).json({ error: 'User not found' });
      return;
    }

    const newPasswordHash = await bcrypt.hash(password, 12);
    const emailChanged = email.toLowerCase() !== user.email.toLowerCase();

    let nextTokenVersion = 0;
    if (emailChanged) {
      // Check if new email is already taken
      const existing = await findUserByEmail(email);
      if (existing) {
        res.status(409).json({ error: 'Email already in use by another account' });
        return;
      }
      // Hash old email for audit trail
      const oldEmailHash = crypto.createHash('sha256').update(user.email.toLowerCase()).digest('hex');
      nextTokenVersion = await updateCredentials(userId, email, newPasswordHash, oldEmailHash);
    } else {
      nextTokenVersion = await updateCredentials(userId, email, newPasswordHash);
    }

    // Generate verification token + send email
    const verificationToken = crypto.randomBytes(32).toString('hex');
    const expires = new Date(Date.now() + 24 * 60 * 60 * 1000);
    await setVerificationToken(userId, verificationToken, expires);
    sendVerificationEmail(email.toLowerCase(), verificationToken).catch((err) =>
      console.error('Failed to send verification email:', err)
    );

    const newToken = signToken({
      userId,
      email: emailChanged ? email.toLowerCase() : user.email,
      tokenVersion: nextTokenVersion,
    });
    setAuthCookies(res, newToken);
    res.json({ ok: true, emailChanged });
  } catch (err) {
    console.error('Update credentials error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// GET /api/auth/verify-email?token=...
router.get('/verify-email', async (req: Request, res: Response) => {
  try {
    const token = String(req.query.token || '');
    if (!token) {
      res.redirect(`${FRONTEND_URL}/verify?error=missing_token`);
      return;
    }

    const user = await findByVerificationToken(token);
    if (!user) {
      res.redirect(`${FRONTEND_URL}/verify?error=invalid_token`);
      return;
    }

    await verifyEmail(user.id);
    await ensureDailyGrant(user.id);

    // Send welcome email (fire-and-forget)
    sendWelcomeEmail(user.email).catch((err) =>
      console.error('Failed to send welcome email:', err)
    );

    const jwtToken = signToken({ userId: user.id, email: user.email, tokenVersion: getAuthTokenVersion(user) });
    setAuthCookies(res, jwtToken);
    res.redirect(`${FRONTEND_URL}/verify?success=true`);
  } catch (err) {
    console.error('Verify email error:', err);
    res.redirect(`${FRONTEND_URL}/verify?error=server_error`);
  }
});

export default router;
