/// SDD-106 `AndroidPermissionRequester` — Dart integration layer.
///
/// Trace:
/// - SDD-106 (Android runtime acquisition of `RECORD_AUDIO` with
///   listen-only fallback; bridge event surface §5).
/// - SRS-209 (runtime microphone permission acquisition; fail-safe
///   to listen-only on denial; user-visible path to grant).
///
/// Responsibilities:
///  * Subscribe to the Kotlin-side `MethodChannel`
///    `app.chanora/android_permissions` for `permissionStateChanged`
///    invocations emitted by `AndroidPermissionRequester.kt`
///    (Kotlin → Dart).
///  * Provide imperative Dart → Kotlin entry points
///    `requestRecordAudio` and `openAppSettings` so the UI can drive
///    the runtime request flow per SDD-106 §1–§3.
///  * Expose the latest resolved state as a [ValueListenable] so UI
///    surfaces (the listen-only banner per SRS-209; the pre-`voice_join`
///    gate per SDD-106 §1) can react without polling.
///
/// ## Non-Android short-circuit
///
/// On Linux / macOS / Windows / iOS / web, RECORD_AUDIO is not gated
/// by this channel (desktop has no Android runtime-permission concept;
/// iOS uses AVAudioSession authorisation which is owned by the audio
/// engine itself per SDD-101). The MethodChannel is therefore never
/// constructed off-Android. The state listenable stays at
/// [AndroidRecordAudioPermissionState.granted] so the voice-join gate
/// (in `main.dart`) becomes a no-op on those platforms.
///
/// ## No global statics
///
/// Following the pattern established by `BackIntentService`
/// (SDD-028), this class is constructor-injected. The host app
/// instantiates one instance at startup and passes it through the
/// widget tree.
///
/// ## Bridge event surface (SDD-106 §5)
///
/// SDD-106 §5 calls for a `BridgeEvent::PermissionState` variant on
/// the existing Rust → Dart event stream so the audio engine can
/// observe the resolved state for the transmit-clamp described in
/// SDD-106 §6. That Rust-side variant is intentionally deferred to a
/// follow-up Wave touching `chanora_bridge`; the current Wave 2B
/// Dart-only path is sufficient for the UI obligations of SRS-209
/// (listen-only banner; path to grant). See the structured handoff
/// note attached to this task for the rationale.
library;

import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io' show Platform;

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

/// MethodChannel name shared with Kotlin `MethodChannels.ANDROID_PERMISSIONS`.
///
/// Trace: SDD-106 §5.
@visibleForTesting
const String androidPermissionsChannelName = 'app.chanora/android_permissions';

/// Inbound (Kotlin → Dart) method invoked whenever the resolved
/// `RECORD_AUDIO` permission state transitions.
///
/// Payload schema: `{permission: String, state: String}` where
/// `state` is one of `"Granted"`, `"Denied"`, `"PermanentlyDenied"`.
///
/// Trace: SDD-106 §5; mirrors Kotlin
/// `MethodChannels.METHOD_PERMISSION_STATE_CHANGED`.
@visibleForTesting
const String methodPermissionStateChanged = 'permissionStateChanged';

/// Outbound (Dart → Kotlin) method that asks the Android requester to
/// (re-)prompt for `RECORD_AUDIO`. The result is delivered
/// asynchronously via [methodPermissionStateChanged].
///
/// Trace: SDD-106 §1.
@visibleForTesting
const String methodRequestRecordAudio = 'requestRecordAudio';
@visibleForTesting
const String methodRequestStartupPermissions = 'requestStartupPermissions';

/// Outbound (Dart → Kotlin) method that deep-links to the application's
/// Android Settings page so the user can re-grant a permanently-denied
/// permission.
///
/// Trace: SDD-106 §3.
@visibleForTesting
const String methodOpenAppSettings = 'openAppSettings';

/// Discrete states surfaced to the Dart UI.
///
/// Trace: SDD-106 §5 (state machine).
enum AndroidRecordAudioPermissionState {
  /// Permission granted; microphone capture is allowed.
  granted,

  /// Permission denied but the user may still be re-prompted by
  /// invoking [AndroidPermissionsService.ensureRecordAudio] again.
  denied,

  /// Permission denied with "Don't ask again" or revoked from system
  /// Settings. The UI must deep-link to app settings via
  /// [AndroidPermissionsService.openAppSettings] (SDD-106 §3).
  permanentlyDenied,

  /// No resolved state yet (cold launch before the first emission, or
  /// non-Android host before short-circuit). The voice-join gate
  /// treats this as "request before joining".
  unknown,
}

enum AndroidPermissionKind {
  recordAudio('android.permission.RECORD_AUDIO'),
  bluetoothConnect('android.permission.BLUETOOTH_CONNECT'),
  postNotifications('android.permission.POST_NOTIFICATIONS');

  const AndroidPermissionKind(this.permission);
  final String permission;
}

/// Maps the Kotlin-side `PermissionState` string into the Dart enum.
AndroidRecordAudioPermissionState _parseState(String? raw) {
  switch (raw) {
    case 'Granted':
      return AndroidRecordAudioPermissionState.granted;
    case 'Denied':
      return AndroidRecordAudioPermissionState.denied;
    case 'PermanentlyDenied':
      return AndroidRecordAudioPermissionState.permanentlyDenied;
    default:
      return AndroidRecordAudioPermissionState.unknown;
  }
}

/// Dart-side integration for `AndroidPermissionRequester` (SDD-106).
///
/// Trace: SDD-106, SRS-209.
class AndroidPermissionsService {
  /// Construct a service bound to [channel]. Injected for testability;
  /// production code uses the default channel keyed on
  /// [androidPermissionsChannelName].
  AndroidPermissionsService({MethodChannel? channel})
    : _channel =
          channel ??
          (_isAndroid
              ? const MethodChannel(androidPermissionsChannelName)
              : null);

  /// Platform-detection seam. Web counts as non-Android.
  static bool get _isAndroid {
    if (kIsWeb) return false;
    return Platform.isAndroid;
  }

  /// The underlying channel, or `null` on non-Android (and unset in
  /// tests that omit the optional argument).
  final MethodChannel? _channel;

  final ValueNotifier<AndroidRecordAudioPermissionState> _state =
      ValueNotifier<AndroidRecordAudioPermissionState>(
        // On non-Android, present as granted so the voice-join gate is a
        // no-op (desktop / iOS have separate audio-permission paths owned
        // elsewhere; see SDD-101 for iOS).
        _isAndroid
            ? AndroidRecordAudioPermissionState.unknown
            : AndroidRecordAudioPermissionState.granted,
      );

  final Map<AndroidPermissionKind, ValueNotifier<AndroidRecordAudioPermissionState>>
  _extraStates = {
    for (final kind in AndroidPermissionKind.values)
      kind: ValueNotifier<AndroidRecordAudioPermissionState>(
        _isAndroid
            ? AndroidRecordAudioPermissionState.unknown
            : AndroidRecordAudioPermissionState.granted,
      ),
  };

  bool _started = false;

  /// Latest known [RECORD_AUDIO] state. Defaults to
  /// [AndroidRecordAudioPermissionState.unknown] on Android and
  /// [AndroidRecordAudioPermissionState.granted] elsewhere.
  ValueListenable<AndroidRecordAudioPermissionState> get recordAudioState =>
      _state;

  /// Start listening for state updates from Kotlin. Idempotent.
  ///
  /// On non-Android this is a no-op.
  ///
  /// Trace: SDD-106 §5.
  void start() {
    if (_started) return;
    _started = true;
    final ch = _channel;
    if (ch == null) return;
    ch.setMethodCallHandler(_handle);
  }

  /// Stop listening. Idempotent.
  void stop() {
    if (!_started) return;
    _started = false;
    final ch = _channel;
    if (ch == null) return;
    ch.setMethodCallHandler(null);
  }

  Future<dynamic> _handle(MethodCall call) async {
    if (call.method != methodPermissionStateChanged) return null;
    final args = call.arguments;
    if (args is! Map) return null;
    final permission = args['permission'];
    if (permission is! String) return null;
    final state = _parseState(args['state'] as String?);
    if (permission == AndroidPermissionKind.recordAudio.permission) {
      _state.value = state;
    }
    for (final entry in _extraStates.entries) {
      if (entry.key.permission == permission) {
        entry.value.value = state;
      }
    }
    return null;
  }

  ValueListenable<AndroidRecordAudioPermissionState> permissionState(
    AndroidPermissionKind kind,
  ) {
    if (kind == AndroidPermissionKind.recordAudio) {
      return _state;
    }
    return _extraStates[kind]!;
  }

  /// Request the system permission. Invokes the Kotlin requester and
  /// then, if the platform synchronously resolves the request, returns
  /// the resolved state without parking for the listener; otherwise
  /// awaits the next `permissionStateChanged` emission.
  ///
  /// On non-Android, resolves immediately with
  /// [AndroidRecordAudioPermissionState.granted].
  ///
  /// Trace: SDD-106 §1, §3.
  Future<AndroidRecordAudioPermissionState> ensureRecordAudio() async {
    final ch = _channel;
    if (ch == null) {
      return AndroidRecordAudioPermissionState.granted;
    }
    if (_state.value == AndroidRecordAudioPermissionState.granted) {
      return AndroidRecordAudioPermissionState.granted;
    }
    // Snapshot the pre-invocation state. New Android hosts return the
    // resolved Kotlin PermissionState string from requestRecordAudio;
    // older/test hosts may still return null and rely only on the
    // permissionStateChanged callback below.
    final preInvokeState = _state.value;
    String? returnedState;
    try {
      returnedState = await ch.invokeMethod<String>(methodRequestRecordAudio);
    } catch (_) {
      // Channel-side failure (e.g. missing handler in a debug build).
      // Fall back to whatever state we currently hold; if still
      // unknown, surface unknown so the caller can choose its own
      // policy. Per SRS-209 the voice-join gate treats unknown as
      // "proceed in listen-only".
      return _state.value;
    }
    final parsedReturnedState = _parseState(returnedState);
    if (parsedReturnedState != AndroidRecordAudioPermissionState.unknown) {
      _state.value = parsedReturnedState;
      return parsedReturnedState;
    }
    // M1 fix (corrected): only short-circuit if the platform changed
    // the state synchronously to a resolved value. A still-unknown
    // state means we must wait. A still-equal-to-pre-invoke state
    // means we also must wait (the platform is re-prompting; the
    // resolution arrives asynchronously after the user interacts with
    // the dialog).
    if (_state.value != preInvokeState &&
        _state.value != AndroidRecordAudioPermissionState.unknown) {
      return _state.value;
    }
    // Otherwise we wait for the listener to observe a state
    // transition. Bounded wait to avoid hanging the voice-join flow
    // if the platform dialog is dismissed without a result. The
    // Kotlin requester treats dismissal as Denied (see
    // AndroidPermissionRequester.handleRequestPermissionsResult) so
    // this is defence-in-depth.
    final completer = Completer<AndroidRecordAudioPermissionState>();
    void listener() {
      if (!completer.isCompleted &&
          _state.value != preInvokeState &&
          _state.value != AndroidRecordAudioPermissionState.unknown) {
        completer.complete(_state.value);
      }
    }

    _state.addListener(listener);
    try {
      return await completer.future.timeout(
        const Duration(seconds: 30),
        onTimeout: () => _state.value,
      );
    } finally {
      _state.removeListener(listener);
    }
  }

  Future<Map<AndroidPermissionKind, AndroidRecordAudioPermissionState>>
  ensureStartupPermissions() async {
    final ch = _channel;
    if (ch == null) {
      return {
        for (final kind in AndroidPermissionKind.values)
          kind: AndroidRecordAudioPermissionState.granted,
      };
    }
    Map<dynamic, dynamic>? returned;
    try {
      returned = await ch.invokeMethod<Map<dynamic, dynamic>>(
        methodRequestStartupPermissions,
      );
    } catch (_) {
      return {
        for (final kind in AndroidPermissionKind.values)
          kind: permissionState(kind).value,
      };
    }
    final resolved = <AndroidPermissionKind, AndroidRecordAudioPermissionState>{};
    for (final kind in AndroidPermissionKind.values) {
      final parsed = _parseState(returned?[kind.permission] as String?);
      if (kind == AndroidPermissionKind.recordAudio) {
        _state.value = parsed;
      } else {
        _extraStates[kind]!.value = parsed;
      }
      resolved[kind] = parsed;
    }
    return resolved;
  }

  /// Deep-link to the system app settings page for the permanently
  /// denied case (SDD-106 §3). On non-Android, a no-op.
  Future<void> openAppSettings() async {
    final ch = _channel;
    if (ch == null) return;
    try {
      await ch.invokeMethod<void>(methodOpenAppSettings);
    } catch (e, st) {
      // Best-effort; failure to launch settings is non-fatal. The
      // Kotlin side already logs ActivityNotFoundException.
      developer.log(
        'openAppSettings failed',
        name: 'AndroidPermissionsService',
        error: e,
        stackTrace: st,
      );
    }
  }

  /// Release the state notifier. Test helper; production keeps the
  /// service alive for the lifetime of the app.
  @visibleForTesting
  void dispose() {
    stop();
    _state.dispose();
    for (final notifier in _extraStates.values) {
      notifier.dispose();
    }
  }
}
