import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { db, schema } from './db';
import { eq, ne, and, sql } from 'drizzle-orm';
import { BCRYPT_ROUNDS_TOKEN, REFRESH_TOKEN_MAX_AGE_MS } from './constants';

// Token configurations
const ACCESS_TOKEN_EXPIRY = '10m'; // 10 minutes

interface TokenPayload {
  userId: string;
  email: string;
  role: string;
  sessionId: string;
}

interface RefreshTokenData {
  sessionId: string;
  tokenFamily: string;
  userId: string;
}

/**
 * Generate a short-lived JWT access token
 */
export function generateAccessToken(payload: TokenPayload): string {
  return jwt.sign(payload, process.env.JWT_ACCESS_SECRET!, {
    expiresIn: ACCESS_TOKEN_EXPIRY,
    issuer: 'scriba',
    audience: 'scriba-client',
  });
}

/**
 * Generate a refresh token and store it in the database
 */
export async function generateRefreshToken(
  userId: string,
  ipAddress?: string,
  userAgent?: string,
  tokenFamily?: string
): Promise<{ refreshToken: string; sessionId: string; tokenFamily: string }> {
  const sessionId = crypto.randomUUID();
  const family = tokenFamily || crypto.randomUUID();
  const rawToken = crypto.randomUUID();
  // Embed sessionId in the token so verifyRefreshToken can look up the session directly
  // rather than scanning the entire sessions table. Format: "<sessionId>.<rawToken>"
  const refreshToken = `${sessionId}.${rawToken}`;

  // Hash only the raw portion for storage
  const refreshTokenHash = await bcrypt.hash(rawToken, BCRYPT_ROUNDS_TOKEN);

  // Store in database
  await db.insert(schema.sessions).values({
    id: sessionId,
    userId,
    refreshTokenHash,
    tokenFamily: family,
    expiresAt: new Date(Date.now() + REFRESH_TOKEN_MAX_AGE_MS),
    ipAddress,
    userAgent,
  });

  return { refreshToken, sessionId, tokenFamily: family };
}

/**
 * Verify and decode an access token
 */
export function verifyAccessToken(token: string): TokenPayload | null {
  try {
    const decoded = jwt.verify(token, process.env.JWT_ACCESS_SECRET!, {
      issuer: 'scriba',
      audience: 'scriba-client',
    }) as TokenPayload;
    
    return decoded;
  } catch (error) {
    return null;
  }
}

/**
 * Verify a refresh token against the database
 */
export async function verifyRefreshToken(refreshToken: string): Promise<RefreshTokenData | null> {
  try {
    // Token format: "<sessionId>.<rawToken>" — extract sessionId for a direct DB lookup
    const dotIndex = refreshToken.indexOf('.');
    if (dotIndex === -1) return null;
    const sessionId = refreshToken.substring(0, dotIndex);
    const rawToken = refreshToken.substring(dotIndex + 1);

    const session = await db.query.sessions.findFirst({
      where: eq(schema.sessions.id, sessionId),
    });

    if (!session || session.revokedAt) return null;

    const isValid = await bcrypt.compare(rawToken, session.refreshTokenHash);
    if (!isValid) return null;

    return {
      sessionId: session.id,
      tokenFamily: session.tokenFamily || 'unknown',
      userId: session.userId,
    };
  } catch (error) {
    console.error('Refresh token verification error:', error);
    return null;
  }
}

/**
 * Revoke a refresh token (and its entire family if specified)
 */
export async function revokeRefreshToken(
  sessionId: string,
  revokeFamily: boolean = false
): Promise<void> {
  if (revokeFamily) {
    // Get the token family first
    const session = await db.query.sessions.findFirst({
      where: eq(schema.sessions.id, sessionId),
    });

    if (session?.tokenFamily) {
      // Revoke all tokens in the same family
      await db
        .update(schema.sessions)
        .set({ revokedAt: new Date() })
        .where(eq(schema.sessions.tokenFamily, session.tokenFamily));
    }
  } else {
    // Revoke only this specific session
    await db
      .update(schema.sessions)
      .set({ revokedAt: new Date() })
      .where(eq(schema.sessions.id, sessionId));
  }
}

/**
 * Rotate refresh token - create new one and revoke old
 */
export async function rotateRefreshToken(
  oldSessionId: string,
  ipAddress?: string,
  userAgent?: string
): Promise<{ refreshToken: string; sessionId: string } | null> {
  try {
    // Get the old session
    const oldSession = await db.query.sessions.findFirst({
      where: eq(schema.sessions.id, oldSessionId),
    });

    if (!oldSession || oldSession.revokedAt) {
      return null;
    }

    // Generate new refresh token
    const { refreshToken, sessionId } = await generateRefreshToken(
      oldSession.userId,
      ipAddress,
      userAgent,
      oldSession.tokenFamily || undefined
    );

    // Mark old token as replaced by new one
    await db
      .update(schema.sessions)
      .set({
        revokedAt: new Date(),
        replacedByToken: sessionId,
      })
      .where(eq(schema.sessions.id, oldSessionId));

    return { refreshToken, sessionId };
  } catch (error) {
    console.error('Refresh token rotation error:', error);
    return null;
  }
}

/**
 * Revoke all sessions for a user (except current one)
 */
export async function revokeAllUserSessions(userId: string, excludeSessionId?: string): Promise<void> {
  const condition = excludeSessionId
    ? and(eq(schema.sessions.userId, userId), ne(schema.sessions.id, excludeSessionId))
    : eq(schema.sessions.userId, userId);

  await db
    .update(schema.sessions)
    .set({ revokedAt: new Date() })
    .where(condition);
}

/**
 * Clean up expired sessions
 */
export async function cleanupExpiredSessions(): Promise<void> {
  await db
    .delete(schema.sessions)
    .where(sql`expires_at < NOW() OR revoked_at < NOW() - INTERVAL '7 days'`);
}
