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

import 'package:scheduler_notifications/notifications.dart';

/// Supported desktop platform categories.
enum DesktopNotificationPlatform {
  /// Linux desktop.
  linux,

  /// macOS desktop.
  macos,

  /// Windows desktop.
  windows,

  /// Unsupported or unknown platform.
  unsupported,
}

/// Logging callback used by fallback notification backends.
typedef DesktopNotificationLogSink = void Function(String line);

/// Process runner used by command-backed notification backends.
typedef DesktopProcessRunner = Future<ProcessResult> Function(
  String executable,
  List<String> arguments,
);

/// Platform-neutral backend used by [DesktopNotificationAdapter].
abstract interface class DesktopNotificationBackend {
  /// Whether this backend has native notification support.
  bool get isSupported;

  /// Deliver [request].
  Future<void> deliver(NotificationRequest request);

  /// Cancel a pending notification by [id], when supported.
  Future<void> cancel(String id);
}

/// Notification adapter that delegates to a desktop backend.
final class DesktopNotificationAdapter implements NotificationAdapter {
  /// Creates an adapter backed by [backend].
  DesktopNotificationAdapter({required DesktopNotificationBackend backend})
      : _backend = backend;

  final DesktopNotificationBackend _backend;

  /// Creates the best available desktop adapter for the current platform.
  factory DesktopNotificationAdapter.defaultInstance({
    DesktopNotificationPlatform? platform,
    DesktopProcessRunner? processRunner,
    DesktopNotificationLogSink? logSink,
  }) {
    return DesktopNotificationAdapter(
      backend: createDefaultDesktopNotificationBackend(
        platform: platform,
        processRunner: processRunner,
        logSink: logSink,
      ),
    );
  }

  @override
  Stream<NotificationFeedback> get feedback => const Stream.empty();

  @override
  Future<void> schedule(NotificationRequest request) {
    return _backend.deliver(request);
  }

  @override
  Future<void> cancel(String id) {
    return _backend.cancel(id.trim());
  }
}

/// Create the best default desktop notification backend.
DesktopNotificationBackend createDefaultDesktopNotificationBackend({
  DesktopNotificationPlatform? platform,
  DesktopProcessRunner? processRunner,
  DesktopNotificationLogSink? logSink,
}) {
  final resolvedPlatform = platform ?? _currentPlatform();
  final runner = processRunner ?? Process.run;
  final fallback = StdoutNotificationBackend(
    logSink: logSink ?? stdout.writeln,
    platform: resolvedPlatform,
  );

  return switch (resolvedPlatform) {
    DesktopNotificationPlatform.linux => LinuxNotifySendBackend(
        processRunner: runner,
        fallback: fallback,
      ),
    DesktopNotificationPlatform.macos => MacOsNotificationBackend(
        processRunner: runner,
        fallback: fallback,
      ),
    DesktopNotificationPlatform.windows => fallback,
    DesktopNotificationPlatform.unsupported => fallback,
  };
}

/// Linux backend using `notify-send`.
final class LinuxNotifySendBackend implements DesktopNotificationBackend {
  /// Creates a Linux command-backed backend.
  LinuxNotifySendBackend({
    required DesktopProcessRunner processRunner,
    required DesktopNotificationBackend fallback,
  })  : _processRunner = processRunner,
        _fallback = fallback;

  final DesktopProcessRunner _processRunner;
  final DesktopNotificationBackend _fallback;

  @override
  bool get isSupported => true;

  @override
  Future<void> deliver(NotificationRequest request) async {
    await _runOrFallback(
      request,
      () => _processRunner('notify-send', <String>[
        '--app-name=ADHD Scheduler',
        request.title,
        request.body,
      ]),
    );
  }

  @override
  Future<void> cancel(String id) async {
    await _fallback.cancel(id);
  }

  Future<void> _runOrFallback(
    NotificationRequest request,
    Future<ProcessResult> Function() run,
  ) async {
    try {
      final result = await run();
      if (result.exitCode != 0) {
        await _fallback.deliver(request);
      }
    } on ProcessException {
      await _fallback.deliver(request);
    }
  }
}

/// macOS backend using `osascript`.
final class MacOsNotificationBackend implements DesktopNotificationBackend {
  /// Creates a macOS command-backed backend.
  MacOsNotificationBackend({
    required DesktopProcessRunner processRunner,
    required DesktopNotificationBackend fallback,
  })  : _processRunner = processRunner,
        _fallback = fallback;

  final DesktopProcessRunner _processRunner;
  final DesktopNotificationBackend _fallback;

  @override
  bool get isSupported => true;

  @override
  Future<void> deliver(NotificationRequest request) async {
    final script = 'display notification ${_appleScriptString(request.body)} '
        'with title ${_appleScriptString(request.title)}';
    try {
      final result = await _processRunner('osascript', <String>['-e', script]);
      if (result.exitCode != 0) {
        await _fallback.deliver(request);
      }
    } on ProcessException {
      await _fallback.deliver(request);
    }
  }

  @override
  Future<void> cancel(String id) async {
    await _fallback.cancel(id);
  }
}

/// Fallback backend that writes notification requests to stdout/logs.
final class StdoutNotificationBackend implements DesktopNotificationBackend {
  /// Creates a stdout fallback backend.
  StdoutNotificationBackend({
    required DesktopNotificationLogSink logSink,
    required this.platform,
  }) : _logSink = logSink;

  final DesktopNotificationLogSink _logSink;

  /// Platform this fallback represents.
  final DesktopNotificationPlatform platform;

  @override
  bool get isSupported => false;

  @override
  Future<void> deliver(NotificationRequest request) async {
    _logSink(
      'notification[$platform] ${request.id}: ${request.title} - '
      '${request.body}',
    );
  }

  @override
  Future<void> cancel(String id) async {
    _logSink('notification[$platform] cancel: ${id.trim()}');
  }
}

DesktopNotificationPlatform _currentPlatform() {
  if (Platform.isLinux) return DesktopNotificationPlatform.linux;
  if (Platform.isMacOS) return DesktopNotificationPlatform.macos;
  if (Platform.isWindows) return DesktopNotificationPlatform.windows;
  return DesktopNotificationPlatform.unsupported;
}

String _appleScriptString(String value) {
  return jsonEncode(value);
}
