import 'package:comunic/helpers/api_helper.dart';
import 'package:comunic/helpers/preferences_helper.dart';
import 'package:comunic/helpers/websocket_helper.dart';
import 'package:comunic/models/api_request.dart';
import 'package:comunic/models/authentication_details.dart';
import 'package:comunic/models/new_account.dart';
import 'package:comunic/models/res_check_password_reset_token.dart';

/// Account helper
///
/// @author Pierre HUBERT

enum AuthResult {
  SUCCESS,
  TOO_MANY_ATTEMPTS,
  NETWORK_ERROR,
  INVALID_CREDENTIALS
}

enum CreateAccountResult {
  SUCCESS,
  ERROR_TOO_MANY_REQUESTS,
  ERROR_EXISTING_EMAIL,
  ERROR
}

class AccountHelper {
  // Current user ID
  static int _currentUserID = -1;

  /// Checkout whether current user is signed in or not
  ///
  /// Warning : This method MUST BE CALLED AT LEAST ONCE AFTER APP START !!!
  Future<bool> signedIn() async {
    bool signedIn =
        (await PreferencesHelper.getInstance()).getLoginToken() != null;

    // Load current user ID for later use
    if (signedIn && _currentUserID == -1) await _loadCurrentUserID();

    return signedIn;
  }

  /// Sign in user
  Future<AuthResult> signIn(AuthenticationDetails auth) async {
    final request = APIRequest(uri: "account/login");
    request.addString("mail", auth.email);
    request.addString("password", auth.password);

    final response = await APIHelper().exec(request);

    // Handle errors
    if (response.code == 401)
      return AuthResult.INVALID_CREDENTIALS;
    else if (response.code == 429)
      return AuthResult.TOO_MANY_ATTEMPTS;
    else if (response.code != 200) return AuthResult.NETWORK_ERROR;

    // Save login token
    await (await PreferencesHelper.getInstance())
        .setLoginToken(response.getObject()["token"]);

    // Get current user ID
    final userID = await _downloadCurrentUserID();
    if (userID == null) {
      await signOut(); // We can not stay signed in without current user ID
      return AuthResult.NETWORK_ERROR;
    }

    // Save current user ID
    final preferences = await PreferencesHelper.getInstance();
    await preferences.setInt(PreferencesKeyList.USER_ID, userID);
    _currentUserID = userID;

    return AuthResult.SUCCESS;
  }

  /// Sign out user
  Future<void> signOut() async {
    await APIRequest.withLogin("account/logout").exec();

    final preferencesHelper = await PreferencesHelper.getInstance();
    await preferencesHelper.setLoginToken(null);
    await preferencesHelper.setInt(PreferencesKeyList.USER_ID, -1);
    _currentUserID = 0;

    // Close current web socket
    WebSocketHelper.close();
  }

  /// Create a new user account
  Future<CreateAccountResult> createAccount(NewAccount info) async {
    final response = await APIRequest(
      uri: "account/create",
      needLogin: false,
      args: {
        "firstName": info.firstName,
        "lastName": info.lastName,
        "emailAddress": info.email,
        "password": info.password,
      },
    ).exec();

    switch (response.code) {
      case 200:
        return CreateAccountResult.SUCCESS;

      case 409:
        return CreateAccountResult.ERROR_EXISTING_EMAIL;

      case 429:
        return CreateAccountResult.ERROR_TOO_MANY_REQUESTS;

      default:
        return CreateAccountResult.ERROR;
    }
  }

  /// Check out whether a given email address exists or not
  ///
  /// Throws in case of failure
  static Future<bool> existsMailAccount(String email) async =>
      (await APIRequest.withoutLogin("account/exists_email")
              .addString("email", email)
              .execWithThrow())
          .getObject()["exists"];

  /// Get current user email address
  static Future<String> getCurrentAccountEmailAddress() async =>
      (await APIRequest.withLogin("account/mail")
          .execWithThrowGetObject())["mail"];

  /// Check out whether security questions have been set for an account or not
  ///
  /// Throws in case of failure
  static Future<bool> hasSecurityQuestions(String email) async =>
      (await APIRequest.withoutLogin("account/has_security_questions")
              .addString("email", email)
              .execWithThrow())
          .getObject()["defined"];

  /// Get the security questions of the user
  ///
  /// Throws in case of failure
  static Future<List<String>> getSecurityQuestions(String email) async =>
      ((await APIRequest.withoutLogin("account/get_security_questions")
                  .addString("email", email)
                  .execWithThrow())
              .getObject()["questions"])
          .cast<String>();

  /// Validate given security answers
  ///
  /// Throws an [Exception] in case of failure
  ///
  /// Returns a password reset token in case of success
  static Future<String> checkAnswers(
          String email, List<String> answers) async =>
      (await APIRequest.withoutLogin("account/check_security_answers")
              .addString("email", email)
              .addString("answers",
                  answers.map((f) => Uri.encodeComponent(f)).join("&"))
              .execWithThrow())
          .getObject()["reset_token"];

  /// Check a password reset token
  ///
  /// Throws in case failure
  static Future<ResCheckPasswordToken> validatePasswordResetToken(
      String token) async {
    final response =
        await APIRequest.withoutLogin("account/check_password_reset_token")
            .addString("token", token)
            .execWithThrowGetObject();

    return ResCheckPasswordToken(
      firstName: response["first_name"],
      lastName: response["last_name"],
      email: response["mail"],
    );
  }

  /// Change account password using password reset token
  ///
  /// Throws an exception in case of failure
  static Future<void> changeAccountPassword(
          String token, String password) async =>
      await APIRequest.withoutLogin("account/reset_user_passwd")
          .addString("token", token)
          .addString("password", password)
          .execWithThrow();

  /// Get current user ID from the server
  Future<int> _downloadCurrentUserID() async {
    final response = await APIRequest.withLogin("account/id").exec();

    if (response.code != 200) return null;

    return response.getObject()["userID"];
  }

  /// Get the ID of the currently signed in user
  Future<void> _loadCurrentUserID() async {
    final preferences = await PreferencesHelper.getInstance();
    _currentUserID = preferences.getInt(PreferencesKeyList.USER_ID);
  }

  /// Check if current user ID is loaded or not
  static bool get isUserIDLoaded => _currentUserID > 0;

  /// Get the ID of the currently signed in user
  static int getCurrentUserID() {
    if (_currentUserID == -1) throw "Current user ID has not been loaded yet!";
    return _currentUserID;
  }

  /// Disconnect all the devices of the current user
  ///
  /// Throws in case of failure
  static Future<void> disconnectAllDevices() async {
    await APIRequest(uri: "account/disconnect_all_devices", needLogin: true)
        .execWithThrow();
  }

  /// Remove permanently a user account
  ///
  /// Throws in case of failure
  static Future<void> deleteAccount(String password) async {
    await APIRequest(uri: "account/delete", needLogin: true)
        .addString("password", password)
        .execWithThrow();
  }
}