import 'dart:developer';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:softplayer_dart_proto/accounts/v1/accounts_v1.pb.dart';
import 'package:softplayer_web/core/api/v1/accounts.dart';
import 'package:softplayer_web/core/api/v1/refresh_session.dart';
import 'package:softplayer_web/core/grpc/grpc_client.dart';
import 'package:softplayer_web/core/tokens/data/token_storage_repository.dart';
import 'package:softplayer_web/features/authorization/application/authorization_application.dart';

class TokenState {
  final String? accessToken;
  final String? refreshToken;
  const TokenState({this.accessToken, this.refreshToken});

  TokenState copyWith({String? accessToken, String? refreshToken}) {
    return TokenState(
      refreshToken: refreshToken ?? this.refreshToken,
      accessToken: accessToken ?? this.accessToken,
    );
  }

  // Get an access token from the state
  String getAccessToken() {
    return accessToken ?? "";
  }

  // Get a refresh token from the state
  String getRefreshToken() {
    return refreshToken ?? "";
  }
}

const accessTokenHeader = "x-access-token";
const refreshTokenHeader = "x-refresh-token";

class Tokens {
  final String accessToken;
  final String refreshToken;

  const Tokens({required this.accessToken, required this.refreshToken});

  factory Tokens.fromProto(TokenPair pair) {
    return Tokens(
      accessToken: pair.accessToken,
      refreshToken: pair.refreshToken,
    );
  }
}

final tokensControllerProvider =
    AsyncNotifierProvider<TokensController, TokenState>(TokensController.new);

class TokensController extends AsyncNotifier<TokenState> {
  @override
  Future<TokenState> build() async {
    final tokenRepo = ref.read(tokenStorageRepositoryProvider);
    final accessToken = await tokenRepo.getAccessToken();
    final refreshToken = await tokenRepo.getRefreshToken();

    // If refresh token is not valid, we just drop the session,
    // even if there is an active access token, and return
    // an empty state
    if (!verifyJWT(refreshToken)) {
      log("Refresh token is not valid, logging out");
      await tokenRepo.clearStorage();
      return TokenState();
    }

    // If access token is not valid, refresh it using the refresh token
    if (verifyJWT(accessToken)) {
      log("Access token is valid");
      return TokenState(refreshToken: refreshToken, accessToken: accessToken);
    }
    log("Only refresh token is valid");
    return TokenState(refreshToken: refreshToken);
  }

  Future<void> resetTokens() async {
    final tokenRepo = ref.read(tokenStorageRepositoryProvider);
    await tokenRepo.clearStorage();
    state = await AsyncValue.guard(() async {
      return TokenState();
    });
  }

  // Store an access token to the storage and save it to the state
  Future<void> writeAccessToken(String token) async {
    final tokenRepo = ref.read(tokenStorageRepositoryProvider);
    await tokenRepo.storeAccessToken(token);
    state = await AsyncValue.guard(() async {
      return state.value!.copyWith(accessToken: token);
    });
  }

  // Store a pair of tokens to the storage and refresh the state
  Future<void> writeTokenPair(Tokens pair) async {
    final tokenRepo = ref.read(tokenStorageRepositoryProvider);
    log("Writing tokens");
    await tokenRepo.storeRefreshToken(pair.refreshToken);
    await tokenRepo.storeAccessToken(pair.accessToken);
    state = await AsyncValue.guard(() async {
      return state.value!.copyWith(
        accessToken: pair.accessToken,
        refreshToken: pair.refreshToken,
      );
    });
  }

  Future<void> checkTokens() async {
    final currentState = state.value;
    if (currentState == null ||
        currentState.accessToken == null ||
        currentState.refreshToken == null) {
      log("Trying to get tokens from the storage");
      state = await AsyncValue.guard(() async {
        final tokenRepo = ref.read(tokenStorageRepositoryProvider);
        return TokenState(
          accessToken: await tokenRepo.getAccessToken(),
          refreshToken: await tokenRepo.getRefreshToken(),
        );
      });
    }
    log("checking tokens");

    final inMemAccessToken = state.value!.getAccessToken();
    // If we have a valid token in memory, nothing must be done
    // TODO: Store the expiration in memory, to make it cheaper to check
    if (verifyJWT(inMemAccessToken)) {
      return;
    } else {
      log("Access it not valid");
      final inMemRefreshToken = state.value!.getRefreshToken();
      if (verifyJWT(inMemRefreshToken)) {
        log("Trying to refresh");
        try {
          final refreshSessionGrpc = ref.read(refreshSessionGrpcProvider);
          log("Trying to refresh");
          final response = await refreshSessionGrpc.refreshSession(
            inMemRefreshToken,
          );
          await writeTokenPair(Tokens.fromProto(response.tokenPair));
          return;
        } catch (e) {
          log(e.toString());
        }
      }
    }
    log("Refrsh it not valid");
    await resetTokens();
    ref.read(authorizationControllerProvider.notifier).logout();
  }

  bool verifyJWT(String? token) {
    if (token == null || token.isEmpty) {
      return false;
    }

    try {
      bool isExpired = JwtDecoder.isExpired(token);
      return !isExpired;
    } catch (_) {
      rethrow;
    }
  }
}
