// ignore_for_file: depend_on_referenced_packages

import 'dart:collection';
import 'dart:convert';
import 'dart:io';

import 'package:dart_framework/platform/isolate_manager.dart';
import 'package:dart_framework/platform/process.dart';
import 'package:dart_framework/utils/os_startup.dart';
import 'package:dart_framework/utils/string_util.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/cli/commands/scheduler/data/scheduler_data.dart';
import 'package:oto/cli/commands/scheduler/scheduler_isolate.dart';
import 'package:oto/cli/commands/scheduler/scheduler_linux.dart';
import 'package:oto/cli/commands/scheduler/scheduler_osx.dart';
import 'package:oto/cli/commands/scheduler/scheduler_windows.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/system_runtime.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:path/path.dart' as path;

class SchedulerManager {
  static String get _schedulerPath {
    return '$dataPath/scheduler';
  }

  static String get _logsPath {
    return '$_schedulerPath/logs';
  }

  ProcessInfo? currentScheduler;
  final Map<String, IsolateHandler> _map = {};
  final logFile = File(
      '$_logsPath/[$pid]_scheduler_${getDate()}_${getTime().replaceAll(':', '')}.txt');

  String get exePath {
    return throw UnimplementedError('Unimplemented exePath');
  }

  String get _settingsPath {
    return '$_schedulerPath/settings.json';
  }

  SchedulerListData get _localData {
    var file = File(_settingsPath);
    if (!file.existsSync()) {
      _localData = SchedulerListData();
    }
    return SchedulerListData.fromJson(jsonDecode(file.readAsStringSync()));
  }

  set _localData(SchedulerListData data) {
    var file = File(_settingsPath);
    file.writeAsStringSync(JsonEncoder.withIndent('  ').convert(data.toJson()),
        mode: FileMode.write, flush: true);
  }

  String get _startupID {
    return isDebug ? '$appIdendifier.debug' : appIdendifier;
  }

  HashSet<String> get _schedulers {
    HashSet<String> hash = HashSet();
    var dir = Directory(_schedulerPath);
    if (dir.existsSync()) {
      var fileList = dir.listSync();
      for (var item in fileList) {
        if (item is File) {
          if (path.extension(item.path).toLowerCase().contains('.yaml')) {
            hash.add(path.basenameWithoutExtension(item.path));
          }
        }
      }
    }
    return hash;
  }

  final SystemRuntime runtime;
  final Future<List<ProcessInfo>> Function(String) processFinder;
  final void Function(int) exitHandler;

  SchedulerManager({
    SystemRuntime? runtime,
    Future<List<ProcessInfo>> Function(String)? processFinder,
    void Function(int)? exitHandler,
  })  : runtime = runtime ?? const DefaultSystemRuntime(),
        processFinder = processFinder ?? findProcess,
        exitHandler = exitHandler ?? exit;

  factory SchedulerManager.create({
    SystemRuntime? runtime,
    Future<List<ProcessInfo>> Function(String)? processFinder,
    void Function(int)? exitHandler,
  }) {
    if (Platform.isMacOS) {
      return SchedulerOsx(
        runtime: runtime,
        processFinder: processFinder,
        exitHandler: exitHandler,
      );
    } else if (Platform.isWindows) {
      return SchedulerWindows(
        runtime: runtime,
        processFinder: processFinder,
        exitHandler: exitHandler,
      );
    } else if (Platform.isLinux) {
      return SchedulerLinux(
        runtime: runtime,
        processFinder: processFinder,
        exitHandler: exitHandler,
      );
    } else {
      throw UnsupportedError('This platform not supported.');
    }
  }

  Future regist(String filePath) async {
    var file = File(filePath);
    if (!file.existsSync()) {
      return throw Exception("The file '$filePath' does not exist.");
    }
    Map<String, dynamic>? map;
    DataBuild? build;
    try {
      map = Application.getMapFromYamlA(await file.readAsString());
      build = DataBuild.fromJson(map!);
    } on Exception catch (e, stacktace) {
      print(stacktace);
      return throw Exception(
          "The file '$filePath' is not in yaml format or is an unsupported build format.");
    }

    if (build.scheduler == null) {
      return throw Exception('Scheduler entry does not exist within yaml.');
    }

    var dirLogs = Directory(_logsPath);
    var alias = build.scheduler!.alias;
    var hash = _schedulers;
    if (hash.contains(alias)) {
      return throw Exception(
          'A scheduler with the same alias is registered. Please do it with a different alias.');
    }
    await _checkStartup();
    await _checkProcess();
    await _startProcess();

    if (!dirLogs.existsSync()) {
      dirLogs.createSync(recursive: true);
    }
    var newPath = '$_schedulerPath/$alias.yaml';
    await file.copy(newPath);

    print('');
    print(':::: Scheduler registered.');
    print('');
    print('[alias]'.padRight(8) + alias);
    print('[path]'.padRight(8) + newPath);
    print('');

    //update settings
    var local = _localData;
    var schedulerData = SchedulerData();
    schedulerData.alias = alias;
    local.map[alias] = schedulerData;
    _localData = local;
    return simpleFuture;
  }

  Future unregist(String alias) async {
    if (_schedulers.contains(alias)) {
      var file = File('$_schedulerPath/$alias.yaml');
      file.deleteSync();
    } else {
      return throw Exception("There is no scheduler called '$alias'.");
    }

    var local = _localData;
    if (local.map.containsKey(alias)) {
      local.map.remove(alias);
      _localData = local;
    }

    await _checkTerminate();
    print('');
    print(":::: Scheduler '$alias's registration has been removed.");
  }

  Future showList() async {
    print('');
    var scheduleList = _schedulers;
    if (scheduleList.isEmpty) {
      print(':::: There are no registered schedulers.');
      print('');
    } else {
      print(':::: List of currently registered schedulers.');
      print('');
      print('${'   [Enable]'.padRight(20)}${'[Alias]'.padRight(20)}[Path]');
      var local = _localData;
      for (var item in _schedulers) {
        var enable = true;
        if (local.map.containsKey(item)) {
          enable = local.map[item]!.enable;
        }
        print(
            '${' - $enable'.padRight(20)}${item.padRight(20)}$_schedulerPath/$item.yaml');
      }
      print('');
    }
    await _checkProcess();
    return simpleFuture;
  }

  Future enableScheduler(String alias, bool enable) async {
    var local = _localData;
    if (_schedulers.contains(alias)) {
      var data = SchedulerData();
      data.alias = alias;
      data.enable = enable;
      local.map[alias] = data;
      _localData = local;
      var activate = enable ? 'activated' : 'deactivate';
      print('');
      print("Scheduler '$alias' has been $activate.");
      print('');

      await _checkStartup();
      await _checkProcess();
      await _startProcess();

      exitHandler(0);
    } else {
      return throw Exception("There is no scheduler called '$alias'.");
    }
  }

  Future terminate() async {
    await _checkProcess();
    if (currentScheduler != null) {
      runtime.killPid(currentScheduler!.pid);
      print('Scheduler operation stopped.');
    }
    return simpleFuture;
  }

  Future _checkTerminate() async {
    if (_schedulers.isEmpty) {
      await _unregistStartup();
      await terminate();
    }
    return simpleFuture;
  }

  Future _startProcess() async {
    // Start directly if Launchd did not auto-launch the process
    if (currentScheduler == null) {
      if (runtime.isWindows) {
        final program = Platform.resolvedExecutable;
        var arguments = <String>[];
        if (isDebug) arguments.add(scriptPath);
        arguments.addAll(['scheduler', '-s']);
        await runtime.startExecutable(program, arguments,
            mode: ProcessStartMode.detached);
      } else {
        var script = isDebug ? ' $scriptPath' : '';
        var shell = StringBuffer();
        shell.writeln('nohup $executablePath$script scheduler -s &');
        // shell.writeln('disown');
        print(shell.toString());
        runtime.runShell(shell,
            workspace: Directory.systemTemp.path, printStdout: false);
        await _checkProcess();
      }
    }
    return simpleFuture;
  }

  Future _checkProcess() async {
    var oto = runtime.isWindows ? '$appName.exe' : appName;
    //process check
    var processes = await processFinder(runtime.isWindows
        ? (isDebug ? 'dart.exe' : '$appName.exe')
        : 'scheduler -s');

    for (var process in processes) {
      var processName = process.name;
      if (isDebug) {
        if (processName.contains('main.dart scheduler -s')) {
          currentScheduler = process;
        }
      } else {
        if (processName.contains('$oto scheduler -s')) {
          currentScheduler = process;
        }
      }

      if (currentScheduler != null) {
        print('Scheduler is running on pid [${currentScheduler!.pid}].');
        break;
      }
    }

    return simpleFuture;
  }

  Future _checkStartup() async {
    if (!await OSStartup.isRegistedStartup(_startupID)) {
      List<String> param = [];
      var exe = Platform.resolvedExecutable;
      if (isDebug) {
        param.add(scriptPath);
      } else {
        exe = exePath;
      }
      param.add('scheduler');
      param.add('-s');
      await OSStartup.registStartup(exe, _startupID,
          arguments: param, logErrorFilePath: '$_schedulerPath/launch_err.log');
    }
    return simpleFuture;
  }

  Future _unregistStartup() async {
    if (await OSStartup.isRegistedStartup(_startupID)) {
      await OSStartup.unregistStartup(_startupID);
    }
    return simpleFuture;
  }

  //======================== other process ========================//
  Future start() async {
    _log('Scheduler is running on pid [$pid].');
    while (true) {
      // Check every second for registered scheduler files
      await Future.delayed(Duration(seconds: 1));
      var settings = _localData.map;
      for (var alias in _schedulers) {
        var enable =
            settings.containsKey(alias) ? settings[alias]!.enable : true;
        if (!_map.containsKey(alias) && enable) {
          _log('Schduler found: $alias');
          var handler = IsolateManager.create(IsolateScheduler(
              {'alias': alias, 'root': _schedulerPath, 'logs': _logsPath}));
          handler.addListener('terminated', _onTerminated);
          handler.addListener('log', _onLog);
          _map[alias] = handler;
        }
        if (_map.containsKey(alias) && !enable) {
          _removeHandler(alias);
        }
      }
    }
  }

  void _removeHandler(String alias) {
    var handler = _map.remove(alias);
    handler!.exit();
  }

  void _onTerminated(Object? data) async {
    var map = data as Map;
    var alias = map['alias'] as String;
    var type = map['type'] as String;
    var message = map['message'] as String;
    _removeHandler(alias);
    switch (type) {
      case 'error':
        _log('[Error] $message');
        break;
    }

    var local = _localData;
    var list = _schedulers;
    if (list.isEmpty) {
      _log('Scheduler operation stopped.');
      if (local.map.containsKey(alias)) {
        local.map.remove(alias);
      }
    } else {
      list.contains(alias);
      SchedulerData data = SchedulerData();
      if (local.map.containsKey(alias)) {
        data = local.map[alias]!;
      }
      data.alias = alias;
      data.enable = false;
      local.map[alias] = data;
    }
    _localData = local;
    await _checkTerminate();
  }

  void _onLog(Object? data) {
    _log(data as String);
  }

  void _log(String message) {
    logFile.writeAsStringSync('[${getDates()}] $message\n',
        mode: FileMode.append, flush: true);
  }

  // visible for testing
  Future testStartProcess() => _startProcess();
  Future testCheckProcess() => _checkProcess();
}
