import 'dart:async';
import 'dart:convert';

import 'package:crypto/crypto.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';

import '../push/push_secure_storage.dart';

class AccountData {
  static const _usernameField = 'username';
  static const _passwordField = 'password';
  // App passwords live in the push-shared (group-scoped) keystore so the iOS
  // Notification Service Extension can authenticate Nextcloud calls too.
  // The talk password authenticates the second (apptype=talk) push
  // registration — Nextcloud binds each push subscription to its session
  // token, so two registrations need two app passwords.
  static const _appPasswordField = 'nextcloud_app_password';
  static const _appPasswordTalkField = 'nextcloud_app_password_talk';
  // Marks accounts whose Nextcloud credentials came from Login Flow v2 (2FA):
  // the real password is not valid against Nextcloud, only the flow-issued
  // app password is — and no further app passwords can be minted silently.
  static const _loginFlowField = 'nextcloud_login_flow';
  // Persists the demo session across cold starts (see DemoMode).
  static const _demoField = 'is_demo';
  // Keeps isPopulated()/getPassword() valid; demo mode never uses a real one.
  static const _demoPasswordPlaceholder = 'demo';

  static const FlutterSecureStorage _secureStorage = FlutterSecureStorage();

  static final AccountData _instance = AccountData._construct();
  Completer<void> _populated = Completer();

  factory AccountData() => _instance;

  AccountData._construct() {
    _migrateAndLoad();
  }

  String? _username;
  String? _password;
  String? _appPassword;
  String? _appPasswordTalk;
  bool _isDemo = false;
  bool _usesLoginFlow = false;

  /// True while the active session is a local demo session (see DemoMode).
  bool get isDemo => _isDemo;

  /// True when the Nextcloud credentials were obtained via Login Flow v2
  /// (browser login, e.g. because the account has two-factor authentication).
  /// In that mode the stored real password only authenticates MarianumConnect;
  /// every Nextcloud call must use the flow-issued app password.
  bool get usesLoginFlow => _usesLoginFlow;

  String getUsername() {
    if (_username == null) throw Exception('Username not initialized');
    return _username!;
  }

  String getPassword() {
    if (_password == null) throw Exception('Password not initialized');
    return _password!;
  }

  String getUserSecret() => sha512
      .convert(utf8.encode('${getUsername()}:${getPassword()}'))
      .toString();

  Future<void> setData(String username, String password) async {
    await _secureStorage.write(key: _usernameField, value: username);
    await _secureStorage.write(key: _passwordField, value: password);
    _username = username;
    _password = password;
    if (!_populated.isCompleted) _populated.complete();
  }

  /// Enters a local demo session for [username] — no real credentials or token;
  /// every backend is served from fixtures while [isDemo] is true (see DemoMode).
  Future<void> setDemo(String username) async {
    await _secureStorage.write(key: _usernameField, value: username);
    await _secureStorage.write(
      key: _passwordField,
      value: _demoPasswordPlaceholder,
    );
    await _secureStorage.write(key: _demoField, value: 'true');
    _username = username;
    _password = _demoPasswordPlaceholder;
    _isDemo = true;
    if (!_populated.isCompleted) _populated.complete();
  }

  Future<void> removeData() async {
    _populated = Completer();
    _username = null;
    _password = null;
    _appPassword = null;
    _appPasswordTalk = null;
    _isDemo = false;
    _usesLoginFlow = false;
    await _secureStorage.delete(key: _usernameField);
    await _secureStorage.delete(key: _passwordField);
    await _secureStorage.delete(key: _demoField);
    await _secureStorage.delete(key: _loginFlowField);
    await _clearAppPasswordStorage();
    await _clearAppPasswordTalkStorage();
  }

  /// Persists a freshly minted Nextcloud app password. After this every
  /// [getBasicAuthHeader] call authenticates with the app password instead of
  /// the real password.
  Future<void> setAppPassword(String appPassword) async {
    _appPassword = appPassword;
    try {
      await pushSecureStorage.write(key: _appPasswordField, value: appPassword);
    } on Object {
      // Group-scoped keystore may be unavailable (e.g. iOS entitlement not yet
      // provisioned). Keeping it in memory still lets this session use it.
    }
  }

  Future<void> clearAppPassword() async {
    _appPassword = null;
    await _clearAppPasswordStorage();
  }

  /// Adopts an app password obtained via Login Flow v2 and switches the
  /// account into flow mode (see [usesLoginFlow]). Any previously stored Talk
  /// app password belonged to the old session era and is dropped — the second
  /// (optional) flow pass stores a fresh one via [setAppPasswordTalk].
  Future<void> setLoginFlow(String appPassword) async {
    await setAppPassword(appPassword);
    await clearAppPasswordTalk();
    _usesLoginFlow = true;
    await _secureStorage.write(key: _loginFlowField, value: 'true');
  }

  bool hasAppPassword() => _appPassword != null && _appPassword!.isNotEmpty;

  /// Persists the app password backing the Talk push registration.
  Future<void> setAppPasswordTalk(String appPassword) async {
    _appPasswordTalk = appPassword;
    try {
      await pushSecureStorage.write(
        key: _appPasswordTalkField,
        value: appPassword,
      );
    } on Object {
      // Group-scoped keystore may be unavailable — in-memory still works for
      // this session, matching setAppPassword.
    }
  }

  Future<void> clearAppPasswordTalk() async {
    _appPasswordTalk = null;
    await _clearAppPasswordTalkStorage();
  }

  bool hasAppPasswordTalk() =>
      _appPasswordTalk != null && _appPasswordTalk!.isNotEmpty;

  Future<void> _clearAppPasswordStorage() async {
    try {
      await pushSecureStorage.delete(key: _appPasswordField);
    } on Object {
      // ignore — nothing stored or keystore unavailable
    }
  }

  Future<void> _clearAppPasswordTalkStorage() async {
    try {
      await pushSecureStorage.delete(key: _appPasswordTalkField);
    } on Object {
      // ignore — nothing stored or keystore unavailable
    }
  }

  Future<void> _migrateAndLoad() async {
    await _migrateFromLegacyStorage();
    _username = await _secureStorage.read(key: _usernameField);
    _password = await _secureStorage.read(key: _passwordField);
    _isDemo = (await _secureStorage.read(key: _demoField)) == 'true';
    _usesLoginFlow = (await _secureStorage.read(key: _loginFlowField)) == 'true';
    try {
      _appPassword = await pushSecureStorage.read(key: _appPasswordField);
      _appPasswordTalk = await pushSecureStorage.read(
        key: _appPasswordTalkField,
      );
    } on Object {
      _appPassword = null;
      _appPasswordTalk = null;
    }
    if (!_populated.isCompleted) _populated.complete();
  }

  // Move credentials from the old SharedPreferences plain-text storage into the
  // platform's secure keystore. Run once per install and clear the legacy keys.
  Future<void> _migrateFromLegacyStorage() async {
    final prefs = await SharedPreferences.getInstance();
    final legacyUsername = prefs.getString(_usernameField);
    final legacyPassword = prefs.getString(_passwordField);
    if (legacyUsername == null || legacyPassword == null) return;

    final hasSecure = (await _secureStorage.read(key: _usernameField)) != null;
    if (!hasSecure) {
      await _secureStorage.write(key: _usernameField, value: legacyUsername);
      await _secureStorage.write(key: _passwordField, value: legacyPassword);
    }
    await prefs.remove(_usernameField);
    await prefs.remove(_passwordField);
  }

  Future<bool> waitForPopulation() async {
    await _populated.future;
    return isPopulated();
  }

  bool isPopulated() => _username != null && _password != null;

  /// Returns the value for an HTTP `Authorization` header using HTTP Basic.
  /// Prefer this over embedding credentials in URLs — error logs and crash
  /// reports often capture the URL but not headers.
  String getBasicAuthHeader() {
    _requirePopulated();
    // Prefer the scoped app password once available; it survives real-password
    // rotation and is what the push-v2 registration is bound to.
    return _basicAuth(_appPassword ?? _password!);
  }

  /// Basic-auth header using the Talk app password — authenticates the
  /// apptype=talk push registration (and its unregister). Throws when the
  /// talk password has not been minted yet; callers treat that as a failed
  /// talk registration and retry on the next start.
  String getTalkBasicAuthHeader() {
    _requirePopulated();
    if (!hasAppPasswordTalk()) {
      // Login-flow account whose second (talk) flow pass was skipped: no
      // silent minting possible, the talk registration shares the single
      // flow-issued credential.
      if (_usesLoginFlow && hasAppPassword()) return _basicAuth(_appPassword!);
      throw StateError('Talk app password not available yet');
    }
    return _basicAuth(_appPasswordTalk!);
  }

  /// Basic-auth header that always uses the real password. Needed exactly once,
  /// to mint the app password via `core/getapppassword` (an app password cannot
  /// mint another).
  String getRealPasswordBasicAuthHeader() {
    _requirePopulated();
    return _basicAuth(_password!);
  }

  /// Secret authenticating against Nextcloud: the app password once available
  /// (minted or flow-issued), otherwise the real password. Mirrors the
  /// preference of [getBasicAuthHeader] for clients that need the raw secret
  /// (WebDAV client construction).
  String getNextcloudSecret() {
    _requirePopulated();
    return _appPassword ?? _password!;
  }

  void _requirePopulated() {
    if (!isPopulated()) {
      throw Exception(
        'AccountData (e.g. username or password) is not initialized!',
      );
    }
  }

  String _basicAuth(String secret) =>
      'Basic ${base64Encode(utf8.encode('$_username:$secret'))}';

  /// Convenience wrapper around [getBasicAuthHeader] returning a single-entry
  /// header map ready to merge into HTTP request headers.
  Map<String, String> authHeaders() => {'Authorization': getBasicAuthHeader()};
}
