import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import mongoose from 'mongoose';
import { config } from '../config';
import { createLogger } from '../utils/logger';
import { authAttemptsCounter } from '../utils/metrics';
import { AuthPayload, UserCredentials } from '../types';
import { UserModel } from '../models/User';

const logger = createLogger('Auth');

// Failed authentication attempts tracker (for IP banning)
const failedAttempts = new Map<string, { count: number; lastAttempt: Date }>();

// IP ban list
const bannedIPs = new Map<string, Date>();

// Check whether MongoDB is connected
function isDbConnected(): boolean {
  return mongoose.connection.readyState === 1;
}

/**
 * Initialize demo user (for testing)
 */
export async function initializeDemoUser() {
  if (!isDbConnected()) return;

  const demoEmail = 'demo@example.com';
  const demoPassword = 'demo123';

  const existing = await UserModel.findOne({ email: demoEmail });
  if (!existing) {
    const passwordHash = await bcrypt.hash(demoPassword, 10);
    await UserModel.create({ email: demoEmail, passwordHash, role: 'user', provider: 'local' });
    logger.info('Demo user created', { email: demoEmail });
  }
}

/**
 * Register a new user
 */
export async function registerUser(credentials: UserCredentials): Promise<void> {
  if (!isDbConnected()) throw new Error('Database not available');

  const existing = await UserModel.findOne({ email: credentials.email });
  if (existing) throw new Error('User already exists');

  const passwordHash = await bcrypt.hash(credentials.password, 10);
  await UserModel.create({ email: credentials.email, passwordHash, role: 'user', provider: 'local' });

  logger.info('User registered', { email: credentials.email });
}

/**
 * Authenticate user and generate JWT token
 */
export async function authenticateUser(credentials: UserCredentials, ip: string): Promise<string> {
  // Check if IP is banned
  if (config.security.enableIpBanning && bannedIPs.has(ip)) {
    const banExpiry = bannedIPs.get(ip)!;
    if (banExpiry > new Date()) {
      authAttemptsCounter.inc({ status: 'banned' });
      throw new Error('IP address is temporarily banned');
    } else {
      bannedIPs.delete(ip);
    }
  }

  if (!isDbConnected()) throw new Error('Database not available');

  const user = await UserModel.findOne({ email: credentials.email });

  if (!user || !user.passwordHash) {
    await trackFailedAttempt(ip);
    authAttemptsCounter.inc({ status: 'failed' });
    throw new Error('Invalid credentials');
  }

  const isValid = await bcrypt.compare(credentials.password, user.passwordHash);
  if (!isValid) {
    await trackFailedAttempt(ip);
    authAttemptsCounter.inc({ status: 'failed' });
    throw new Error('Invalid credentials');
  }

  failedAttempts.delete(ip);

  const payload: AuthPayload = { userId: user.email, email: user.email, role: user.role, plan: user.plan || 'free' };
  const token = jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn as string } as jwt.SignOptions);

  authAttemptsCounter.inc({ status: 'success' });
  logger.info('User authenticated', { email: user.email });

  return token;
}

/**
 * Change user password
 */
export async function changePassword(email: string, currentPassword: string, newPassword: string): Promise<void> {
  if (!isDbConnected()) throw new Error('Database not available');

  const user = await UserModel.findOne({ email });
  if (!user || !user.passwordHash) throw new Error('Invalid credentials');

  const isValid = await bcrypt.compare(currentPassword, user.passwordHash);
  if (!isValid) throw new Error('Invalid credentials');

  user.passwordHash = await bcrypt.hash(newPassword, 10);
  await user.save();

  logger.info('Password changed', { email });
}

/**
 * Track failed authentication attempt
 */
async function trackFailedAttempt(ip: string): Promise<void> {
  if (!config.security.enableIpBanning) return;

  const attempts = failedAttempts.get(ip) || { count: 0, lastAttempt: new Date() };
  attempts.count++;
  attempts.lastAttempt = new Date();
  failedAttempts.set(ip, attempts);

  if (attempts.count >= config.security.maxFailedAuthAttempts) {
    const banUntil = new Date(Date.now() + config.security.banDuration);
    bannedIPs.set(ip, banUntil);
    logger.warn('IP banned due to failed auth attempts', { ip, attempts: attempts.count, banUntil });
  }
}

/**
 * Verify JWT token middleware
 */
export function verifyToken(req: Request, res: Response, next: NextFunction): void {
  const authHeader = req.headers.authorization;

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    res.status(401).json({ error: 'No token provided' });
    return;
  }

  const token = authHeader.substring(7);

  try {
    const decoded = jwt.verify(token, config.jwt.secret) as AuthPayload;
    (req as any).user = decoded;
    next();
  } catch (error) {
    logger.warn('Invalid token', { error: error instanceof Error ? error.message : String(error) });
    res.status(401).json({ error: 'Invalid token' });
  }
}

/**
 * Optional authentication middleware (doesn't fail if no token)
 */
export function optionalAuth(req: Request, _res: Response, next: NextFunction): void {
  const authHeader = req.headers.authorization;

  if (authHeader && authHeader.startsWith('Bearer ')) {
    const token = authHeader.substring(7);
    try {
      const decoded = jwt.verify(token, config.jwt.secret) as AuthPayload;
      (req as any).user = decoded;
    } catch {
      // Ignore invalid tokens in optional auth
    }
  }

  next();
}

/**
 * Verify JWT token from header or query parameter (for resource requests)
 */
export function verifyTokenWithQuery(req: Request, res: Response, next: NextFunction): void {
  const authHeader = req.headers.authorization;
  let token: string | undefined;

  if (authHeader && authHeader.startsWith('Bearer ')) {
    token = authHeader.substring(7);
  } else {
    token = req.query.token as string | undefined;

    if (token && (token.includes('%2F') || token.includes('%2B') || token.includes('%3D'))) {
      try {
        token = decodeURIComponent(token);
      } catch {
        // If decode fails, use original
      }
    }
  }

  if (!token) {
    res.status(401).json({ error: 'No token provided' });
    return;
  }

  try {
    const decoded = jwt.verify(token, config.jwt.secret) as AuthPayload;
    (req as any).user = decoded;
    next();
  } catch (error) {
    logger.warn('Invalid token', {
      error: error instanceof Error ? error.message : String(error),
      tokenLength: token?.length,
      tokenPrefix: token?.substring(0, 20),
      url: req.url,
    });
    res.status(401).json({ error: 'Invalid token' });
  }
}

/**
 * Find or create a user from Google OAuth profile, returns JWT
 */
export async function findOrCreateGoogleUser(profile: { email: string; googleId: string; name?: string }): Promise<string> {
  if (!isDbConnected()) throw new Error('Database not available');

  let user = await UserModel.findOne({ email: profile.email });

  if (user) {
    if (!user.googleId) {
      user.googleId = profile.googleId;
      await user.save();
    }
  } else {
    user = await UserModel.create({ email: profile.email, role: 'user', provider: 'google', googleId: profile.googleId });
    logger.info('Google user created', { email: profile.email });
  }

  const payload: AuthPayload = { userId: user.email, email: user.email, role: user.role, plan: user.plan || 'free' };
  const token = jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn as string } as jwt.SignOptions);

  authAttemptsCounter.inc({ status: 'success' });
  logger.info('Google user authenticated', { email: user.email });

  return token;
}

/**
 * Find or create a user from Apple Sign-In, returns JWT
 */
export async function findOrCreateAppleUser(profile: { email: string; appleId: string }): Promise<string> {
  if (!isDbConnected()) throw new Error('Database not available');

  let user = await UserModel.findOne({ appleId: profile.appleId });

  if (user) {
    if (profile.email && !profile.email.includes('privaterelay') && user.email !== profile.email) {
      user.email = profile.email;
      await user.save();
    }
  } else {
    user = await UserModel.findOne({ email: profile.email });
    if (user) {
      user.appleId = profile.appleId;
      await user.save();
    } else {
      user = await UserModel.create({ email: profile.email, role: 'user', provider: 'apple', appleId: profile.appleId });
      logger.info('Apple user created', { email: profile.email });
    }
  }

  const payload: AuthPayload = { userId: user.email, email: user.email, role: user.role, plan: user.plan || 'free' };
  const token = jwt.sign(payload, config.jwt.secret, { expiresIn: config.jwt.expiresIn as string } as jwt.SignOptions);

  authAttemptsCounter.inc({ status: 'success' });
  logger.info('Apple user authenticated', { email: user.email });

  return token;
}

/**
 * Check if user has required role
 */
export function requireRole(role: string) {
  return (req: Request, res: Response, next: NextFunction): void => {
    const user = (req as any).user as AuthPayload;

    if (!user) {
      res.status(401).json({ error: 'Authentication required' });
      return;
    }

    if (user.role !== role && user.role !== 'admin') {
      res.status(403).json({ error: 'Insufficient permissions' });
      return;
    }

    next();
  };
}
