import 'dart:async';

import 'package:flutter/material.dart';

import 'package:share_plus/share_plus.dart';

import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/signup.dart';
import 'package:pshared/api/requests/login_data.dart';
import 'package:pshared/api/responses/error/server.dart';
import 'package:pshared/api/responses/verification/response.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/models/account/account.dart';
import 'package:pshared/models/auth/login_outcome.dart';
import 'package:pshared/models/auth/probe_result.dart';
import 'package:pshared/models/auth/pending_login.dart';
import 'package:pshared/models/auth/state.dart';
import 'package:pshared/models/describable.dart';
import 'package:pshared/models/storable.dart';
import 'package:pshared/provider/locale.dart';
import 'package:pshared/provider/resource.dart';
import 'package:pshared/service/account.dart';
import 'package:pshared/service/authorization/service.dart';
import 'package:pshared/service/verification.dart';
import 'package:pshared/utils/exception.dart';


class AccountProvider extends ChangeNotifier {
  AccountProvider();

  static String get currentUserRef => Constants.nilObjectRef;

  /// Auth lifecycle state to avoid multiple ad-hoc flags.
  AuthState _authState = AuthState.idle;
  AuthState get authState => _authState;

  // The resource now wraps our Account? state along with its loading/error state.
  Resource<Account?> _resource = Resource(data: null);
  Resource<Account?> get resource => _resource;
  late LocaleProvider _localeProvider;
  PendingLogin? _pendingLogin;

  Account? get account => _resource.data;
  PendingLogin? get pendingLogin => _pendingLogin;
  bool get isLoggedIn => account != null;
  bool get isLoading => _resource.isLoading;
  Object? get error => _resource.error;
  bool get isReady => (!isLoading) && (account != null);

  Account? currentUser() {
    final acc = account;
    if (acc == null) return null;
    return Account(
      storable: newStorable(
        id: currentUserRef,
        createdAt: acc.createdAt,
        updatedAt: acc.updatedAt,
      ),
      describable: acc.describable,
      lastName: acc.lastName,
      avatarUrl: acc.avatarUrl,
      login: acc.login,
      locale: acc.locale,
    );
  }

  @protected
  Future<void> onAccountChanged(Account? previous, Account? current) => Future<void>.value();

  void _setResource(Resource<Account?> newResource) {
    final previousAccount = _resource.data;
    _resource = newResource;
    final currentAccount = newResource.data;

    if (previousAccount != currentAccount) {
      unawaited(onAccountChanged(previousAccount, currentAccount));
    }

    notifyListeners();
  }

  void updateProvider(LocaleProvider localeProvider) => _localeProvider = localeProvider; 

  void _pickupLocale(String locale) => _localeProvider.setLocale(Locale(locale));

  Future<LoginOutcome> login({
    required String email,
    required String password,
    required String locale,
  }) async {
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      final outcome = await AccountService.login(LoginData.build(
        login: email,
        password: password,
        locale: locale,
      ));
      if (outcome.isCompleted) {
        _authState = AuthState.ready;
        _setResource(Resource(data: outcome.account, isLoading: false));
        _pickupLocale(outcome.account!.locale);
      } else {
        final pending = outcome.pending;
        if (!outcome.isPending || pending == null) {
          throw StateError('Pending login data is missing');
        }
        final confirmation = await VerificationService.requestLoginCode(pending, target: email);
        _pendingLogin = _applyConfirmationMeta(pending, confirmation);
        _authState = AuthState.idle;
        _setResource(_resource.copyWith(isLoading: false));
      }
      return outcome;
    } catch (e) {
      _authState = AuthState.error;
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  PendingLogin _applyConfirmationMeta(
    PendingLogin pending, 
    VerificationResponse confirmation,
  ) {
    final ttlSeconds = confirmation.ttlSeconds != 0 ? confirmation.ttlSeconds : pending.ttlSeconds;
    final target = confirmation.target.isNotEmpty ? confirmation.target : pending.target;
    final cooldownSeconds = confirmation.cooldownSeconds;

    return pending.copyWith(
      ttlSeconds: ttlSeconds,
      target: target,
      cooldownSeconds: cooldownSeconds > 0 ? cooldownSeconds : null,
      cooldownUntil: cooldownSeconds > 0 ? DateTime.now().add(confirmation.cooldownDuration) : null,
      clearCooldown: cooldownSeconds <= 0,
      idempotencyKey: confirmation.idempotencyKey,
    );
  }

  void updatePendingLogin(VerificationResponse confirmation) {
    final pending = _pendingLogin;
    if (pending == null) return;
    _pendingLogin = _applyConfirmationMeta(pending, confirmation);
    notifyListeners();
  }

  void completePendingLogin(Account account) {
    _pendingLogin = null;
    _authState = AuthState.ready;
    _setResource(Resource(data: account, isLoading: false, error: null));
    _pickupLocale(account.locale);
  }

  Future<bool> isAuthorizationStored() async => AuthorizationService.isAuthorizationStored();

  Future<Account?> restore() async {
    _authState = AuthState.checking;
    notifyListeners();
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      final acc = await AccountService.restore();
      _authState = AuthState.ready;
      _setResource(Resource(data: acc, isLoading: false));
      _pickupLocale(acc.locale);
      return acc;
    } catch (e) {
      _authState = AuthState.error;
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> signup({
    required AccountData account,
    required Describable organization,
    required Describable ownerRole,
    required Describable cryptoWallet,
    required Describable ledgerWallet,
    required String timezone,
  }) async {
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      await AccountService.signup(
        SignupRequest.build(
          account: account,
          organization: organization,
          organizationTimeZone: timezone,
          ownerRole: ownerRole,
          cryptoWallet: cryptoWallet,
          ledgerWallet: ledgerWallet,
        ),
      );
      // Signup might not automatically log in the user,
      // so we just mark the request as complete.
      _setResource(_resource.copyWith(isLoading: false));
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> resendVerificationEmail(String email) async {
    try {
      await AccountService.resendVerificationEmail(email);
    } catch (e) {
      _setResource(_resource.copyWith(error: toException(e)));
      rethrow;
    }
  }

  Future<void> logout() async {
    _authState = AuthState.empty;
    _setResource(_resource.copyWith(isLoading: true, error: null));
    _pendingLogin = null;
    try {
      await AccountService.logout();
      _setResource(Resource(data: null, isLoading: false));
    } catch (e) {
      _authState = AuthState.error;
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<Account?> update({
    Describable? describable,
    String? lastName,
    String? locale,
    String? avatarUrl,
    String? notificationFrequency,
  }) async {
    if (account == null) throw ErrorUnauthorized();
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      final updated = await AccountService.update(
        account!.copyWith(
          describable: describable,
          lastName: lastName,
          avatarUrl: () => avatarUrl ?? account!.avatarUrl,
          locale: locale ?? account!.locale,
        ),
      );
      _setResource(Resource(data: updated, isLoading: false));
      return updated;
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> changePassword(String oldPassword, String newPassword) async {
    if (account == null) throw ErrorUnauthorized();
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      final updated = await AccountService.changePassword(oldPassword, newPassword);
      _setResource(Resource(data: updated, isLoading: false));
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<Account?> uploadAvatar(XFile avatarFile) async {
    if (account == null) throw ErrorUnauthorized();
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      final avatarUrl = await AccountService.uploadAvatar(account!.id, avatarFile);
      // Reuse the update method to update the avatar URL.
      return update(avatarUrl: avatarUrl);
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<Account?> resetUsername(String userName, {String? lastName}) async {
    if (account == null) throw ErrorUnauthorized();
    return update(
      describable: account!.describable.copyWith(name: userName),
      lastName: lastName ?? account!.lastName,
    );
  }

  Future<void> forgotPassword(String email) async {
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      await AccountService.forgotPassword(email);
      _setResource(_resource.copyWith(isLoading: false));
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> resetPassword(String accountId, String token, String newPassword) async {
    _setResource(_resource.copyWith(isLoading: true, error: null));
    try {
      await AccountService.resetPassword(accountId, token, newPassword);
      _setResource(_resource.copyWith(isLoading: false));
    } catch (e) {
      _setResource(_resource.copyWith(isLoading: false, error: toException(e)));
      rethrow;
    }
  }

  Future<void> restoreIfPossible() async {
    if (_authState == AuthState.checking || _authState == AuthState.ready) return;
    final hasAuth = await AuthorizationService.isAuthorizationStored();
    if (!hasAuth) {
      _authState = AuthState.empty;
      notifyListeners();
      return;
    }
    await restore();
  }

  Future<AuthProbeResult> probeAuthorization({
    required String email,
    required String password,
    required String locale,
  }) async {
    try {
      final outcome = await AccountService.login(LoginData.build(
        login: email,
        password: password,
        locale: locale,
      ));
      if (outcome.isCompleted) {
        await AuthorizationService.logout();
        return AuthProbeResult.authorized;
      }
      if (outcome.isPending) {
        return AuthProbeResult.authorized;
      }
      return AuthProbeResult.error;
    } catch (e) {
      if (e is ErrorResponse && e.error == 'account_not_verified') {
        return AuthProbeResult.notVerified;
      }
      return AuthProbeResult.error;
    }
  }
}
