const redis = require("../redis/init")
const { v4: uuidv4 } = require("uuid")
const { logWarn } = require("../logging")
const env = require("../environment")

interface CreateSession {
  sessionId: string
  tenantId: string
  csrfToken?: string
}

interface Session extends CreateSession {
  userId: string
  lastAccessedAt: string
  createdAt: string
  // make optional attributes required
  csrfToken: string
}

interface SessionKey {
  key: string
}

interface ScannedSession {
  value: Session
}

// a week in seconds
const EXPIRY_SECONDS = 86400 * 7

function makeSessionID(userId: string, sessionId: string) {
  return `${userId}/${sessionId}`
}

export async function getSessionsForUser(userId: string): Promise<Session[]> {
  if (!userId) {
    console.trace("Cannot get sessions for undefined userId")
    return []
  }
  const client = await redis.getSessionClient()
  const sessions: ScannedSession[] = await client.scan(userId)
  return sessions.map(session => session.value)
}

export async function invalidateSessions(
  userId: string,
  opts: { sessionIds?: string[]; reason?: string } = {}
) {
  try {
    const reason = opts?.reason || "unknown"
    let sessionIds: string[] = opts.sessionIds || []
    let sessionKeys: SessionKey[]

    // If no sessionIds, get all the sessions for the user
    if (sessionIds.length === 0) {
      const sessions = await getSessionsForUser(userId)
      sessionKeys = sessions.map(session => ({
        key: makeSessionID(session.userId, session.sessionId),
      }))
    } else {
      // use the passed array of sessionIds
      sessionIds = Array.isArray(sessionIds) ? sessionIds : [sessionIds]
      sessionKeys = sessionIds.map(sessionId => ({
        key: makeSessionID(userId, sessionId),
      }))
    }

    if (sessionKeys && sessionKeys.length > 0) {
      const client = await redis.getSessionClient()
      const promises = []
      for (let sessionKey of sessionKeys) {
        promises.push(client.delete(sessionKey.key))
      }
      if (!env.isTest()) {
        logWarn(
          `Invalidating sessions for ${userId} (reason: ${reason}) - ${sessionKeys
            .map(sessionKey => sessionKey.key)
            .join(", ")}`
        )
      }
      await Promise.all(promises)
    }
  } catch (err) {
    console.error(`Error invalidating sessions: ${err}`)
  }
}

export async function createASession(
  userId: string,
  createSession: CreateSession
) {
  // invalidate all other sessions
  await invalidateSessions(userId, { reason: "creation" })

  const client = await redis.getSessionClient()
  const sessionId = createSession.sessionId
  const csrfToken = createSession.csrfToken ? createSession.csrfToken : uuidv4()
  const key = makeSessionID(userId, sessionId)

  const session: Session = {
    ...createSession,
    csrfToken,
    createdAt: new Date().toISOString(),
    lastAccessedAt: new Date().toISOString(),
    userId,
  }
  await client.store(key, session, EXPIRY_SECONDS)
}

export async function updateSessionTTL(session: Session) {
  const client = await redis.getSessionClient()
  const key = makeSessionID(session.userId, session.sessionId)
  session.lastAccessedAt = new Date().toISOString()
  await client.store(key, session, EXPIRY_SECONDS)
}

export async function endSession(userId: string, sessionId: string) {
  const client = await redis.getSessionClient()
  await client.delete(makeSessionID(userId, sessionId))
}

export async function getSession(
  userId: string,
  sessionId: string
): Promise<Session> {
  if (!userId || !sessionId) {
    throw new Error(`Invalid session details - ${userId} - ${sessionId}`)
  }
  const client = await redis.getSessionClient()
  const session = await client.get(makeSessionID(userId, sessionId))
  if (!session) {
    throw new Error(`Session not found - ${userId} - ${sessionId}`)
  }
  return session
}
