GraphInstagramService:
interface GraphInstagramService {
    @GET("access_token")
    suspend fun getLongAccessTokenInfo(
        @Query("grant_type") grantType: String,
        @Query("client_secret") clientSecret: String,
        @Query("access_token") accessToken: String
    ): LongAccessTokenInfo
    @GET("me")
    suspend fun getUserInfo(
        @Query("fields") fields: String,
        @Query("access_token") accessToken: String
    ): UserInfoResponse
    @GET("me/media")
    suspend fun getMedias(
        @Query("fields") fields: String,
        @Query("access_token") accessToken: String
    ): MediasResponse
    @GET("{albumId}/children")
    suspend fun getAlbumDetail(
        @Path("albumId") albumId: String,
        @Query("fields") fields: String,
        @Query("access_token") accessToken: String
    ): AlbumDetailResponse
    @GET("{mediaId}")
    suspend fun getMediaItem(
        @Path("mediaId") mediaId: String,
        @Query("fields") fields: String,
        @Query("access_token") accessToken: String
    ): Response<Map<String, Any>>
}
DataRepository:
class DataRepository(
    context: Context,
    private val apiInstagramService: ApiInstagramService,
    private val graphInstagramService: GraphInstagramService
) {
    private val preference = SharedPreferencesManagerImpl(context)
    private val TAG = javaClass.name
    private val _accessTokenResult = MutableLiveData<Boolean?>(null)
    val accessTokenResult: LiveData<Boolean?> = _accessTokenResult
    suspend fun getUserInfo(): UserInfoResponse {
        if (preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "").isNullOrEmpty()) {
            throw Exception("TOKEN_EMPTY")
        }
        if (!isTokenValid()) {
            throw Exception("TOKEN_EXPIRED")
        }
        return withContext(Dispatchers.IO) {
            return@withContext graphInstagramService.getUserInfo(
                fields = "id,username,account_type",
                accessToken = preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "")!!
            )
        }
    }
    suspend fun getMedias(): List<Map<String,Any>> {
        if (preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "").isNullOrEmpty()) {
            throw Exception("TOKEN_EMPTY")
        }
        if (!isTokenValid()) {
            throw Exception("TOKEN_EXPIRED")
        }
        return withContext(Dispatchers.IO) {
            return@withContext graphInstagramService.getMedias(
                fields = "id,caption,media_type,timestamp,permalink,media_url,thumbnail_url",
                accessToken = preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "")!!
            ).data
        }
    }
    suspend fun getAlbumDetail(albumId: String): List<Map<String, Any>> {
        if (preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "").isNullOrEmpty()) {
            throw Exception("TOKEN_EMPTY")
        }
        if (!isTokenValid()) {
            throw Exception("TOKEN_EXPIRED")
        }
        return withContext(Dispatchers.IO) {
            return@withContext graphInstagramService.getAlbumDetail(
                albumId = albumId,
                fields = "id,media_type,media_url,timestamp,thumbnail_url",
                accessToken = preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "")!!
            ).data
        }
    }
    suspend fun getMediaItem(mediaId: String): Map<String, Any> {
        if (preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "").isNullOrEmpty()) {
            throw Exception("TOKEN_EMPTY")
        }
        if (!isTokenValid()) {
            throw Exception("TOKEN_EXPIRED")
        }
        return withContext(Dispatchers.IO) {
            return@withContext graphInstagramService.getMediaItem(
                mediaId = mediaId,
                fields = "id,caption,media_type,timestamp,permalink,media_url,thumbnail_url",
                accessToken = preference.getString(Constants.PREF_KEY_ACCESS_TOKEN, "")!!
            ).body() ?: throw Exception("UNKNOWN_EXCEPTION")
        }
    }
    suspend fun logout(): UserInfoResponse {
        return withContext(Dispatchers.IO) {
            preference.clear()
            return@withContext UserInfoResponse("","","")
        }
    }
    suspend fun getAccessToken(
        clientId: String,
        clientSecret: String,
        code: String,
        redirectUri: String
    ) {
        withContext(Dispatchers.IO) {
            getShortAccessToken(
                clientId,
                clientSecret,
                code,
                redirectUri
            )
        }
    }
    private suspend fun getShortAccessToken(
        clientId: String,
        clientSecret: String,
        code: String,
        redirectUri: String
    ) {
        try {
            val shortAccessTokenInfo = apiInstagramService.getShortAccessTokenInfo(
                clientId = clientId,
                clientSecret = clientSecret,
                code = code,
                grantType = "authorization_code",
                redirectUri = redirectUri
            )
            Log.d(TAG, "shortAccessTokenInfo = $shortAccessTokenInfo")
            preference.set(Constants.PREF_KEY_INSTAGRAM_USER_ID, shortAccessTokenInfo.userId)
            val currentTimeMillis: Long = System.currentTimeMillis()
            getLongAccessToken(shortAccessTokenInfo.accessToken, clientSecret, currentTimeMillis)
        } catch (exception: UnknownHostException) { // Request Api when no internet
            exception.printStackTrace()
            Log.e(TAG, "shortAccessTokenInfo exception = $exception")
            _accessTokenResult.postValue(false)
        } catch (exception: Exception) {
            exception.printStackTrace()
            Log.e(TAG, "shortAccessTokenInfo exception = $exception")
            _accessTokenResult.postValue(false)
        }
    }
    private suspend fun getLongAccessToken(
        shortAccessToken: String,
        clientSecret: String,
        currentTimeMillis: Long
    ) {
        try {
            val longAccessTokenInfo = graphInstagramService.getLongAccessTokenInfo(
                grantType = "ig_exchange_token",
                clientSecret = clientSecret,
                accessToken = shortAccessToken
            )
            Log.d(TAG, "longAccessTokenInfo = $longAccessTokenInfo")
            val expiredTimeMillis = currentTimeMillis + longAccessTokenInfo.expiresIn
            Log.d(
                TAG,
                "currentTimeMillis = $currentTimeMillis, expiredTimeMillis = $expiredTimeMillis"
            )
            preference.set(Constants.PREF_KEY_EXPIRED_MILLISECONDS, expiredTimeMillis)
            preference.set(Constants.PREF_KEY_ACCESS_TOKEN, longAccessTokenInfo.accessToken)
            _accessTokenResult.postValue(true)
        } catch (exception: UnknownHostException) { // Request Api when no internet
            exception.printStackTrace()
            Log.e(TAG, "getLongAccessToken exception = $exception")
            _accessTokenResult.postValue(false)
        } catch (exception: Exception) {
            exception.printStackTrace()
            Log.e(TAG, "getLongAccessToken exception = $exception")
            _accessTokenResult.postValue(false)
        }
    }
    fun clear() {
        _accessTokenResult.value = null
    }
    private fun isTokenValid(): Boolean {
        val expiredMilliseconds = preference.getLong(Constants.PREF_KEY_EXPIRED_MILLISECONDS, 0)
        val currentTimeMillis: Long = System.currentTimeMillis()
        return currentTimeMillis < expiredMilliseconds
    }
}