import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
import { db, dbGet, users, magicLinkTokens, User } from '../db/index.js';
import { eq } from 'drizzle-orm';
import {
  hashPassword,
  verifyPassword,
  createToken,
  createRefreshToken,
  isFirstUser,
  getAuthUser,
  validatePassword,
  createMagicLinkToken,
  verifyMagicLinkToken,
  invalidateAllUserSessions,
  bumpTokenVersion,
  requireAuth,
  getUserPasswordHash,
} from '../lib/auth.js';
import { generateId, getNow, toDbBool } from '../lib/utils.js';
import { sendEmail } from '../lib/email.js';
import { rateLimitMiddleware } from '../lib/rateLimit.js';

// Per-IP rate limit for sensitive auth endpoints (registration, login, and all
// email-dispatching flows) to curb credential stuffing and email flooding.
const authRateLimit = rateLimitMiddleware({ max: 20, windowMs: 15 * 60 * 1000, prefix: 'auth' });

// User type that includes all fields (some added in schema updates)
type AuthUser = User & {
  isClaimed: boolean;
  googleId: string | null;
  rucNumber: string | null;
  accountStatus: string;
};

const auth = new Hono();

// Rate limiting store (in production, use Redis)
const loginAttempts = new Map<string, { count: number; resetAt: number }>();
const MAX_LOGIN_ATTEMPTS = 5;
const LOCKOUT_DURATION = 15 * 60 * 1000; // 15 minutes

function checkRateLimit(email: string): { allowed: boolean; retryAfter?: number } {
  const now = Date.now();
  const attempts = loginAttempts.get(email);
  
  if (!attempts) {
    return { allowed: true };
  }
  
  if (now > attempts.resetAt) {
    loginAttempts.delete(email);
    return { allowed: true };
  }
  
  if (attempts.count >= MAX_LOGIN_ATTEMPTS) {
    return { allowed: false, retryAfter: Math.ceil((attempts.resetAt - now) / 1000) };
  }
  
  return { allowed: true };
}

function recordFailedAttempt(email: string): void {
  const now = Date.now();
  const attempts = loginAttempts.get(email) || { count: 0, resetAt: now + LOCKOUT_DURATION };
  attempts.count++;
  loginAttempts.set(email, attempts);
}

function clearFailedAttempts(email: string): void {
  loginAttempts.delete(email);
}

const registerSchema = z.object({
  email: z.string().email(),
  password: z.string().min(10, 'Password must be at least 10 characters'),
  name: z.string().min(2),
  phone: z.string().optional(),
  languagePreference: z.enum(['en', 'es']).optional(),
});

const loginSchema = z.object({
  email: z.string().email(),
  password: z.string(),
});

const magicLinkRequestSchema = z.object({
  email: z.string().email(),
});

const magicLinkVerifySchema = z.object({
  token: z.string(),
});

const passwordResetRequestSchema = z.object({
  email: z.string().email(),
});

const passwordResetSchema = z.object({
  token: z.string(),
  password: z.string().min(10, 'Password must be at least 10 characters'),
});

const claimAccountSchema = z.object({
  token: z.string(),
  password: z.string().min(10, 'Password must be at least 10 characters'),
});

const changePasswordSchema = z.object({
  currentPassword: z.string(),
  newPassword: z.string().min(10, 'Password must be at least 10 characters'),
});

const googleAuthSchema = z.object({
  credential: z.string(), // Google ID token
});

// Register
auth.post('/register', authRateLimit, zValidator('json', registerSchema), async (c) => {
  const data = c.req.valid('json');
  
  // Validate password strength
  const passwordValidation = validatePassword(data.password);
  if (!passwordValidation.valid) {
    return c.json({ error: passwordValidation.error }, 400);
  }
  
  // Check if email exists
  const existing = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, data.email))
  );
  if (existing) {
    // If user exists but is unclaimed, allow claiming
    if (!existing.isClaimed || existing.accountStatus === 'unclaimed') {
      return c.json({ 
        error: 'Email already registered', 
        canClaim: true,
        message: 'This email has an unclaimed account. Please check your email for the claim link or request a new one.'
      }, 400);
    }
    return c.json({ error: 'Email already registered' }, 400);
  }
  
  // Check if first user (becomes admin)
  const firstUser = await isFirstUser();
  
  const hashedPassword = await hashPassword(data.password);
  const now = getNow();
  const id = generateId();
  
  const newUser = {
    id,
    email: data.email,
    password: hashedPassword,
    name: data.name,
    phone: data.phone || null,
    role: firstUser ? 'admin' : 'user',
    languagePreference: data.languagePreference || null,
    isClaimed: toDbBool(true),
    googleId: null,
    rucNumber: null,
    accountStatus: 'active',
    createdAt: now,
    updatedAt: now,
  };
  
  await (db as any).insert(users).values(newUser);
  
  const token = await createToken(id, data.email, newUser.role, 0);
  const refreshToken = await createRefreshToken(id);
  
  return c.json({
    user: {
      id,
      email: data.email,
      name: data.name,
      role: newUser.role,
      isClaimed: true,
    },
    token,
    refreshToken,
    message: firstUser ? 'Admin account created successfully' : 'Account created successfully',
  }, 201);
});

// Login with email/password
auth.post('/login', authRateLimit, zValidator('json', loginSchema), async (c) => {
  const data = c.req.valid('json');
  
  // Check rate limit
  const rateLimit = checkRateLimit(data.email);
  if (!rateLimit.allowed) {
    return c.json({ 
      error: 'Too many login attempts. Please try again later.',
      retryAfter: rateLimit.retryAfter
    }, 429);
  }
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, data.email))
  );
  if (!user) {
    recordFailedAttempt(data.email);
    return c.json({ error: 'Invalid credentials' }, 401);
  }
  
  // Check if account is suspended
  if (user.accountStatus === 'suspended') {
    return c.json({ error: 'Account is suspended. Please contact support.' }, 403);
  }
  
  // Check if user has a password set
  if (!user.password) {
    return c.json({ 
      error: 'No password set for this account',
      needsClaim: !user.isClaimed,
      message: user.isClaimed 
        ? 'Please use Google login or request a password reset.'
        : 'Please claim your account first.'
    }, 400);
  }
  
  const validPassword = await verifyPassword(data.password, user.password);
  if (!validPassword) {
    recordFailedAttempt(data.email);
    return c.json({ error: 'Invalid credentials' }, 401);
  }
  
  // Clear failed attempts on successful login
  clearFailedAttempts(data.email);

  // Transparently upgrade legacy bcrypt hashes to argon2 now that we have the
  // plaintext and have verified it. Best-effort: a failure here must not block
  // the login.
  if (!String(user.password).startsWith('$argon2')) {
    try {
      const upgradedHash = await hashPassword(data.password);
      await (db as any)
        .update(users)
        .set({ password: upgradedHash })
        .where(eq((users as any).id, user.id));
    } catch (err: any) {
      console.error('[auth] Failed to upgrade legacy password hash:', err?.message || err);
    }
  }
  
  const token = await createToken(user.id, user.email, user.role, user.tokenVersion ?? 0);
  const refreshToken = await createRefreshToken(user.id);
  
  return c.json({
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      isClaimed: user.isClaimed,
      phone: user.phone,
      rucNumber: user.rucNumber,
      languagePreference: user.languagePreference,
    },
    token,
    refreshToken,
  });
});

// Request magic link login
auth.post('/magic-link/request', authRateLimit, zValidator('json', magicLinkRequestSchema), async (c) => {
  const { email } = c.req.valid('json');
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, email))
  );
  
  if (!user) {
    // Don't reveal if email exists
    return c.json({ message: 'If an account exists with this email, a login link has been sent.' });
  }
  
  if (user.accountStatus === 'suspended') {
    return c.json({ message: 'If an account exists with this email, a login link has been sent.' });
  }
  
  // Create magic link token (expires in 10 minutes)
  const token = await createMagicLinkToken(user.id, 'login', 10);
  const magicLink = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/auth/magic-link?token=${token}`;
  
  // Send email
  try {
    await sendEmail({
      to: email,
      subject: 'Your Spanglish Login Link',
      html: `
        <h2>Login to Spanglish</h2>
        <p>Click the link below to log in. This link expires in 10 minutes.</p>
        <p><a href="${magicLink}" style="background-color: #3B82F6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Log In</a></p>
        <p>Or copy this link: ${magicLink}</p>
        <p>If you didn't request this, you can safely ignore this email.</p>
      `,
    });
  } catch (error) {
    console.error('Failed to send magic link email:', error);
  }
  
  return c.json({ message: 'If an account exists with this email, a login link has been sent.' });
});

// Verify magic link and login
auth.post('/magic-link/verify', authRateLimit, zValidator('json', magicLinkVerifySchema), async (c) => {
  const { token } = c.req.valid('json');
  
  const verification = await verifyMagicLinkToken(token, 'login');
  
  if (!verification.valid) {
    return c.json({ error: verification.error }, 400);
  }
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).id, verification.userId))
  );
  
  if (!user || user.accountStatus === 'suspended') {
    return c.json({ error: 'Invalid token' }, 400);
  }
  
  const authToken = await createToken(user.id, user.email, user.role, user.tokenVersion ?? 0);
  const refreshToken = await createRefreshToken(user.id);
  
  return c.json({
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      isClaimed: user.isClaimed,
      phone: user.phone,
      rucNumber: user.rucNumber,
      languagePreference: user.languagePreference,
    },
    token: authToken,
    refreshToken,
  });
});

// Request password reset
auth.post('/password-reset/request', authRateLimit, zValidator('json', passwordResetRequestSchema), async (c) => {
  const { email } = c.req.valid('json');
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, email))
  );
  
  if (!user) {
    // Don't reveal if email exists
    return c.json({ message: 'If an account exists with this email, a password reset link has been sent.' });
  }
  
  if (user.accountStatus === 'suspended') {
    return c.json({ message: 'If an account exists with this email, a password reset link has been sent.' });
  }
  
  // Create reset token (expires in 30 minutes)
  const token = await createMagicLinkToken(user.id, 'reset_password', 30);
  const resetLink = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/auth/reset-password?token=${token}`;
  
  // Send email
  try {
    await sendEmail({
      to: email,
      subject: 'Reset Your Spanglish Password',
      html: `
        <h2>Reset Your Password</h2>
        <p>Click the link below to reset your password. This link expires in 30 minutes.</p>
        <p><a href="${resetLink}" style="background-color: #3B82F6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Reset Password</a></p>
        <p>Or copy this link: ${resetLink}</p>
        <p>If you didn't request this, you can safely ignore this email.</p>
      `,
    });
  } catch (error) {
    console.error('Failed to send password reset email:', error);
  }
  
  return c.json({ message: 'If an account exists with this email, a password reset link has been sent.' });
});

// Reset password
auth.post('/password-reset/confirm', authRateLimit, zValidator('json', passwordResetSchema), async (c) => {
  const { token, password } = c.req.valid('json');
  
  // Validate password strength
  const passwordValidation = validatePassword(password);
  if (!passwordValidation.valid) {
    return c.json({ error: passwordValidation.error }, 400);
  }
  
  const verification = await verifyMagicLinkToken(token, 'reset_password');
  
  if (!verification.valid) {
    return c.json({ error: verification.error }, 400);
  }
  
  const hashedPassword = await hashPassword(password);
  const now = getNow();
  
  await (db as any)
    .update(users)
    .set({ 
      password: hashedPassword,
      updatedAt: now,
    })
    .where(eq((users as any).id, verification.userId));
  
  // Invalidate all existing sessions/JWTs for security
  await invalidateAllUserSessions(verification.userId!);
  await bumpTokenVersion(verification.userId!);
  
  return c.json({ message: 'Password reset successfully. Please log in with your new password.' });
});

// Claim unclaimed account
auth.post('/claim-account/request', authRateLimit, zValidator('json', magicLinkRequestSchema), async (c) => {
  const { email } = c.req.valid('json');
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).email, email))
  );
  
  if (!user) {
    return c.json({ message: 'If an unclaimed account exists with this email, a claim link has been sent.' });
  }
  
  if (user.isClaimed && user.accountStatus !== 'unclaimed') {
    return c.json({ error: 'Account is already claimed' }, 400);
  }
  
  // Create claim token (expires in 1 hour)
  const token = await createMagicLinkToken(user.id, 'claim_account', 60);
  const claimLink = `${process.env.FRONTEND_URL || 'http://localhost:3000'}/auth/claim-account?token=${token}`;
  
  // Send email
  try {
    await sendEmail({
      to: email,
      subject: 'Claim Your Spanglish Account',
      html: `
        <h2>Claim Your Account</h2>
        <p>An account was created for you during booking. Click below to set up your login credentials.</p>
        <p><a href="${claimLink}" style="background-color: #3B82F6; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; display: inline-block;">Claim Account</a></p>
        <p>Or copy this link: ${claimLink}</p>
        <p>This link expires in 1 hour.</p>
      `,
    });
  } catch (error) {
    console.error('Failed to send claim account email:', error);
  }
  
  return c.json({ message: 'If an unclaimed account exists with this email, a claim link has been sent.' });
});

// Complete account claim
auth.post('/claim-account/confirm', authRateLimit, zValidator('json', claimAccountSchema), async (c) => {
  const { token, password } = c.req.valid('json');
  
  const verification = await verifyMagicLinkToken(token, 'claim_account');
  
  if (!verification.valid) {
    return c.json({ error: verification.error }, 400);
  }
  
  const passwordValidation = validatePassword(password);
  if (!passwordValidation.valid) {
    return c.json({ error: passwordValidation.error }, 400);
  }

  const now = getNow();
  // Only set a password here. Linking a Google account requires a verified Google
  // ID token via /google; we never trust a client-supplied googleId.
  const updates: Record<string, any> = {
    isClaimed: toDbBool(true),
    accountStatus: 'active',
    password: await hashPassword(password),
    updatedAt: now,
  };
  
  await (db as any)
    .update(users)
    .set(updates)
    .where(eq((users as any).id, verification.userId));
  
  const user = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).id, verification.userId))
  );
  
  const authToken = await createToken(user.id, user.email, user.role, user.tokenVersion ?? 0);
  const refreshToken = await createRefreshToken(user.id);
  
  return c.json({
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      isClaimed: user.isClaimed,
      phone: user.phone,
      rucNumber: user.rucNumber,
      languagePreference: user.languagePreference,
    },
    token: authToken,
    refreshToken,
    message: 'Account claimed successfully!',
  });
});

// Google OAuth login/register
auth.post('/google', authRateLimit, zValidator('json', googleAuthSchema), async (c) => {
  const { credential } = c.req.valid('json');
  
  try {
    // Verify the Google ID token. Google's tokeninfo endpoint validates the
    // signature and expiry server-side; we additionally enforce the audience so a
    // token minted for a different OAuth client cannot be replayed against us.
    const response = await fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${encodeURIComponent(credential)}`);
    
    if (!response.ok) {
      return c.json({ error: 'Invalid Google token' }, 400);
    }
    
    const googleData = await response.json() as {
      sub: string;
      email: string;
      name: string;
      email_verified: string;
      aud?: string;
      exp?: string;
    };

    // email_verified can be returned as boolean true or string "true"
    if (String(googleData.email_verified) !== 'true') {
      return c.json({ error: 'Google email not verified' }, 400);
    }

    // Enforce audience when a client ID is configured (closes token-confusion attacks)
    const expectedAud = process.env.GOOGLE_CLIENT_ID;
    if (expectedAud) {
      if (googleData.aud !== expectedAud) {
        return c.json({ error: 'Invalid Google token audience' }, 400);
      }
    } else {
      console.warn('[auth] GOOGLE_CLIENT_ID is not set; skipping audience verification for Google login.');
    }

    // Reject expired tokens (defense-in-depth; tokeninfo also rejects them)
    if (googleData.exp && Number(googleData.exp) * 1000 < Date.now()) {
      return c.json({ error: 'Google token expired' }, 400);
    }
    
    const { sub: googleId, email, name } = googleData;
    
    // Check if user exists by email or google_id
    let user = await dbGet<any>(
      (db as any).select().from(users).where(eq((users as any).email, email))
    );
    
    if (!user) {
      // Check by google_id
      user = await dbGet<any>(
        (db as any).select().from(users).where(eq((users as any).googleId, googleId))
      );
    }
    
    const now = getNow();
    
    if (user) {
      // User exists - link Google account if not already linked
      if (user.accountStatus === 'suspended') {
        return c.json({ error: 'Account is suspended. Please contact support.' }, 403);
      }
      
      if (!user.googleId) {
        await (db as any)
          .update(users)
          .set({ 
            googleId,
            isClaimed: toDbBool(true),
            accountStatus: 'active',
            updatedAt: now,
          })
          .where(eq((users as any).id, user.id));
      }
      
      // Refresh user data
      user = await dbGet<any>(
        (db as any).select().from(users).where(eq((users as any).id, user.id))
      );
    } else {
      // Create new user
      const firstUser = await isFirstUser();
      const id = generateId();
      
      const newUser = {
        id,
        email,
        password: null,
        name,
        phone: null,
        role: firstUser ? 'admin' : 'user',
        languagePreference: null,
        isClaimed: toDbBool(true),
        googleId,
        rucNumber: null,
        accountStatus: 'active',
        createdAt: now,
        updatedAt: now,
      };
      
      await (db as any).insert(users).values(newUser);
      user = newUser;
    }
    
    const authToken = await createToken(user.id, user.email, user.role, user.tokenVersion ?? 0);
    const refreshToken = await createRefreshToken(user.id);
    
    return c.json({
      user: {
        id: user.id,
        email: user.email,
        name: user.name,
        role: user.role,
        isClaimed: user.isClaimed,
        phone: user.phone,
        rucNumber: user.rucNumber,
        languagePreference: user.languagePreference,
      },
      token: authToken,
      refreshToken,
    });
  } catch (error) {
    console.error('Google auth error:', error);
    return c.json({ error: 'Failed to authenticate with Google' }, 500);
  }
});

// Get current user
auth.get('/me', async (c) => {
  const user = await getAuthUser(c);
  
  if (!user) {
    return c.json({ error: 'Unauthorized' }, 401);
  }
  
  return c.json({
    user: {
      id: user.id,
      email: user.email,
      name: user.name,
      role: user.role,
      phone: user.phone,
      isClaimed: user.isClaimed,
      rucNumber: user.rucNumber,
      languagePreference: user.languagePreference,
      accountStatus: user.accountStatus,
      createdAt: user.createdAt,
    },
  });
});

// Change password (authenticated users)
auth.post('/change-password', requireAuth(), zValidator('json', changePasswordSchema), async (c) => {
  const user = (c as any).get('user') as AuthUser;
  const { currentPassword, newPassword } = c.req.valid('json');
  
  // Validate new password
  const passwordValidation = validatePassword(newPassword);
  if (!passwordValidation.valid) {
    return c.json({ error: passwordValidation.error }, 400);
  }
  
  // Verify current password if user has one
  const existingHash = await getUserPasswordHash(user.id);
  if (existingHash) {
    const validPassword = await verifyPassword(currentPassword, existingHash);
    if (!validPassword) {
      return c.json({ error: 'Current password is incorrect' }, 400);
    }
  }
  
  const hashedPassword = await hashPassword(newPassword);
  const now = getNow();
  
  await (db as any)
    .update(users)
    .set({ 
      password: hashedPassword,
      updatedAt: now,
    })
    .where(eq((users as any).id, user.id));

  // Invalidate all previously issued JWTs so a stolen old token can't outlive the change,
  // then hand the current client a fresh token so it stays logged in on this device.
  await bumpTokenVersion(user.id);
  const refreshedUser = await dbGet<any>(
    (db as any).select().from(users).where(eq((users as any).id, user.id))
  );
  const newToken = await createToken(user.id, user.email, user.role, refreshedUser?.tokenVersion ?? 0);
  
  return c.json({ message: 'Password changed successfully', token: newToken });
});

// Logout - invalidate all previously issued JWTs for this user (logout everywhere)
auth.post('/logout', async (c) => {
  const user = await getAuthUser(c);
  if (user) {
    await invalidateAllUserSessions(user.id);
    await bumpTokenVersion(user.id);
  }
  return c.json({ message: 'Logged out successfully' });
});

export default auth;
