import 'dart:async';
import 'dart:io';

import 'package:sentry_flutter/sentry_flutter.dart';

import 'app_logger.dart';
import 'device_channel.dart';

enum AndroidStartupPhase {
  nativeOnCreate('native_on_create'),
  dartMain('dart_main'),
  runApp('runApp'),
  firstFrame('first_frame'),
  databaseOpenStarted('database_open_started'),
  databaseReady('database_ready'),
  credentialsLoaded('credentials_loaded'),
  bindingStarted('binding_started'),
  bindingSettled('binding_settled'),
  mainScreen('main_screen');

  const AndroidStartupPhase(this.id);

  final String id;
}

enum AndroidUiState {
  mainScreen('main_screen'),
  player('player');

  const AndroidUiState(this.id);

  final String id;
}

/// Best-effort bridge for the newest Android 11+ historical process exit.
abstract final class AndroidExitDiagnostics {
  static const _allowedReasons = {'crash', 'native_crash', 'anr', 'low_memory', 'user_requested', 'other'};
  static const _allowedAbis = {'arm64-v8a', 'armeabi-v7a', 'x86_64', 'x86', 'unknown'};
  static const _allowedCodecContexts = {
    'audio:aac',
    'audio:ac3',
    'audio:eac3',
    'audio:dts',
    'audio:truehd',
    'audio:flac',
    'audio:pcm',
    'audio:other',
    'video:dolby_vision',
    'video:hevc',
    'video:avc',
    'video:other',
  };
  static const _allowedUiStates = {'startup', 'authentication', 'main_screen', 'player', 'player_disposed'};
  static const _allowedStartupPhases = {
    'native_on_create',
    'dart_main',
    'runApp',
    'first_frame',
    'database_open_started',
    'database_ready',
    'credentials_loaded',
    'binding_started',
    'binding_settled',
    'main_screen',
  };
  static final _decoderNamePattern = RegExp(r'^[A-Za-z0-9_.:-]{1,96}$');
  static final _startupWatch = Stopwatch()..start();
  static final List<({String phase, int elapsedMs})> _pendingBreadcrumbs = [];
  static var _telemetryReady = false;
  static var _nativeOnCreateRecorded = false;
  static var _lastElapsedMs = 0;

  /// Flushes phase breadcrumbs recorded before Sentry's app runner started.
  static void markTelemetryReady() {
    if (_telemetryReady) return;
    _telemetryReady = true;
    final pending = List.of(_pendingBreadcrumbs);
    _pendingBreadcrumbs.clear();
    for (final mark in pending) {
      _sendBreadcrumb(mark.phase, mark.elapsedMs);
    }
  }

  /// Persists and records one fixed, privacy-safe startup phase.
  static void markStartupPhase(AndroidStartupPhase phase) {
    try {
      if (Platform.isAndroid && !_nativeOnCreateRecorded) {
        _nativeOnCreateRecorded = true;
        _recordPhase(AndroidStartupPhase.nativeOnCreate.id, 0);
      }
      final measuredMs = _startupWatch.elapsedMilliseconds;
      final elapsedMs = measuredMs < _lastElapsedMs ? _lastElapsedMs : measuredMs;
      _lastElapsedMs = elapsedMs;
      _recordPhase(phase.id, elapsedMs);
      if (Platform.isAndroid) {
        unawaited(_persistStartupPhase(phase.id));
      }
    } catch (_) {
      // Startup diagnostics must never affect the startup path.
    }
  }

  static void _recordPhase(String phase, int elapsedMs) {
    try {
      appLogger.i('Startup phase: phase=$phase elapsedMs=$elapsedMs');
    } catch (_) {
      // Local logging is best-effort.
    }
    if (_telemetryReady) {
      _sendBreadcrumb(phase, elapsedMs);
    } else {
      _pendingBreadcrumbs.add((phase: phase, elapsedMs: elapsedMs));
    }
  }

  static void _sendBreadcrumb(String phase, int elapsedMs) {
    try {
      unawaited(
        Sentry.addBreadcrumb(
          Breadcrumb(
            message: 'Startup phase $phase',
            category: 'startup.phase',
            data: {'phase': phase, 'elapsedMs': elapsedMs},
          ),
        ).catchError((_) {}),
      );
    } catch (_) {
      // Breadcrumb emission is best-effort.
    }
  }

  static Future<void> _persistStartupPhase(String phase) async {
    try {
      await deviceChannel.invokeMethod<bool>('setStartupPhase', phase);
    } catch (_) {
      // Native phase persistence is best-effort.
    }
  }

  static Future<void> markUiState(AndroidUiState state) async {
    if (!Platform.isAndroid) return;
    try {
      await deviceChannel.invokeMethod<bool>('setRuntimeUiState', state.id);
    } catch (_) {
      // Runtime diagnostics are best-effort and must never affect navigation.
    }
  }

  /// Records a native-sanitized previous-exit report in local logs and Sentry.
  ///
  /// Native persistence makes this one-shot across launches. Every failure is
  /// intentionally contained because historical diagnostics must not affect
  /// startup.
  static Future<void> logPreviousExit() async {
    if (!Platform.isAndroid) return;
    try {
      final raw = await deviceChannel.invokeMapMethod<String, Object?>('getPreviousExit');
      final report = _validate(raw);
      if (report == null) return;

      appLogger.w(
        'Previous Android application exit: '
        'reason=${report['reason']} status=${report['status']} '
        'importance=${report['importance']} timestamp=${report['timestamp']} '
        'deviceModel=${report['deviceModel']} apiLevel=${report['apiLevel']} '
        'abi=${report['abi']} lowRam=${report['lowRam']} '
        'startupPhase=${report['startupPhase'] ?? 'omitted'} '
        'codecContext=${report['codecContext'] ?? 'omitted'} '
        'channels=${report['channelCount'] ?? 'omitted'} sampleRate=${report['sampleRate'] ?? 'omitted'} '
        'decoder=${report['selectedDecoder'] ?? 'omitted'} '
        'passthrough=${report['passthroughEnabled'] ?? 'omitted'} '
        'downmix=${report['downmixEnabled'] ?? 'omitted'} '
        'normalization=${report['normalizationEnabled'] ?? 'omitted'} '
        'uiState=${report['uiState'] ?? 'omitted'}',
      );
      await Sentry.captureMessage(
        'Previous Android application exit',
        level: SentryLevel.warning,
        withScope: (scope) => scope.setContexts('android_previous_exit', report),
      );
    } catch (_) {
      // Historical diagnostics are best-effort and must never escape startup.
    }
  }

  static Map<String, Object?>? _validate(Map<String, Object?>? raw) {
    if (raw == null) return null;
    final reason = raw['reason'];
    final status = raw['status'];
    final importance = raw['importance'];
    final timestamp = raw['timestamp'];
    final deviceModel = raw['deviceModel'];
    final apiLevel = raw['apiLevel'];
    final abi = raw['abi'];
    final lowRam = raw['lowRam'];
    final startupPhase = raw['startupPhase'];
    final codecContext = raw['codecContext'];
    final channelCount = raw['channelCount'];
    final sampleRate = raw['sampleRate'];
    final selectedDecoder = raw['selectedDecoder'];
    final passthroughEnabled = raw['passthroughEnabled'];
    final downmixEnabled = raw['downmixEnabled'];
    final normalizationEnabled = raw['normalizationEnabled'];
    final uiState = raw['uiState'];
    if (reason is! String || !_allowedReasons.contains(reason)) return null;
    if (status is! int || importance is! int || timestamp is! int) return null;
    if (deviceModel is! String ||
        deviceModel.isEmpty ||
        deviceModel.length > 80 ||
        deviceModel.runes.any(_isControlCharacter)) {
      return null;
    }
    if (apiLevel is! int || apiLevel < 1 || abi is! String || !_allowedAbis.contains(abi) || lowRam is! bool) {
      return null;
    }
    if (startupPhase != null && (startupPhase is! String || !_allowedStartupPhases.contains(startupPhase))) {
      return null;
    }
    if (codecContext != null && (codecContext is! String || !_allowedCodecContexts.contains(codecContext))) return null;
    if (channelCount != null && (channelCount is! int || channelCount < 1 || channelCount > 32)) return null;
    if (sampleRate != null && (sampleRate is! int || sampleRate < 1 || sampleRate > 768000)) return null;
    if (selectedDecoder != null && (selectedDecoder is! String || !_decoderNamePattern.hasMatch(selectedDecoder))) {
      return null;
    }
    if (passthroughEnabled != null && passthroughEnabled is! bool) return null;
    if (downmixEnabled != null && downmixEnabled is! bool) return null;
    if (normalizationEnabled != null && normalizationEnabled is! bool) return null;
    if (uiState != null && (uiState is! String || !_allowedUiStates.contains(uiState))) return null;
    return <String, Object?>{
      'reason': reason,
      'status': status,
      'importance': importance,
      'timestamp': timestamp,
      'deviceModel': deviceModel,
      'apiLevel': apiLevel,
      'abi': abi,
      'lowRam': lowRam,
      if (startupPhase case final String safeStartupPhase) 'startupPhase': safeStartupPhase,
      if (codecContext case final String safeCodecContext) 'codecContext': safeCodecContext,
      if (channelCount case final int safeChannelCount) 'channelCount': safeChannelCount,
      if (sampleRate case final int safeSampleRate) 'sampleRate': safeSampleRate,
      if (selectedDecoder case final String safeSelectedDecoder) 'selectedDecoder': safeSelectedDecoder,
      if (passthroughEnabled case final bool safePassthroughEnabled) 'passthroughEnabled': safePassthroughEnabled,
      if (downmixEnabled case final bool safeDownmixEnabled) 'downmixEnabled': safeDownmixEnabled,
      if (normalizationEnabled case final bool safeNormalizationEnabled)
        'normalizationEnabled': safeNormalizationEnabled,
      if (uiState case final String safeUiState) 'uiState': safeUiState,
    };
  }

  static bool _isControlCharacter(int rune) => rune < 0x20 || rune == 0x7f;
}
