import 'dart:async';
import 'package:dio/dio.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import '../api/api_client.dart';
import '../api/api_client_provider.dart';
import 'auth_bridge.dart';
import 'token_storage.dart';

part 'auth_notifier.g.dart';

// Error envelope — carries the user-facing message plus structured details
// (error code, optional retry_after_seconds) so screens can gate CTAs after
// rate-limit responses without re-parsing the message string.
class AuthErrorInfo {
  final String message;
  final String? code;
  final int? retryAfterSeconds;

  const AuthErrorInfo(this.message, {this.code, this.retryAfterSeconds});

  @override
  String toString() => message;
}

// States

sealed class AuthData {
  const AuthData();
}

class AuthInitialData extends AuthData {
  const AuthInitialData();
}

/// Signed-in with a server-generated anonymous customer (no identity yet).
class AuthAnonymousData extends AuthData {
  final Map<String, dynamic> profile;
  const AuthAnonymousData(this.profile);

  String get customerId => profile['id'] as String;
  String get displayName => (profile['display_name'] as String?) ?? '';
}

/// Signed-in with a real identity (phone / google / apple).
class AuthAuthenticatedData extends AuthData {
  final Map<String, dynamic> profile;
  const AuthAuthenticatedData(this.profile);
}

/// Phone OTP requested; waiting for the code.
class AuthOtpSentData extends AuthData {
  final String otpRequestId;
  final String? channelUsed;
  const AuthOtpSentData(this.otpRequestId, {this.channelUsed});
}

/// Anonymity has been disabled by admin — user must identify before continuing.
class AuthForceRegisterData extends AuthData {
  final Map<String, dynamic> profile;
  const AuthForceRegisterData(this.profile);

  String get customerId => profile['id'] as String;
  String get displayName => (profile['display_name'] as String?) ?? '';
}

/// Authenticated but display_name is empty (e.g. Apple withheld it) —
/// user must set one before chatting.
class AuthNeedsDisplayNameData extends AuthData {
  final Map<String, dynamic> profile;
  const AuthNeedsDisplayNameData(this.profile);
}

// Notifier

@Riverpod(keepAlive: true)
class Auth extends _$Auth {
  final _storage = TokenStorage();

  ApiClient get _apiClient => ref.read(apiClientProvider);
  AuthBridge get _bridge => ref.read(authBridgeProvider);

  @override
  FutureOr<AuthData> build() async {
    _bridge.refresh = _refreshForBridge;
    _bridge.onUnauthenticated = () {
      _bridge.clear();
      state = const AsyncData(AuthInitialData());
    };

    final profile = await _refreshFromStorage();
    if (profile == null) return const AuthInitialData();
    return await _stateForProfile(profile);
  }

  // ---------------- Refresh / bootstrap ----------------

  Future<Map<String, dynamic>?> _refreshFromStorage() async {
    final refreshToken = await _storage.readRefreshToken();
    if (refreshToken == null) return null;

    try {
      final response = await _apiClient.postRaw(
        '/api/shared/auth/refresh',
        data: {'refresh_token': refreshToken},
        skipAuth: true,
      );
      return _applyTokens(response);
    } catch (_) {
      await _storage.clear();
      _bridge.clear();
      final current = state.valueOrNull;
      if (current is AuthAuthenticatedData ||
          current is AuthAnonymousData ||
          current is AuthForceRegisterData ||
          current is AuthNeedsDisplayNameData) {
        state = const AsyncData(AuthInitialData());
      }
      return null;
    }
  }

  Future<String?> _refreshForBridge() async {
    final profile = await _refreshFromStorage();
    if (profile == null) return null;
    // Keep the notifier state in sync if we were already signed in.
    final current = state.valueOrNull;
    if (current != null && current is! AuthInitialData && current is! AuthOtpSentData) {
      state = AsyncData(await _stateForProfile(profile));
    }
    return _bridge.accessToken;
  }

  /// Persists refresh token, updates the access token on the bridge, and
  /// returns the profile. Shared by bootstrap, OTP verify, social sign-in.
  Future<Map<String, dynamic>> _applyTokens(Map<String, dynamic> response) async {
    final data = response['data'] as Map<String, dynamic>;
    final access = data['access_token'] as String;
    final refresh = data['refresh_token'] as String;
    final profile = data['profile'] as Map<String, dynamic>;

    await _storage.writeRefreshToken(refresh);
    _bridge.setAccessToken(access);
    return profile;
  }

  /// Decides which AuthData variant matches the current customer row,
  /// consulting server config for the anonymity toggle.
  Future<AuthData> _stateForProfile(Map<String, dynamic> profile) async {
    final hasIdentity = _hasIdentity(profile);
    if (hasIdentity) {
      final displayName = profile['display_name'] as String?;
      if (displayName == null || displayName.isEmpty) {
        return AuthNeedsDisplayNameData(profile);
      }
      return AuthAuthenticatedData(profile);
    }
    // Anonymous — check if platform allows it.
    try {
      final config = await _apiClient.get('/api/shared/config/anonymity');
      final anonymityEnabled = config['data']['anonymity_enabled'] as bool? ?? true;
      if (!anonymityEnabled) return AuthForceRegisterData(profile);
    } catch (_) {
      // On config fetch failure, default to allowing anonymity (best UX).
    }
    return AuthAnonymousData(profile);
  }

  bool _hasIdentity(Map<String, dynamic> profile) {
    return (profile['phone'] as String?) != null ||
        (profile['google_sub'] as String?) != null ||
        (profile['apple_sub'] as String?) != null;
  }

  String? _currentAnonymousCustomerId() {
    final current = state.valueOrNull;
    if (current is AuthAnonymousData) return current.customerId;
    if (current is AuthForceRegisterData) return current.customerId;
    return null;
  }

  // ---------------- Anonymous ----------------

  Future<void> loginAnonymous(String displayName) async {
    state = const AsyncLoading();
    try {
      final anon = await _apiClient.postRaw(
        '/api/shared/auth/anonymous',
        skipAuth: true,
      );
      final profile = await _applyTokens(anon);

      // Patch display name if user chose one (anonymous endpoint auto-generates).
      final trimmed = displayName.trim();
      Map<String, dynamic> finalProfile = profile;
      if (trimmed.isNotEmpty && trimmed != profile['display_name']) {
        try {
          final patch = await _apiClient.patch(
            '/api/client/auth/profile',
            data: {'display_name': trimmed},
          );
          finalProfile = patch['data'] as Map<String, dynamic>;
        } catch (_) {
          // Keep the server-generated name if patch fails.
        }
      }

      state = AsyncData(await _stateForProfile(finalProfile));
    } catch (_) {
      state = AsyncError('Gagal masuk sebagai tamu. Coba lagi.', StackTrace.current);
    }
  }

  // ---------------- Phone OTP ----------------

  Future<void> requestOtp(String phone, {String? channel}) async {
    state = const AsyncLoading();
    try {
      final response = await _apiClient.postRaw(
        '/api/client/auth/otp/request',
        data: {
          'phone': phone,
          if (channel != null) 'channel': channel,
        },
        skipAuth: true,
      );
      final data = response['data'] as Map<String, dynamic>;
      state = AsyncData(AuthOtpSentData(
        data['otp_request_id'] as String,
        channelUsed: data['channel_used'] as String?,
      ));
    } on DioException catch (e) {
      state = AsyncError(_otpRequestErrorInfo(e), StackTrace.current);
    } catch (_) {
      state = AsyncError(
        const AuthErrorInfo('Gagal mengirim OTP. Coba lagi.'),
        StackTrace.current,
      );
    }
  }

  Future<void> verifyOtp(String otpRequestId, String code) async {
    state = const AsyncLoading();
    try {
      final response = await _apiClient.postRaw(
        '/api/client/auth/otp/verify',
        data: {
          'otp_request_id': otpRequestId,
          'code': code,
          if (_currentAnonymousCustomerId() != null)
            'anonymous_customer_id': _currentAnonymousCustomerId(),
        },
        skipAuth: true,
      );
      final profile = await _applyTokens(response);
      state = AsyncData(await _stateForProfile(profile));
    } on DioException catch (e) {
      state = AsyncError(_otpVerifyErrorInfo(e), StackTrace.current);
    } catch (_) {
      state = AsyncError(
        const AuthErrorInfo('Gagal verifikasi. Coba lagi.'),
        StackTrace.current,
      );
    }
  }

  // ---------------- Google ----------------

  Future<void> loginGoogle() async {
    state = const AsyncLoading();
    try {
      final googleUser = await GoogleSignIn().signIn();
      if (googleUser == null) {
        // User cancelled — revert to previous state.
        final previous = _currentAnonymousCustomerId();
        if (previous != null) {
          // We were anonymous; rebuild state from the stored refresh token to
          // get the right variant (Anonymous vs ForceRegister).
          final profile = await _refreshFromStorage();
          if (profile != null) {
            state = AsyncData(await _stateForProfile(profile));
            return;
          }
        }
        state = const AsyncData(AuthInitialData());
        return;
      }
      final googleAuth = await googleUser.authentication;
      final idToken = googleAuth.idToken;
      if (idToken == null) {
        state = AsyncError('Gagal mendapatkan kredensial Google.', StackTrace.current);
        return;
      }
      final response = await _apiClient.postRaw(
        '/api/client/auth/google',
        data: {
          'id_token': idToken,
          if (_currentAnonymousCustomerId() != null)
            'anonymous_customer_id': _currentAnonymousCustomerId(),
        },
        skipAuth: true,
      );
      final profile = await _applyTokens(response);
      state = AsyncData(await _stateForProfile(profile));
    } on DioException catch (e) {
      state = AsyncError(_socialSignInMessage(e), StackTrace.current);
    } catch (_) {
      state = AsyncError('Gagal masuk dengan Google.', StackTrace.current);
    }
  }

  // ---------------- Apple ----------------

  Future<void> loginApple() async {
    state = const AsyncLoading();
    try {
      final credential = await SignInWithApple.getAppleIDCredential(
        scopes: [AppleIDAuthorizationScopes.email],
      );
      final idToken = credential.identityToken;
      if (idToken == null) {
        state = AsyncError('Gagal mendapatkan kredensial Apple.', StackTrace.current);
        return;
      }
      final response = await _apiClient.postRaw(
        '/api/client/auth/apple',
        data: {
          'id_token': idToken,
          if (_currentAnonymousCustomerId() != null)
            'anonymous_customer_id': _currentAnonymousCustomerId(),
        },
        skipAuth: true,
      );
      final profile = await _applyTokens(response);
      state = AsyncData(await _stateForProfile(profile));
    } on SignInWithAppleAuthorizationException {
      state = AsyncError('Login Apple dibatalkan.', StackTrace.current);
    } on DioException catch (e) {
      state = AsyncError(_socialSignInMessage(e), StackTrace.current);
    } catch (_) {
      state = AsyncError('Gagal masuk dengan Apple.', StackTrace.current);
    }
  }

  // ---------------- Display name update ----------------

  Future<void> setDisplayName(String displayName) async {
    state = const AsyncLoading();
    try {
      final response = await _apiClient.patch(
        '/api/client/auth/profile',
        data: {'display_name': displayName},
      );
      final profile = response['data'] as Map<String, dynamic>;
      state = AsyncData(await _stateForProfile(profile));
    } catch (_) {
      state = AsyncError('Gagal menyimpan nama. Coba lagi.', StackTrace.current);
    }
  }

  // ---------------- Logout ----------------

  Future<void> logout() async {
    try {
      await _apiClient.post('/api/shared/auth/logout');
    } catch (_) {}
    await _storage.clear();
    _bridge.clear();
    state = const AsyncData(AuthInitialData());
  }

  // ---------------- Error-code mapping ----------------

  int? _retryAfterSecondsFrom(DioException e) {
    final raw = e.response?.data?['error']?['details']?['retry_after_seconds'];
    if (raw is num) return raw.toInt();
    return null;
  }

  AuthErrorInfo _otpRequestErrorInfo(DioException e) {
    final code = e.response?.data?['error']?['code'] as String?;
    final retryAfter = _retryAfterSecondsFrom(e);
    final message = switch (code) {
      'PHONE_INVALID' => 'Nomor HP tidak valid.',
      'OTP_COOLDOWN' =>
        e.response?.data?['error']?['message'] as String? ??
            'Tunggu sebentar sebelum minta OTP lagi.',
      'OTP_RATE_LIMIT_PHONE' || 'OTP_RATE_LIMIT_IP' =>
        'Terlalu banyak permintaan OTP. Coba lagi nanti.',
      _ => 'Gagal mengirim OTP. Coba lagi.',
    };
    return AuthErrorInfo(message, code: code, retryAfterSeconds: retryAfter);
  }

  AuthErrorInfo _otpVerifyErrorInfo(DioException e) {
    final code = e.response?.data?['error']?['code'] as String?;
    final retryAfter = _retryAfterSecondsFrom(e);
    final message = switch (code) {
      'WRONG_FLOW' => 'OTP tidak valid untuk login pelanggan.',
      'CODE_MISMATCH' || 'CODE_INVALID' => 'Kode OTP salah.',
      'OTP_EXPIRED' => 'Kode OTP kedaluwarsa. Minta kode baru.',
      'OTP_USED' => 'Kode OTP sudah digunakan.',
      'OTP_ATTEMPTS_EXCEEDED' => 'Terlalu banyak percobaan. Minta kode baru.',
      'IDENTITY_CONFLICT' => 'Nomor ini sudah terdaftar di akun lain.',
      _ => 'Gagal verifikasi. Coba lagi.',
    };
    return AuthErrorInfo(message, code: code, retryAfterSeconds: retryAfter);
  }

  String _socialSignInMessage(DioException e) {
    final code = e.response?.data?['error']?['code'] as String?;
    switch (code) {
      case 'IDENTITY_CONFLICT':
        return 'Akun ini sudah terhubung ke pengguna lain.';
      case 'INVALID_ID_TOKEN':
        return 'Verifikasi identitas gagal. Coba lagi.';
      default:
        return 'Gagal masuk. Coba lagi.';
    }
  }
}
