// ignore_for_file: constant_identifier_names, depend_on_referenced_packages, avoid_init_to_null

import 'dart:async';
import 'dart:io';
import 'package:dart_framework/platform/process.dart';
import 'package:dart_framework/utils/system_util.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/commands/command_runtime.dart';
import 'package:oto/oto/core/execution_context.dart';
import 'package:oto/oto/core/tag_system.dart';
import 'package:oto/oto/data/command_data.dart';
import 'package:oto/oto/pipeline/pipeline_exe_handle.dart';

enum CommandType {
  SimpleCommand,
  Print,
  SetValue,
  CreateAppData,
  BuildDart,
  BuildFlutter,
  BuildDartCompile,
  BuildDotNet,
  BuildiOS,
  TestflightUpload,
  TestflightStatusCheck,
  TestflightDistribute,
  XcodeprojAddFile,
  BuildMSBuild,
  CodeSign,
  CodeSignVerify,
  ProductBuild,
  Notarize,
  ArchiveiOS,
  ExportiOS,
  PublishiOS,
  Upload,
  Download,
  Copy,
  Rename,
  Delete,
  FileDiffCheck,
  FileRead,
  FileWrite,
  Files,
  FileInfo,
  DirectoryCreate,
  Slack,
  SlackFile,
  SlackBuild,
  Mattermost,
  MattermostBuild,
  StringSub,
  StringReplace,
  StringReplacePattern,
  StringIndex,
  URLInfo,
  Jira,
  Zip,
  Shell,
  ShellFile,
  Git,
  GitCommit,
  GitPull,
  GitPush,
  GitCheckout,
  GitRev,
  GitCount,
  GitReset,
  GitStashPush,
  GitStashApply,
  GitBranch,
  GitHub,
  GitHubPullRequestCreate,
  GitHubPullRequestList,
  GitHubPullRequestClose,
  Protobuf,
  JsonReader,
  JsonReaderFile,
  JsonWriterFile,
  AwsCli,
  Docker,
  Gradle,
  WebRequest,
  WebFile,
  ProcessPortUse,
  ProcessRun,
  ProcessKill,
  SMBAuth,
  Delay,
  JenkinsParameterModify
}

class CommandSpec {
  final CommandType type;
  final String category;
  final String dataModel;
  final String? samplePath;

  const CommandSpec({
    required this.type,
    required this.category,
    required this.dataModel,
    this.samplePath,
  });

  Map<String, String> toCatalogRow() => {
        'type': type.name,
        'category': category,
        'dataModel': dataModel,
        'samplePath': samplePath ?? '',
      };
}

abstract class Command {
  static final Map<CommandType, Command Function()> _commandMap = {};
  static final Map<CommandType, CommandSpec> _specMap = {};

  static void register(
    CommandType type,
    Command Function() factory, {
    CommandSpec? spec,
  }) {
    _commandMap[type] = factory;
    if (spec != null) {
      assert(spec.type == type,
          'CommandSpec.type (${spec.type}) must match registered type ($type)');
      _specMap[type] = spec;
    }
  }

  static Map<CommandType, CommandSpec> get specs => Map.unmodifiable(_specMap);

  static List<Map<String, String>> get catalogRows {
    final rows = _specMap.values.map((spec) => spec.toCatalogRow()).toList();
    rows.sort((a, b) {
      final byCategory = a['category']!.compareTo(b['category']!);
      return byCategory == 0 ? a['type']!.compareTo(b['type']!) : byCategory;
    });
    return List.unmodifiable(rows);
  }

  static Iterable<CommandType> get registeredTypes => _commandMap.keys;

  factory Command.byType(CommandType type) {
    final factory = _commandMap[type];
    if (factory == null) {
      throw StateError(
          'Command "$type" is not registered. Did you call registerAllCommands()?');
    }
    return factory();
  }

  Command();
  late String id;

  ExecutionContext? context;
  ExecutionContext get runtimeContext => context ?? Application.instance.context;

  static RegExp get regEx => TagSystem.regEx;
  static RegExp get regSetEx => TagSystem.regSetEx;

  static Map<String, dynamic> replaceAllTagsMap(Map<String, dynamic> map,
          {bool replace = false}) =>
      TagSystem.replaceAllTagsMap(map, replace: replace);

  static List replaceAllTagsList(List list, {bool replace = false}) =>
      TagSystem.replaceAllTagsList(list, replace: replace);

  static Future setPropertyValue(String tag, dynamic value,
          {bool? showPrint}) =>
      TagSystem.setPropertyValue(tag, value, showPrint: showPrint);

  static String getAbs(String path) => TagSystem.getAbs(path);

  static String getPathNormal(String path) => TagSystem.getPathNormal(path);

  static dynamic replaceTagValue(String raw) => TagSystem.replaceTagValue(raw);

  static dynamic replaceSingleTagValue(String raw) =>
      TagSystem.replaceSingleTagValue(raw);

  late ExeHandleData? exeHandle = null;

  static CommandRuntime defaultRuntime = const DefaultCommandRuntime();
  CommandRuntime? _runtime;
  CommandRuntime get runtime => _runtime ?? defaultRuntime;
  set runtime(CommandRuntime value) => _runtime = value;

  late String _workspace;
  String get workspace {
    return _workspace;
  }

  Map<String, dynamic> get property => runtimeContext.property;

  DataCommon? get commonData => runtimeContext.commonData;

  String getWorkspace(String? dataWorkspace) {
    if (dataWorkspace == null) {
      // No workspace on model: prefer workspace from property
      if (property.containsKey('workspace')) {
        return getAbs(property['workspace']);
      }
      // No workspace in property either: fall back to the current project root
      else {
        return getAbs(commonData?.workspace ?? '.');
      }
    } else {
      return getAbs(dataWorkspace);
    }
  }

  String getCDPath(String path) {
    String drive = '';
    if (Platform.isWindows) {
      drive = '${path[0]}: $and ';
    }
    return '${drive}cd $path';
  }

  String replaceTag(String raw) {
    return TagSystem.replaceTagValue(raw, context: runtimeContext);
  }

  dynamic replaceSingleTag(String raw) {
    return TagSystem.replaceSingleTagValue(raw, context: runtimeContext);
  }

  Future setProperty(String tag, dynamic value, {bool? showPrint}) async {
    await TagSystem.setPropertyValue(tag, value,
        showPrint: showPrint, context: runtimeContext);
  }

  Map<String, dynamic> getParam(DataCommand command) {
    Map<String, dynamic> map;
    if (command.param == null) {
      map = <String, dynamic>{};
    } else {
      map = TagSystem.replaceAllTagsMap(command.param, context: runtimeContext);
    }
    map['workspace'] = _workspace = getWorkspace(map['workspace']);
    return map;
  }

  Future execute(DataCommand command) async {
    return simpleFuture;
  }

  Future executeHandle(DataCommand command, ExeHandleData handleData) async {
    exeHandle = handleData;
    try {
      return await execute(command);
    } on Exception {
      return await exeHandle!.pipelineFail!.execute();
    }
  }

  Future<List<int>> getMergedPassExitCode(
      Map<String, dynamic>? param, List<int>? passExitCodes) async {
    var c = Completer<List<int>>();
    var passExitCodesMerged = [0];
    if (param != null) {
      var data = DataParam.fromJson(param);
      if (data.passExitCodes != null) {
        passExitCodesMerged.addAll(data.passExitCodes!);
      }
    }
    if (passExitCodes != null) {
      passExitCodesMerged.addAll(passExitCodes);
    }
    c.complete(passExitCodesMerged);
    return c.future;
  }

  Future completeProcess(ProcessData processData, DataCommand command,
      {List<int>? passExitCodes}) async {
    var exitCode = processData.exitCode ?? await processData.process.exitCode;
    var passExitCodesMerge =
        await getMergedPassExitCode(command.param, passExitCodes);
    String? resultMessage = null;
    if (exitCode != 0 && passExitCodesMerge.contains(exitCode)) {
      resultMessage = processData.stderr.toString();
    }
    return await complete(exitCode, command,
        errorMessage: processData.stderr.toString(),
        passExitCodes: passExitCodes,
        resultMessage: resultMessage);
  }

  Future<dynamic> complete(int exitCode, DataCommand command,
      {String? errorMessage,
      List<int>? passExitCodes,
      String? resultMessage}) async {
    var passExitCodesMerge =
        await getMergedPassExitCode(command.param, passExitCodes);
    if (command.param != null) {
      var data = DataParam.fromJson(command.param);
      if (data.setExitCode != null) {
        await setProperty(data.setExitCode!, exitCode);
      }
      if (data.setResult != null) {
        await setProperty(data.setResult!, resultMessage);
      }
    }
    if (exeHandle == null) {
      if (passExitCodesMerge.contains(exitCode)) {
        return simpleFuture;
      } else {
        Application.log('Exit Code: $exitCode');
        var data = ExceptionData();
        data.phase = command.name;
        data.message = errorMessage;
        throw Exception(data);
      }
    } else {
      if (passExitCodesMerge.contains(exitCode)) {
        return await exeHandle!.pipelineSuccess!.execute();
      } else {
        return await exeHandle!.pipelineFail!.execute();
      }
    }
  }
}

//Templete
class SimpleCommand extends Command {
  @override
  Future execute(DataCommand command) async {
    Application.log(command.param);
    return simpleFuture;
  }
}

class ExceptionData {
  String? phase;
  String? message;

  @override
  String toString() {
    var str = ' [$phase]\n\n';
    str += message ??= '';
    return str;
  }
}
