import * as jose from 'jose';
import * as argon2 from 'argon2';
import bcrypt from 'bcryptjs';
import crypto from 'crypto';
import { Context } from 'hono';
import { db, dbGet, dbAll, users, magicLinkTokens, userSessions } from '../db/index.js';
import { eq, and, gt, sql, isNull } from 'drizzle-orm';
import { generateId, getNow, toDbDate } from './utils.js';

const DEFAULT_DEV_JWT_SECRET = 'your-super-secret-key-change-in-production';
const rawJwtSecret = process.env.JWT_SECRET;

// Never allow the insecure default in production: forgeable tokens = full account takeover.
if (process.env.NODE_ENV === 'production' && (!rawJwtSecret || rawJwtSecret === DEFAULT_DEV_JWT_SECRET)) {
  throw new Error('JWT_SECRET must be set to a strong, unique value in production. Refusing to start with the default secret.');
}
if (!rawJwtSecret) {
  console.warn('[auth] JWT_SECRET is not set; using an insecure development default. Set JWT_SECRET in production.');
}

const JWT_SECRET = new TextEncoder().encode(rawJwtSecret || DEFAULT_DEV_JWT_SECRET);
const JWT_ISSUER = 'spanglish';
const JWT_AUDIENCE = 'spanglish-app';

export interface JWTPayload {
  sub: string;
  email: string;
  role: string;
  tokenVersion?: number;
  iat: number;
  exp: number;
}

// Password hashing with Argon2 (spec requirement)
export async function hashPassword(password: string): Promise<string> {
  return argon2.hash(password, {
    type: argon2.argon2id,
    memoryCost: 65536, // 64 MB
    timeCost: 3,
    parallelism: 4,
  });
}

export async function verifyPassword(password: string, hash: string): Promise<boolean> {
  // Support both bcrypt (legacy) and argon2 hashes for migration
  if (hash.startsWith('$argon2')) {
    return argon2.verify(hash, password);
  }
  // Legacy bcrypt support
  return bcrypt.compare(password, hash);
}

// Generate secure random token for magic links
export function generateSecureToken(): string {
  return crypto.randomBytes(32).toString('hex');
}

// Create magic link token
export async function createMagicLinkToken(
  userId: string,
  type: 'login' | 'reset_password' | 'claim_account' | 'email_verification',
  expiresInMinutes: number = 10
): Promise<string> {
  const token = generateSecureToken();
  const now = getNow();
  const expiresAt = toDbDate(new Date(Date.now() + expiresInMinutes * 60 * 1000));

  await (db as any).insert(magicLinkTokens).values({
    id: generateId(),
    userId,
    token,
    type,
    expiresAt,
    createdAt: now,
  });

  return token;
}

// Verify and consume magic link token
export async function verifyMagicLinkToken(
  token: string,
  type: 'login' | 'reset_password' | 'claim_account' | 'email_verification'
): Promise<{ valid: boolean; userId?: string; error?: string }> {
  const now = getNow();
  
  const tokenRecord = await dbGet<any>(
    (db as any)
      .select()
      .from(magicLinkTokens)
      .where(
        and(
          eq((magicLinkTokens as any).token, token),
          eq((magicLinkTokens as any).type, type)
        )
      )
  );

  // Use a single generic error for all invalid states to avoid leaking token state
  const genericError = 'Invalid or expired token';

  if (!tokenRecord) {
    return { valid: false, error: genericError };
  }

  if (tokenRecord.usedAt) {
    return { valid: false, error: genericError };
  }

  if (new Date(tokenRecord.expiresAt) < new Date()) {
    return { valid: false, error: genericError };
  }

  // Atomically consume the token: only the request that flips used_at from NULL wins.
  // This prevents a double-spend race where two concurrent requests both pass the
  // read-time "not used" check above.
  const result: any = await (db as any)
    .update(magicLinkTokens)
    .set({ usedAt: now })
    .where(and(
      eq((magicLinkTokens as any).id, tokenRecord.id),
      isNull((magicLinkTokens as any).usedAt)
    ));

  const affected = result?.changes ?? result?.rowCount ?? 0;
  if (affected === 0) {
    return { valid: false, error: genericError };
  }

  return { valid: true, userId: tokenRecord.userId };
}

// Create user session
export async function createUserSession(
  userId: string,
  userAgent?: string,
  ipAddress?: string
): Promise<string> {
  const sessionToken = generateSecureToken();
  const now = getNow();
  const expiresAt = toDbDate(new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)); // 30 days

  await (db as any).insert(userSessions).values({
    id: generateId(),
    userId,
    token: sessionToken,
    userAgent: userAgent || null,
    ipAddress: ipAddress || null,
    lastActiveAt: now,
    expiresAt,
    createdAt: now,
  });

  return sessionToken;
}

// Get user's active sessions
export async function getUserSessions(userId: string) {
  const now = getNow();
  
  return dbAll(
    (db as any)
      .select()
      .from(userSessions)
      .where(
        and(
          eq((userSessions as any).userId, userId),
          gt((userSessions as any).expiresAt, now)
        )
      )
  );
}

// Invalidate a specific session
export async function invalidateSession(sessionId: string, userId: string): Promise<boolean> {
  const result = await (db as any)
    .delete(userSessions)
    .where(
      and(
        eq((userSessions as any).id, sessionId),
        eq((userSessions as any).userId, userId)
      )
    );
  
  return true;
}

// Invalidate all user sessions (logout everywhere)
export async function invalidateAllUserSessions(userId: string): Promise<void> {
  await (db as any)
    .delete(userSessions)
    .where(eq((userSessions as any).userId, userId));
}

// Small blocklist of common/weak passwords (and obvious app-specific ones).
// Compared case-insensitively after stripping non-alphanumerics so that e.g.
// "P@ssw0rd!" still matches "password".
const COMMON_PASSWORDS = new Set([
  'password', 'passw0rd', '123456', '1234567', '12345678', '123456789', '1234567890',
  'qwerty', 'qwertyuiop', 'letmein', 'welcome', 'admin', 'administrator', 'iloveyou',
  'monkey', 'dragon', 'sunshine', 'princess', 'football', 'baseball', 'abc123',
  'spanglish', 'changeme', 'secret', 'master', 'login', 'access',
]);

// Password policy: 10-128 chars, requires a mix of character types, and rejects
// common/weak passwords. Centralized so register/reset/change all share it.
export function validatePassword(password: string): { valid: boolean; error?: string } {
  if (password.length < 10) {
    return { valid: false, error: 'Password must be at least 10 characters long' };
  }
  if (password.length > 128) {
    return { valid: false, error: 'Password must be at most 128 characters long' };
  }

  const hasLower = /[a-z]/.test(password);
  const hasUpper = /[A-Z]/.test(password);
  const hasDigit = /\d/.test(password);
  const hasSymbol = /[^A-Za-z0-9]/.test(password);

  // Require lowercase, uppercase, and at least one digit or symbol.
  if (!hasLower || !hasUpper || !(hasDigit || hasSymbol)) {
    return {
      valid: false,
      error: 'Password must include uppercase and lowercase letters and at least one number or symbol',
    };
  }

  const normalized = password.toLowerCase().replace(/[^a-z0-9]/g, '');
  if (COMMON_PASSWORDS.has(normalized)) {
    return { valid: false, error: 'Password is too common. Please choose a less guessable password.' };
  }

  return { valid: true };
}

export async function createToken(userId: string, email: string, role: string, tokenVersion: number = 0): Promise<string> {
  const token = await new jose.SignJWT({ sub: userId, email, role, tokenVersion })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setIssuer(JWT_ISSUER)
    .setAudience(JWT_AUDIENCE)
    .setExpirationTime('1d')
    .sign(JWT_SECRET);
  
  return token;
}

// Invalidate all previously issued JWTs for a user (logout-everywhere, password change/reset).
export async function bumpTokenVersion(userId: string): Promise<void> {
  await (db as any)
    .update(users)
    .set({ tokenVersion: sql`${(users as any).tokenVersion} + 1` })
    .where(eq((users as any).id, userId));
}

export async function createRefreshToken(userId: string): Promise<string> {
  const token = await new jose.SignJWT({ sub: userId, type: 'refresh' })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setIssuer(JWT_ISSUER)
    .setExpirationTime('30d')
    .sign(JWT_SECRET);
  
  return token;
}

export async function verifyToken(token: string): Promise<JWTPayload | null> {
  try {
    const { payload } = await jose.jwtVerify(token, JWT_SECRET, {
      issuer: JWT_ISSUER,
      audience: JWT_AUDIENCE,
    });
    return payload as unknown as JWTPayload;
  } catch {
    return null;
  }
}

export async function getAuthUser(c: Context): Promise<any | null> {
  const authHeader = c.req.header('Authorization');
  if (!authHeader?.startsWith('Bearer ')) {
    return null;
  }
  
  const token = authHeader.slice(7);
  const payload = await verifyToken(token);
  
  if (!payload) {
    return null;
  }
  
  // Never load the password hash into request context — it is only needed for
  // explicit password-verification routes that query it separately.
  const user = await dbGet<any>(
    (db as any)
      .select({
        id: (users as any).id,
        email: (users as any).email,
        name: (users as any).name,
        phone: (users as any).phone,
        role: (users as any).role,
        languagePreference: (users as any).languagePreference,
        isClaimed: (users as any).isClaimed,
        googleId: (users as any).googleId,
        rucNumber: (users as any).rucNumber,
        accountStatus: (users as any).accountStatus,
        tokenVersion: (users as any).tokenVersion,
        createdAt: (users as any).createdAt,
        updatedAt: (users as any).updatedAt,
      })
      .from(users)
      .where(eq((users as any).id, payload.sub))
  );

  if (!user) {
    return null;
  }

  // Reject tokens issued before a logout-everywhere / password change
  if ((payload.tokenVersion ?? 0) !== (user.tokenVersion ?? 0)) {
    return null;
  }

  // Suspended/unclaimed accounts must not retain API access via an old JWT
  if (user.accountStatus && user.accountStatus !== 'active') {
    return null;
  }

  return user;
}

export function requireAuth(roles?: string[]) {
  return async (c: Context, next: () => Promise<void>) => {
    const user = await getAuthUser(c);
    
    if (!user) {
      return c.json({ error: 'Unauthorized' }, 401);
    }
    
    if (roles && !roles.includes(user.role)) {
      return c.json({ error: 'Forbidden' }, 403);
    }
    
    c.set('user', user);
    await next();
  };
}

export async function isFirstUser(): Promise<boolean> {
  const result = await dbAll(
    (db as any).select().from(users).limit(1)
  );
  return !result || result.length === 0;
}

/** Fetch only the password hash column (never expose via getAuthUser). */
export async function getUserPasswordHash(userId: string): Promise<string | null> {
  const row = await dbGet<any>(
    (db as any)
      .select({ password: (users as any).password })
      .from(users)
      .where(eq((users as any).id, userId))
  );
  const hash = row?.password;
  return hash && String(hash).length > 0 ? String(hash) : null;
}
