import 'dart:async';
import 'package:dio/dio.dart';
import 'package:riverpod_annotation/riverpod_annotation.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';

sealed class MitraAuthData {
  const MitraAuthData();
}

class MitraAuthInitialData extends MitraAuthData {
  const MitraAuthInitialData();
}

class MitraAuthAuthenticatedData extends MitraAuthData {
  final Map<String, dynamic> profile;
  const MitraAuthAuthenticatedData(this.profile);
}

class MitraAuthOtpSentData extends MitraAuthData {
  final String otpRequestId;
  final String? channelUsed;
  const MitraAuthOtpSentData(this.otpRequestId, {this.channelUsed});
}

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

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

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

    final profile = await _refreshFromStorage();
    if (profile != null) {
      return MitraAuthAuthenticatedData(profile);
    }
    return const MitraAuthInitialData();
  }

  /// Reads the stored refresh token, rotates it against the backend, and
  /// returns the fresh profile. Also updates the [AuthBridge] access token
  /// and persists the new refresh token. Returns null if no token is stored
  /// or the refresh call fails (refresh expired, revoked, etc).
  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,
      );
      final data = response['data'] as Map<String, dynamic>;
      final newAccess = data['access_token'] as String;
      final newRefresh = data['refresh_token'] as String;
      final profile = data['profile'] as Map<String, dynamic>;

      await _storage.writeRefreshToken(newRefresh);
      _bridge.setAccessToken(newAccess);

      // Sync notifier state if it was already authenticated (e.g. mid-session
      // 401 refresh). On initial bootstrap, build() sets state from our return.
      final current = state.valueOrNull;
      if (current is MitraAuthAuthenticatedData) {
        state = AsyncData(MitraAuthAuthenticatedData(profile));
      }
      return profile;
    } catch (_) {
      await _storage.clear();
      _bridge.clear();
      final current = state.valueOrNull;
      if (current is MitraAuthAuthenticatedData) {
        state = const AsyncData(MitraAuthInitialData());
      }
      return null;
    }
  }

  /// Thin adapter for [AuthBridge.refresh]: returns the rotated access
  /// token (or null on failure) so api_client can retry a 401.
  Future<String?> _refreshForBridge() async {
    final profile = await _refreshFromStorage();
    return profile == null ? null : _bridge.accessToken;
  }

  Future<void> requestOtp(String phone, {String? channel}) async {
    state = const AsyncLoading();
    try {
      final response = await _apiClient.postRaw(
        '/api/mitra/auth/otp/request',
        data: {
          'phone': phone,
          if (channel != null) 'channel': channel,
        },
        skipAuth: true,
      );
      final data = response['data'] as Map<String, dynamic>;
      state = AsyncData(MitraAuthOtpSentData(
        data['otp_request_id'] as String,
        channelUsed: data['channel_used'] as String?,
      ));
    } on DioException catch (e) {
      state = AsyncError(_otpRequestMessage(e), StackTrace.current);
    } catch (_) {
      state = AsyncError('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/mitra/auth/otp/verify',
        data: {'otp_request_id': otpRequestId, 'code': code},
        skipAuth: true,
      );
      final data = response['data'] as Map<String, dynamic>;
      final accessToken = data['access_token'] as String;
      final refreshToken = data['refresh_token'] as String;
      final profile = data['profile'] as Map<String, dynamic>;

      await _storage.writeRefreshToken(refreshToken);
      _bridge.setAccessToken(accessToken);
      state = AsyncData(MitraAuthAuthenticatedData(profile));
    } on DioException catch (e) {
      state = AsyncError(_otpVerifyMessage(e), StackTrace.current);
    } catch (_) {
      state = AsyncError('Gagal verifikasi. Coba lagi.', StackTrace.current);
    }
  }

  Future<void> logout() async {
    // Best-effort server-side revocation; local clear always runs.
    try {
      await _apiClient.post('/api/shared/auth/logout');
    } catch (_) {}
    await _storage.clear();
    _bridge.clear();
    state = const AsyncData(MitraAuthInitialData());
  }

  String _otpRequestMessage(DioException e) {
    final code = e.response?.data?['error']?['code'] as String?;
    switch (code) {
      case 'PHONE_INVALID':
        return 'Nomor HP tidak valid.';
      case 'OTP_COOLDOWN':
        return e.response?.data?['error']?['message'] as String? ??
            'Tunggu sebentar sebelum minta OTP lagi.';
      case 'OTP_RATE_LIMIT_PHONE':
      case 'OTP_RATE_LIMIT_IP':
        return 'Terlalu banyak permintaan OTP. Coba lagi nanti.';
      default:
        return 'Gagal mengirim OTP. Coba lagi.';
    }
  }

  String _otpVerifyMessage(DioException e) {
    final code = e.response?.data?['error']?['code'] as String?;
    switch (code) {
      case 'ACCOUNT_INACTIVE':
        return 'Akun tidak aktif. Hubungi administrator.';
      case 'WRONG_FLOW':
        return 'OTP tidak valid untuk login mitra.';
      case 'CODE_MISMATCH':
      case 'CODE_INVALID':
        return 'Kode OTP salah.';
      case 'OTP_EXPIRED':
        return 'Kode OTP kedaluwarsa. Minta kode baru.';
      case 'OTP_USED':
        return 'Kode OTP sudah digunakan.';
      case 'OTP_ATTEMPTS_EXCEEDED':
        return 'Terlalu banyak percobaan. Minta kode baru.';
      default:
        return 'Gagal verifikasi. Coba lagi.';
    }
  }
}
