import 'dart:async';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_lucide/flutter_lucide.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:pub_semver/pub_semver.dart';

import '../../api/client.dart';
import '../../api/endpoints/health.dart';
import '../../auth/auth_provider.dart';
import '../../theme/theme_extension.dart';

/// Result of the most recent /healthz version compatibility check.
///
/// `skipped` means the server didn't emit a `min_client_version` field
/// (older servers, partial deploys). Treat as compatible.
enum VersionResult { ok, tooOld, skipped }

VersionResult _resultFromString(String? s) {
  switch (s) {
    case 'ok':
      return VersionResult.ok;
    case 'tooOld':
      return VersionResult.tooOld;
    default:
      return VersionResult.skipped;
  }
}

String _stringFromResult(VersionResult r) {
  switch (r) {
    case VersionResult.ok:
      return 'ok';
    case VersionResult.tooOld:
      return 'tooOld';
    case VersionResult.skipped:
      return 'skipped';
  }
}

/// Drives the version-compatibility check against /healthz. Non-blocking:
/// the controller hydrates from a 1h-throttled cache on boot, exposes
/// the current `VersionResult`, and refreshes itself in the background.
/// UI (VersionGate + _VersionTooOldBanner) reads this state to decide
/// whether to surface the "too old" banner — never blocks rendering.
///
/// Cache keys live in flutter_secure_storage so the cadence survives
/// app restarts. 1h throttle bounds /healthz traffic; explicit
/// `recheck()` (from the banner's "Check now" button or app resume)
/// bypasses the throttle.
class VersionCheckController extends AsyncNotifier<VersionResult> {
  static const _kResult = 'version_check_result';
  static const _kAtMs = 'version_check_at_ms';
  // 1 minute. The /healthz response is sub-1KB and the call is
  // non-blocking, so a tight cadence buys faster recovery from
  // server-side min_client_version bumps without measurable cost. The
  // gate also acts as a safety net against duplicate calls when the
  // periodic timer and a resume event fire close together.
  static const _staleMs = 60 * 1000;

  @override
  Future<VersionResult> build() async {
    final storage = ref.read(secureStorageProvider);
    final resultStr = await storage.read(key: _kResult);
    final atStr = await storage.read(key: _kAtMs);
    final cached = _resultFromString(resultStr);
    final at = int.tryParse(atStr ?? '') ?? 0;
    final nowMs = DateTime.now().millisecondsSinceEpoch;

    // Fire background recheck on cold-start when cache is missing or stale.
    // Doesn't block: build returns the cached result immediately; the
    // recheck updates state asynchronously when it lands.
    if (nowMs - at > _staleMs) {
      Future.microtask(_runCheck);
    }
    return cached;
  }

  /// Force a fresh check, bypassing the 1h staleness gate. Used by the
  /// "Check now" banner button and (indirectly) by the AppLifecycleState
  /// resume observer in VersionGate.
  Future<void> recheck() async => _runCheck();

  /// Recheck only if the cache is older than the 1h throttle. No-op
  /// when fresh. Used on app resume to avoid hammering /healthz when
  /// the user is briefly switching between apps.
  Future<void> recheckIfStale() async {
    final storage = ref.read(secureStorageProvider);
    final atStr = await storage.read(key: _kAtMs);
    final at = int.tryParse(atStr ?? '') ?? 0;
    if (DateTime.now().millisecondsSinceEpoch - at > _staleMs) {
      await _runCheck();
    }
  }

  Future<void> _runCheck() async {
    try {
      final url = await ref.read(serverUrlProvider.future);
      if (url == null || url.isEmpty) return;
      // Bounded timeouts for a health probe — the default
      // ApiClient.buildDio dio is tuned for actual data fetches
      // (8s connect + 30s receive). /healthz should resolve in
      // tens of milliseconds; failing fast unblocks slow networks.
      final dio = ApiClient.buildDio(
        baseUrl: url,
        tokenResolver: () async => null,
      );
      dio.options.connectTimeout = const Duration(seconds: 3);
      dio.options.receiveTimeout = const Duration(seconds: 2);
      final body = await HealthApi(dio).check();
      final min = body['min_client_version'];
      VersionResult result;
      if (min == null || min.isEmpty) {
        result = VersionResult.skipped;
      } else {
        final info = await PackageInfo.fromPlatform();
        final mine = Version.parse(info.version);
        final required = Version.parse(min);
        result = mine < required ? VersionResult.tooOld : VersionResult.ok;
      }
      // Persist + flip state. Storage write before state assignment so
      // a hot reload immediately after won't see a fresh state with a
      // stale cache.
      final storage = ref.read(secureStorageProvider);
      await storage.write(key: _kResult, value: _stringFromResult(result));
      await storage.write(
        key: _kAtMs,
        value: DateTime.now().millisecondsSinceEpoch.toString(),
      );
      state = AsyncData(result);
    } on DioException catch (_) {
      // Network error / timeout. Keep the cached state; don't bump the
      // timestamp so the next staleness check still triggers a retry.
    } catch (_) {
      // Other errors (PackageInfo, parsing, etc.). Same handling —
      // soft-fail so the UI never sees an error state from a defensive
      // background check.
    }
  }
}

final versionCheckProvider =
    AsyncNotifierProvider<VersionCheckController, VersionResult>(
  VersionCheckController.new,
);

/// Wraps the shell. Non-blocking: always renders the child. Activates
/// the version check controller on mount and reruns it on app resume
/// (gated by the controller's 1h staleness throttle).
class VersionGate extends ConsumerStatefulWidget {
  const VersionGate({required this.child, super.key});
  final Widget child;

  @override
  ConsumerState<VersionGate> createState() => _VersionGateState();
}

class _VersionGateState extends ConsumerState<VersionGate>
    with WidgetsBindingObserver {
  // Polling interval during active foreground use. Paired with the
  // controller's 1m staleness gate so concurrent fires (timer + resume)
  // dedupe to a single network call. Tight cadence is affordable —
  // /healthz is sub-1KB and the call is non-blocking.
  static const _pollInterval = Duration(minutes: 1);
  Timer? _pollTimer;

  @override
  void initState() {
    super.initState();
    // Read once so the provider's build() runs; the controller
    // hydrates from cache + fires a background check when stale.
    ref.read(versionCheckProvider);
    WidgetsBinding.instance.addObserver(this);
    _startPolling();
  }

  @override
  void dispose() {
    _stopPolling();
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  void _startPolling() {
    _pollTimer?.cancel();
    _pollTimer = Timer.periodic(_pollInterval, (_) {
      // ignore: unawaited_futures
      ref.read(versionCheckProvider.notifier).recheckIfStale();
    });
  }

  void _stopPolling() {
    _pollTimer?.cancel();
    _pollTimer = null;
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        // Re-arm the timer + fire one immediate (staleness-gated) check
        // so users who foreground after a long away period get a fresh
        // result without waiting for the next tick.
        // ignore: unawaited_futures
        ref.read(versionCheckProvider.notifier).recheckIfStale();
        _startPolling();
      case AppLifecycleState.paused:
      case AppLifecycleState.inactive:
      case AppLifecycleState.hidden:
      case AppLifecycleState.detached:
        // Stop firing while backgrounded — no need to burn battery on
        // health probes the user can't see the result of.
        _stopPolling();
    }
  }

  @override
  Widget build(BuildContext context) => widget.child;
}

/// Banner that surfaces when the server reports our client is too old.
/// Non-blocking: the rest of the app keeps working (offline-mode-style
/// — locally cached metadata + audio still play). Tap "Check now" to
/// force a re-check after installing a new build.
class VersionTooOldBanner extends ConsumerWidget {
  const VersionTooOldBanner({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final asyncResult = ref.watch(versionCheckProvider);
    final result = asyncResult.asData?.value;
    if (result != VersionResult.tooOld) return const SizedBox.shrink();
    final fs = Theme.of(context).extension<FabledSwordTheme>()!;
    return SafeArea(
      bottom: false,
      child: Container(
        color: fs.error.withValues(alpha: 0.15),
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
        child: Row(children: [
          Icon(LucideIcons.triangle_alert, color: fs.error, size: 18),
          const SizedBox(width: 8),
          Expanded(
            child: Text(
              "This app is older than the server requires. "
              "You can keep using cached content; install an update when ready.",
              style: TextStyle(color: fs.parchment, fontSize: 13),
            ),
          ),
          TextButton(
            onPressed: () =>
                ref.read(versionCheckProvider.notifier).recheck(),
            style: TextButton.styleFrom(
              foregroundColor: fs.parchment,
              minimumSize: const Size(0, 36),
              padding: const EdgeInsets.symmetric(horizontal: 12),
            ),
            child: const Text('Check now'),
          ),
        ]),
      ),
    );
  }
}
