import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:fcs/domain/constants.dart';
import 'package:fcs/domain/entities/auth_result.dart' as fcs;
import 'package:fcs/domain/entities/auth_status.dart';
import 'package:fcs/domain/entities/setting.dart';
import 'package:fcs/domain/entities/user.dart';
import 'package:fcs/domain/exceiptions/signin_exception.dart';
import 'package:fcs/helpers/api_helper.dart';
import 'package:fcs/helpers/firebase_helper.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:logging/logging.dart';

class AuthFb {
  final log = Logger('AuthFb');

  static final AuthFb instance = AuthFb._();
  AuthFb._();

  StreamController<User> controller;
  static final FirebaseAuth _fb = FirebaseAuth.instance;
  static String _verificationId;

  Future<fcs.AuthResult> sendSmsCodeToPhoneNumber(String phoneNumber) {
    Completer<fcs.AuthResult> completer = Completer();
    bool codeSentCompleted = false;

    final PhoneVerificationCompleted verificationCompleted =
        (AuthCredential credential) async {
      AuthResult _authResult;
      try {
        _authResult = await _fb.signInWithCredential(credential);
        print("PhoneVerificationCompleted :$_authResult");
        if (_authResult == null) {
          throw SigninException("Sigin error!");
        }
      } catch (e) {
        print("Exception:$e");
        // throw e;
        completer.completeError(SigninException(e.toString()));
        return;
      }
      fcs.AuthResult auth =
          fcs.AuthResult(authStatus: AuthStatus.AUTH_VERIFIED);
      completer.complete(auth);
      print(
          'Inside _sendCodeToPhoneNumber: signInWithPhoneNumber auto succeeded: ${_authResult.user}');
    };

    final PhoneVerificationFailed verificationFailed =
        (AuthException authException) async {
      print(
          'Phone number verification failed. Code: ${authException.code}. Message: ${authException.message}');
      completer.completeError(SigninException(
          "Phone number verification failed:${authException.message}"));
    };

    final PhoneCodeSent codeSent =
        (String verificationId, [int forceResendingToken]) async {
      _verificationId = verificationId;
      print("codeSent  " + phoneNumber);
      codeSentCompleted = true;
      completer.complete(fcs.AuthResult(authStatus: AuthStatus.SMS_SENT));
    };

    final PhoneCodeAutoRetrievalTimeout codeAutoRetrievalTimeout =
        (String verificationId) {
      print("codeAutoRetrievalTimeout $verificationId ");

      _verificationId = verificationId;
      if (codeSentCompleted) {
        completer.complete(fcs.AuthResult(authStatus: AuthStatus.SMS_SENT));
      } else {
        completer.completeError(SigninException("SMS code failed"));
      }
    };

    _fb.verifyPhoneNumber(
        phoneNumber: phoneNumber,
        timeout: const Duration(seconds: 0),
        verificationCompleted: verificationCompleted,
        verificationFailed: verificationFailed,
        codeSent: codeSent,
        codeAutoRetrievalTimeout: codeAutoRetrievalTimeout);

    return completer.future;
  }

  Future<fcs.AuthResult> signInWithPhoneNumber(String smsCode) async {
    try {
      final AuthCredential credential = PhoneAuthProvider.getCredential(
        verificationId: _verificationId,
        smsCode: smsCode,
      );
      AuthResult _authResult = await _fb.signInWithCredential(credential);
      if (_authResult == null) {
        throw SigninException("Sigin error!");
      }
      await _addUserToStream(refreshIdToken: true);
    } on Exception catch (e) {
      return Future.error(SigninException(e.toString()));
    }
    return Future.value(fcs.AuthResult(authStatus: AuthStatus.AUTH_VERIFIED));
  }

  Future<void> signout() async {
    if (userListener != null) await userListener.cancel();
    return _fb.signOut();
  }

  Future<void> _addUserToStream({bool refreshIdToken = false}) async {
    FirebaseUser firebaseUser = await _fb.currentUser();
    if (firebaseUser == null) return null;
    Map claims = await getClaims(refreshIdToken: refreshIdToken);

    log.info("Claims:$claims");

    String cid = claims["cid"];
    User user;
    if (cid != null && cid != "") {
      user = await _getUserFromFirestore(cid);
    }
    if (user == null) {
      controller.add(null);
      return;
    }

    // add privileges
    String privileges = claims["pr"];
    if (privileges != null && privileges != "") {
      user.privileges = privileges.split(":").toList();
    } else {
      user.privileges = [];
    }
    controller.add(user);
  }

  Future<User> _getUserFromFirestore(String userID) async {
    DocumentSnapshot snap = await Firestore.instance
        .collection(user_collection)
        .document(userID)
        .get();
    if (snap.exists) {
      User user = User.fromMap(snap.data, snap.documentID);
      return user;
    }
    return null;
  }

  Future<bool> isLogin() async {
    final FirebaseUser firebaseUser = await _fb.currentUser();
    return Future.value(firebaseUser != null);
  }

  Future<void> signup(String userName) async {
    await requestAPI("/signup", "POST",
        payload: {
          'user_name': userName,
        },
        token: await getToken());
    await _addUserToStream(refreshIdToken: true);
    _startUserListener();
    return;
  }

  Future<void> joinInvite(String userName) async {
    await requestAPI("/join_invite", "POST",
        payload: {
          'user_name': userName,
        },
        token: await getToken());
    // refresh token once signup
    await _addUserToStream(refreshIdToken: true);
    _startUserListener();
    return;
  }

  Future<bool> hasInvite() async {
    var invited =
        await requestAPI("/check_invitation", "GET", token: await getToken());
    return invited["invited"];
  }

  Future<void> updateProfileName(String newUserName) async {
    return await requestAPI("/profile", "PUT",
        payload: {"user_name": newUserName}, token: await getToken());
  }

  Future<void> updatePreferredCurrency(String currency) async {
    return await requestAPI("/currency", "PUT",
        payload: {"preferred_currency": currency}, token: await getToken());
  }

  Stream<Setting> settings() async* {
    Stream<DocumentSnapshot> snapshot = Firestore.instance
        .collection(config_collection)
        .document(setting_doc_id)
        .snapshots();

    await for (var snap in snapshot) {
      Setting setting = Setting.fromMap(snap.data);
      yield setting;
    }
  }

  Future<String> _getCurrentUserID() async {
    FirebaseUser firebaseUser = await _fb.currentUser();
    if (firebaseUser == null) return null;
    Map claims = await getClaims();
    String cid = claims["cid"];
    return cid;
  }

  Future<void> _startUserListener() async {
    if (userListener != null) userListener.cancel();
    String _userID = await _getCurrentUserID();
    if (_userID == null) {
      return;
    }

    Stream<DocumentSnapshot> snapshot = Firestore.instance
        .collection(user_collection)
        .document(_userID)
        .snapshots();
    userListener = snapshot.listen((snap) async {
      User user = User.fromMap(snap.data, snap.documentID);

      FirebaseUser firebaseUser = await _fb.currentUser();
      if (firebaseUser == null) {
        userListener.cancel();
        return;
      }
      try {
        // get privilege from claim
        IdTokenResult idToken = await firebaseUser.getIdToken(refresh: true);
        String privileges = idToken.claims["pr"];
        if (privileges != null && privileges != "") {
          user.privileges = privileges.split(":").toList();
        }
        controller.add(user);
      } catch (e) {
        controller.add(null);
      }
    });
  }

  StreamSubscription<DocumentSnapshot> userListener;
  Stream<User> user() {
    // ignore: close_sinks
    StreamSubscription<FirebaseUser> authListener;

    Future<void> _start() async {
      authListener = _fb.onAuthStateChanged.listen((firebaseUser) async {
        if (firebaseUser == null) {
          controller.add(null);
        } else {
          _addUserToStream(refreshIdToken: true);
          _startUserListener();
        }
      });
    }

    void _stop() {
      if (userListener != null) {
        userListener.cancel();
      }
      if (authListener != null) {
        authListener.cancel();
      }
    }

    controller = StreamController<User>(
        onListen: _start, onPause: _stop, onResume: _start, onCancel: _stop);

    return controller.stream;
  }
}
