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' as fb;
import 'package:logging/logging.dart';

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

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

  late StreamController<User?> controller;
  static final fb.FirebaseAuth _fb = fb.FirebaseAuth.instance;
  static String _verificationId = '';

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

    final fb.PhoneVerificationCompleted verificationCompleted =
        (fb.AuthCredential credential) async {
      fb.UserCredential _authResult;
      try {
        _authResult = await _fb.signInWithCredential(credential);
        print("PhoneVerificationCompleted :$_authResult");
      } 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 fb.PhoneVerificationFailed verificationFailed =
        (fb.FirebaseAuthException authException) async {
      print(
          'Phone number verification failed. Code: ${authException.code}. Message: ${authException.message}');
      completer.completeError(SigninException(
          "Phone number verification failed:${authException.message}"));
    };

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

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

      _verificationId = verificationId;
      if (codeSentCompleted) {
        if (!completer.isCompleted)
          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 fb.AuthCredential credential = fb.PhoneAuthProvider.credential(
          verificationId: _verificationId, smsCode: smsCode);

      await _fb.signInWithCredential(credential);
      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 {
    fb.User? firebaseUser = _fb.currentUser;
    if (firebaseUser == null) return null;
    Map<dynamic, dynamic>? claims =
        await getClaims(refreshIdToken: refreshIdToken);

    log.info("Claims:$claims");
    if (claims == null) return;

    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 FirebaseFirestore.instance
        .collection(user_collection)
        .doc(userID)
        .get();
    if (snap.exists) {
      User user = User.fromMap(snap.data() as Map<String, dynamic>, snap.id);
      return user;
    }
    return null;
  }

  Future<bool> isLogin() async {
    final fb.User? firebaseUser = _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 = FirebaseFirestore.instance
        .collection(config_collection)
        .doc(setting_doc_id)
        .snapshots();

    await for (var snap in snapshot) {
      Setting setting = Setting.fromMap(snap.data() as Map<String, dynamic>);
      yield setting;
    }
  }

  Future<String?> _getCurrentUserID() async {
    fb.User? firebaseUser = _fb.currentUser;
    if (firebaseUser == null) return null;
    Map? claims = await getClaims();
    if (claims == null) return null;
    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 = FirebaseFirestore.instance
        .collection(user_collection)
        .doc(_userID)
        .snapshots();
    userListener = snapshot.listen((snap) async {
      User user = User.fromMap(snap.data() as Map<String, dynamic>, snap.id);

      fb.User? firebaseUser = _fb.currentUser;
      if (firebaseUser == null) {
        userListener?.cancel();
        return;
      }
      try {
        // get privilege from claim
        fb.IdTokenResult idToken = await firebaseUser.getIdTokenResult(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<fb.User?>? authListener;

    Future<void> _start() async {
      authListener = _fb.authStateChanges().listen((firebaseUser) {
        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;
  }
}
