// src/services/tokenService.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { randomUUID } = require('crypto');
const db = require('../db');
const config = require('../config');
// === SECURITY HARDENING: JWT KEY ROTATION ===
const {
  getActiveKey,
  getKeySecret,
  getAllKeys,
  getIssuer,
  getAudience,
  validateTokenClaims,
} = require('./jwtKeys');

const ACCESS_TTL = config.jwtAccessTtl;
const REFRESH_TTL = config.jwtRefreshTtl;
const REFRESH_MAX_IDLE_MS = config.refreshMaxIdleMinutes * 60 * 1000;

// === SECURITY HARDENING: JWT KEY ROTATION ===
// Get active key for signing
const { keyId: ACTIVE_KEY_ID, secret: ACTIVE_SECRET } = getActiveKey();
// For refresh tokens, use a separate key if available, otherwise use active key
const REFRESH_KEY_ID = process.env.JWT_REFRESH_KEY_ID || ACTIVE_KEY_ID;
const REFRESH_SECRET = getKeySecret(REFRESH_KEY_ID) || ACTIVE_SECRET;

function signAccessToken(user, options = {}) {
  // === SECURITY HARDENING: JWT KEY ROTATION ===
  // Include key ID in token header and add standard claims
  const payload = {
    sub: user.id,
    phone_number: user.phone_number,
    role: user.role,
    user_type: user.user_type || null,
    // === SECURITY HARDENING: JWT KEY ROTATION ===
    // Standard JWT claims
    iss: getIssuer(),
    aud: getAudience(),
    iat: Math.floor(Date.now() / 1000),
    // High assurance flag (set after OTP verification for sensitive actions)
    high_assurance: options.highAssurance || false,
  };

  return jwt.sign(
    payload,
    ACTIVE_SECRET,
    {
      expiresIn: ACCESS_TTL,
      // === SECURITY HARDENING: JWT KEY ROTATION ===
      // Include key ID in header for key rotation support
      header: {
        kid: ACTIVE_KEY_ID,
        alg: 'HS256',
      },
    }
  );
}

async function issueRefreshToken({
  userId,
  deviceId,
  userAgent,
  ip,
  rotatedFromId = null,
}) {
  // === SECURITY HARDENING: JWT KEY ROTATION ===
  const tokenId = randomUUID();
  const payload = {
    sub: userId,
    device_id: deviceId,
    jti: tokenId,
    // === SECURITY HARDENING: JWT KEY ROTATION ===
    // Standard JWT claims
    iss: getIssuer(),
    aud: getAudience(),
    iat: Math.floor(Date.now() / 1000),
  };
  
  const token = jwt.sign(
    payload,
    REFRESH_SECRET,
    {
      expiresIn: REFRESH_TTL,
      // === SECURITY HARDENING: JWT KEY ROTATION ===
      // Include key ID in header
      header: {
        kid: REFRESH_KEY_ID,
        alg: 'HS256',
      },
    }
  );
  const decoded = jwt.decode(token);
  const expiresAt = new Date(decoded.exp * 1000);

  await storeRefreshToken({
    userId,
    deviceId,
    token,
    tokenId,
    expiresAt,
    userAgent,
    ip,
    rotatedFromId,
  });

  return token;
}

async function storeRefreshToken({
  userId,
  deviceId,
  token,
  tokenId,
  expiresAt,
  userAgent,
  ip,
  rotatedFromId = null,
}) {
  const tokenHash = await bcrypt.hash(token, 10);

  await db.query(
    `
    INSERT INTO refresh_tokens (
      user_id,
      device_id,
      token_id,
      token_hash,
      user_agent,
      ip_address,
      expires_at,
      rotated_from_id
    )
    VALUES ($1,$2,$3,$4,$5,$6,$7,$8)
    `,
    [
      userId,
      deviceId,
      tokenId,
      tokenHash,
      userAgent || null,
      ip || null,
      expiresAt,
      rotatedFromId,
    ]
  );
}

async function verifyRefreshToken(rawToken) {
  // === SECURITY HARDENING: JWT KEY ROTATION ===
  // Decode token to get key ID from header
  let decoded;
  try {
    decoded = jwt.decode(rawToken, { complete: true });
    if (!decoded || !decoded.header) {
      return null;
    }
  } catch (err) {
    return null;
  }

  // Get secret for the key ID (if present) or try all keys
  const keyId = decoded.header.kid;
  let payload = null;
  let verified = false;

  if (keyId) {
    const secret = getKeySecret(keyId);
    if (secret) {
      try {
        payload = jwt.verify(rawToken, secret);
        verified = true;
      } catch (err) {
        // Key ID specified but verification failed
        return null;
      }
    }
  }

  // If key ID not found or not specified, try all keys (for rotation support)
  if (!verified) {
    const allKeys = getAllKeys();
    for (const [kid, keySecret] of Object.entries(allKeys)) {
      try {
        payload = jwt.verify(rawToken, keySecret);
        verified = true;
        break;
      } catch (err) {
        // Try next key
        continue;
      }
    }
  }

  if (!verified || !payload) {
    return null;
  }

  // === SECURITY HARDENING: JWT KEY ROTATION ===
  // Validate JWT claims (iss, aud, exp, iat, nbf)
  const claimsValidation = validateTokenClaims(payload);
  if (!claimsValidation.valid) {
    return null;
  }

  const { sub: userId, device_id: deviceId, jti: tokenId } = payload;
  if (!tokenId) {
    return null;
  }

  const { rows } = await db.query(
    `SELECT * FROM refresh_tokens WHERE token_id = $1`,
    [tokenId]
  );

  if (rows.length === 0) {
    return null;
  }

  const tokenRow = rows[0];

  if (tokenRow.user_id !== userId || tokenRow.device_id !== deviceId) {
    await handleReuse(tokenRow);
    return { reuseDetected: true };
  }

  const match = await bcrypt.compare(rawToken, tokenRow.token_hash);
  if (!match) {
    await handleReuse(tokenRow);
    return { reuseDetected: true };
  }

  if (tokenRow.revoked_at) {
    await handleReuse(tokenRow);
    return { reuseDetected: true };
  }

  const now = new Date();
  if (tokenRow.expires_at <= now) {
    await revokeToken(tokenRow.id);
    return null;
  }

  if (
    tokenRow.last_used_at &&
    now.getTime() - new Date(tokenRow.last_used_at).getTime() > REFRESH_MAX_IDLE_MS
  ) {
    await revokeToken(tokenRow.id);
    return null;
  }

  await db.query(
    `UPDATE refresh_tokens SET last_used_at = NOW() WHERE id = $1`,
    [tokenRow.id]
  );

  return { userId, deviceId, row: tokenRow };
}

async function rotateRefreshToken({ tokenRow, userAgent, ip }) {
  await revokeToken(tokenRow.id);
  return issueRefreshToken({
    userId: tokenRow.user_id,
    deviceId: tokenRow.device_id,
    userAgent,
    ip,
    rotatedFromId: tokenRow.id,
  });
}

async function revokeRefreshToken(userId, deviceId) {
  await revokeDeviceTokens(userId, deviceId);
}

async function revokeToken(tokenId) {
  await db.query(
    `UPDATE refresh_tokens SET revoked_at = NOW() WHERE id = $1 AND revoked_at IS NULL`,
    [tokenId]
  );
}

async function revokeDeviceTokens(userId, deviceId) {
  await db.query(
    `
    UPDATE refresh_tokens
    SET revoked_at = NOW()
    WHERE user_id = $1 AND device_id = $2 AND revoked_at IS NULL
    `,
    [userId, deviceId]
  );
}

async function handleReuse(tokenRow) {
  await db.query(
    `
    UPDATE refresh_tokens
    SET reuse_detected_at = NOW(),
        revoked_at = COALESCE(revoked_at, NOW())
    WHERE id = $1
    `,
    [tokenRow.id]
  );
  await revokeDeviceTokens(tokenRow.user_id, tokenRow.device_id);
}

module.exports = {
  signAccessToken,
  verifyRefreshToken,
  issueRefreshToken,
  rotateRefreshToken,
  revokeRefreshToken,
};
