// ignore_for_file: avoid_init_to_null

import 'dart:io';
import 'package:dart_framework/utils/path.dart';
import 'package:oto/oto/application.dart';
import 'package:oto/oto/core/execution_context.dart';

/// OTO Tag System
///
/// Handles the tag syntax used in YAML pipelines to reference dynamic values.
///
/// ## Read tags (value substitution)
///
/// ```yaml
/// exe: BuildiOS
/// param:
///   workspace: <!property.workspace>       # direct property access
///   version:   <!property.build.version>   # nested property access
///   state:     <!state.buildStep>          # commandStates access (ready/progress/complete)
/// ```
///
/// When a tag occupies the entire string, the original type is returned (String→Any).
/// When a tag is embedded within a string, it is inserted via toString().
///
/// ## Write tags (value storage)
///
/// ```yaml
/// exe: SetValue
/// param:
///   set-version: <@property.appVersion>   # store value into property
/// ```
///
/// Used when calling `setPropertyValue()` or `setProperty()`.
class TagSystem {
  /// Read tag pattern: `<!namespace.key>`
  static RegExp regEx = RegExp(r'(<!)([_a-zA-Z0-9.-]+)(>)');

  /// Write tag pattern: `<@namespace.key>`
  static RegExp regSetEx = RegExp(r'(<@)([_a-zA-Z0-9.-]+)(>)');

  static ExecutionContext get _defaultContext => Application.instance.context;

  /// Recursively replaces tags in all keys and values of a Map.
  ///
  /// If [replace] is true, modifies and returns the original [map] in place.
  static Map<String, dynamic> replaceAllTagsMap(Map<String, dynamic> map,
      {bool replace = false, ExecutionContext? context}) {
    var newMap = <String, dynamic>{};
    var deleteKeys = <String>[];
    for (var pair in map.entries) {
      var key = pair.key;
      var keyReplacedTag = TagSystem.replaceTagValue(key, context: context);
      if (keyReplacedTag != key) {
        deleteKeys.add(key);
      }
      if (map[key] is String) {
        newMap[keyReplacedTag] =
            TagSystem.replaceTagValue(map[key], context: context);
      } else if (map[key] is List) {
        newMap[keyReplacedTag] =
            replaceAllTagsList(map[key], replace: replace, context: context);
      } else if (map[key] is Map) {
        newMap[keyReplacedTag] =
            replaceAllTagsMap(map[key], replace: replace, context: context);
      } else {
        newMap[keyReplacedTag] = map[key];
      }
      if (replace) {
        map[keyReplacedTag] = newMap[keyReplacedTag];
      }
    }
    if (replace) {
      for (var key in deleteKeys) {
        map.remove(key);
      }
      return map;
    }
    return newMap;
  }

  /// Recursively replaces tags in all elements of a List.
  static List replaceAllTagsList(List list,
      {bool replace = false, ExecutionContext? context}) {
    var newList = <dynamic>[];
    for (var i = 0; i < list.length; ++i) {
      if (list[i] is String) {
        newList.add(TagSystem.replaceTagValue(list[i], context: context));
      } else if (list[i] is List) {
        newList.add(replaceAllTagsList(list[i], context: context));
      } else if (list[i] is Map) {
        newList.add(replaceAllTagsMap(list[i], context: context));
      } else {
        newList.add(list[i]);
      }
      if (replace) {
        list[i] = newList[i];
      }
    }
    if (replace) {
      return newList;
    }
    return newList;
  }

  /// Stores a value into the property specified by a write tag (`<@property.key>`).
  static Future setPropertyValue(String tag, dynamic value,
      {bool? showPrint, ExecutionContext? context}) async {
    final runtimeContext = context ?? _defaultContext;
    var match = regSetEx.firstMatch(tag);
    if (match == null) {
      throw Exception('Please put the correct value. (ex: <@property.value>)');
    }
    tag = match.group(2)!;
    if (tag.startsWith('property')) {
      var key = tag.replaceFirst('property.', '');
      runtimeContext.property[key] = value;
      if (showPrint ?? true) {
        if (value is List) {
          Application.log('[Return] $key :\n');
          var index = 0;
          var length = value.length.toString().length;
          for (var item in value) {
            var indexStr = index.toString().padLeft(length);
            Application.log('$indexStr - $item');
            index++;
          }
        } else {
          Application.log('[Return] $key : $value');
        }
      }
    }
    return Future.value();
  }

  /// Looks up the actual value for a tag name.
  ///
  /// - `property.key` → `context.property[key]`
  /// - `property.nested.key` → traverses nested Maps
  /// - `state.commandId` → `context.commandStates[commandId]`
  static dynamic _getTagValue(String value, {ExecutionContext? context}) {
    final runtimeContext = context ?? _defaultContext;
    var setted = false;
    dynamic data;
    if (value.startsWith('property.')) {
      var map = runtimeContext.property;
      var param = value.replaceFirst('property.', '');
      if (map.containsKey(param)) {
        data = map[param];
        setted = true;
      } else {
        var params = param.split('.');
        if (params.length > 1) {
          var subMap = map;
          for (var index = 0; index < params.length; index++) {
            if (subMap.containsKey(params[index])) {
              var value = subMap[params[index]];
              if (value is Map<String, dynamic>) {
                subMap = value;
              } else {
                data = value;
                setted = true;
              }
            }
          }
        }
      }

      if (!setted) {
        Application.log('"$value" is not exist in property');
        data = null;
        setted = true;
      }
    }

    if (value.startsWith('state.')) {
      var map = runtimeContext.commandStates;
      var param = value.replaceFirst('state.', '');
      if (map.containsKey(param)) {
        var value = map[param].toString().replaceAll('CommandState.', '');
        data = value == '' ? null : value;
      } else {
        data = null;
      }
      setted = true;
    }

    if (!setted) {
      throw Exception(
          'TagSystem.getTagValue: "$value" is not defined in property.');
    }
    return data;
  }

  /// Converts a relative path (`.`, `..`) to an absolute path, also resolving any tags within it.
  static String getAbs(String path, {ExecutionContext? context}) {
    path = getPathNormal(path, context: context);
    if (path == '.' || path.startsWith('./') || path.startsWith('.\\')) {
      path = path.replaceFirst('.', getPathNormal(Application.current));
    }
    if (path == '..' || path.startsWith('../') || path.startsWith('..\\')) {
      path =
          path.replaceFirst('..', '${getPathNormal(Application.current)}/..');
    }
    return File(path).absolute.path;
  }

  /// Normalizes path separators and resolves tags within the path.
  static String getPathNormal(String path, {ExecutionContext? context}) {
    return replaceTagValue(Path.getNormal(path), context: context);
  }

  /// Replaces tags (`<!...>`) within a string.
  ///
  /// If the tag occupies the entire string, returns the original type (dynamic).
  /// If the tag is embedded within a string, it is inserted as a string.
  static dynamic replaceTagValue(String raw, {ExecutionContext? context}) {
    var list = regEx.allMatches(raw);
    if (list.length == 1) {
      var item = list.first;
      var sub = list.first.input.substring(item.start, item.end);
      if (sub == raw) {
        return replaceSingleTagValue(raw, context: context);
      }
    }

    for (var match in list) {
      var param = match.group(2);
      var target = '<!${param!}>';
      raw = raw.replaceAll(
          target, _getTagValue(param, context: context).toString());
    }
    return raw;
  }

  /// Returns the value in its original type from a string that contains exactly one tag.
  static dynamic replaceSingleTagValue(String raw, {ExecutionContext? context}) {
    dynamic value;
    var list = regEx.allMatches(raw);
    for (var match in list) {
      var param = match.group(2);
      value = _getTagValue(param!, context: context);
    }
    return value;
  }
}
