import 'package:platform_support/exceptions.dart';
import 'package:matcher/matcher.dart';
import 'context_aware.dart';
import 'context_validator.dart';

final RegExp _asterisk = RegExp(r'\*$');
final RegExp _forbidden = RegExp(r'!$');
final RegExp _optional = RegExp(r'\?$');

/// Returns a value based the result of a computation.
typedef DefaultValueFunction = Function();

/// Generates an error message based on the given input.
typedef CustomErrorMessageFunction = String Function(dynamic item);

/// Determines if a value is valid.
typedef Filter = bool Function(dynamic value);

/// Converts the desired fields to their numeric representations, if present.
Map<String, dynamic> autoParse(Map inputData, Iterable<String> fields) {
  var data = <String, dynamic>{};

  for (var key in inputData.keys) {
    if (!fields.contains(key)) {
      data[key.toString()] = inputData[key];
    } else {
      try {
        var n = inputData[key] is num
            ? inputData[key]
            : num.parse(inputData[key].toString());
        data[key.toString()] = n == n.toInt() ? n.toInt() : n;
      } catch (e) {
        // Invalid number, don't pass it
      }
    }
  }

  return data;
}

/// Removes undesired fields from a `Map`.
Map<String, dynamic> filter(Map inputData, Iterable<String> only) {
  return inputData.keys.fold(<String, dynamic>{}, (map, key) {
    if (only.contains(key.toString())) map[key.toString()] = inputData[key];
    return map;
  });
}

/// Enforces the validity of input data, according to [Matcher]s.
class Validator extends Matcher {
  /// Pre-defined error messages for certain fields.
  final Map<String, dynamic> customErrorMessages = {};

  /// Values that will be filled for fields if they are not present.
  final Map<String, dynamic> defaultValues = {};

  /// Fields that cannot be present in valid data.
  final List<String> forbiddenFields = [];

  /// Conditions that must be met for input data to be considered valid.
  final Map<String, List<Matcher>> rules = {};

  /// Fields that must be present for data to be considered valid.
  final List<String> requiredFields = [];

  /// Validation error messages.
  final List<String> errorMessages = [];

  void _importSchema(Map<String, dynamic> schema) {
    for (var keys in schema.keys) {
      for (var key in keys.split(',').map((s) => s.trim())) {
        var fieldName = key
            .replaceAll(_asterisk, '')
            .replaceAll(_forbidden, '')
            .replaceAll(_optional, '');
        var isForbidden = _forbidden.hasMatch(key),
            isRequired = _asterisk.hasMatch(key);

        if (isForbidden) {
          forbiddenFields.add(fieldName);
        } else if (isRequired) {
          requiredFields.add(fieldName);
        }

        var tmpIterable =
            schema[keys] is Iterable ? schema[keys] : [schema[keys]];
        var iterable = [];

        void addTo(x) {
          if (x is Iterable) {
            x.forEach(addTo);
          } else {
            iterable.add(x);
          }
        }

        tmpIterable.forEach(addTo);

        for (var rule in iterable) {
          if (rule is Matcher) {
            addRule(fieldName, rule);
          } else if (rule is Filter) {
            addRule(fieldName, predicate(rule));
          } else {
            addRule(fieldName, wrapMatcher(rule));
          }
        }
      }
    }
  }

  Validator.empty();

  Validator(Map<String, dynamic> schema,
      {Map<String, dynamic> defaultValues = const {},
      Map<String, dynamic> customErrorMessages = const {}}) {
    this.defaultValues.addAll(defaultValues);
    this.customErrorMessages.addAll(customErrorMessages);
    _importSchema(schema);
  }

  static bool _hasContextValidators(Iterable it) =>
      it.any((x) => x is ContextValidator);

  /// Validates, and filters input data.
  ValidationResult check(Map inputData) {
    var errors = <String>[];
    var input = Map.from(inputData);
    var data = <String, dynamic>{};

    for (var key in defaultValues.keys) {
      if (!input.containsKey(key)) {
        var value = defaultValues[key];
        input[key] = value is DefaultValueFunction ? value() : value;
      }
    }

    for (var field in forbiddenFields) {
      if (input.containsKey(field)) {
        if (!customErrorMessages.containsKey(field)) {
          errors.add("'$field' is forbidden.");
        } else {
          errors.add(customError(field, input[field]));
        }
      }
    }

    for (var field in requiredFields) {
      if (!_hasContextValidators(rules[field] ?? [])) {
        if (!input.containsKey(field)) {
          if (!customErrorMessages.containsKey(field)) {
            errors.add("'$field' is required.");
          } else {
            errors.add(customError(field, 'none'));
          }
        }
      }
    }

    // Run context validators.

    for (var key in input.keys) {
      if (key is String && rules.containsKey(key)) {
        var valid = true;
        var value = input[key];
        var description = StringDescription("'$key': expected ");

        var rulesList = rules[key] ?? [];
        for (var matcher in rulesList) {
          if (matcher is ContextValidator) {
            if (!matcher.validate(key, input)) {
              errors.add(matcher
                  .errorMessage(description, key, input)
                  .toString()
                  .trim());
              valid = false;
            }
          }
        }

        if (valid) {
          var rulesList = rules[key] ?? [];
          for (var matcher in rulesList) {
            try {
              if (matcher is Validator) {
                var result = matcher.check(value as Map);

                if (result.errors.isNotEmpty) {
                  errors.addAll(result.errors);
                  valid = false;
                  break;
                }
              } else {
                bool result;

                if (matcher is ContextAwareMatcher) {
                  result = matcher.matchesWithContext(value, key, input, {});
                } else {
                  result = matcher.matches(value, {});
                }

                if (!result) {
                  if (!customErrorMessages.containsKey(key)) {
                    errors.add(matcher.describe(description).toString().trim());
                  }
                  valid = false;
                  break;
                }
              }
            } catch (e) {
              if (e is ValidationException) {
                errors.add(e.errors.first);
              } else {
                errors.add(e.toString());
              }

              valid = false;
              break;
            }
          }
        }

        if (valid) {
          data[key] = value;
        } else if (customErrorMessages.containsKey(key)) {
          errors.add(customError(key, input[key]));
        }
      }
    }

    if (errors.isNotEmpty) {
      return ValidationResult().._errors.addAll(errors);
    }

    return ValidationResult().._data.addAll(data);
  }

  /// Validates, and filters input data after running [autoParse].
  ValidationResult checkParsed(Map inputData, List<String> fields) =>
      check(autoParse(inputData, fields));

  /// Renders the given custom error.
  String customError(String key, value) {
    if (!customErrorMessages.containsKey(key)) {
      throw ArgumentError("No custom error message registered for '$key'.");
    }

    var msg = customErrorMessages[key];

    if (msg is String) {
      return msg.replaceAll('{{value}}', value.toString());
    } else if (msg is CustomErrorMessageFunction) {
      return msg(value);
    }

    throw ArgumentError("Invalid custom error message '$key': $msg");
  }

  /// Validates input data, and throws an error if it is invalid.
  ///
  /// Otherwise, the filtered data is returned.
  Map<String, dynamic> enforce(Map inputData,
      {String errorMessage = 'Invalid data.'}) {
    var result = check(inputData);

    if (result._errors.isNotEmpty) {
      throw ValidationException(errorMessage, errors: result._errors);
    }

    return result.data;
  }

  /// Validates, and filters input data after running [autoParse], and throws an error if it is invalid.
  ///
  /// Otherwise, the filtered data is returned.
  Map<String, dynamic> enforceParsed(Map inputData, List<String> fields) =>
      enforce(autoParse(inputData, fields));

  /// Creates a copy with additional validation rules.
  Validator extend(Map<String, dynamic> schema,
      {Map<String, dynamic> defaultValues = const {},
      Map<String, dynamic> customErrorMessages = const {},
      bool overwrite = false}) {
    var tmpSchema = <String, dynamic>{};
    var child = Validator.empty()
      ..defaultValues.addAll(this.defaultValues)
      ..defaultValues.addAll(defaultValues)
      ..customErrorMessages.addAll(this.customErrorMessages)
      ..customErrorMessages.addAll(customErrorMessages)
      ..requiredFields.addAll(requiredFields)
      ..rules.addAll(rules);

    for (var key in schema.keys) {
      var fieldName = key
          .replaceAll(_asterisk, '')
          .replaceAll(_forbidden, '')
          .replaceAll(_optional, '');
      var isForbidden = _forbidden.hasMatch(key);
      var isOptional = _optional.hasMatch(key);
      var isRequired = _asterisk.hasMatch(key);

      if (isForbidden) {
        child
          ..requiredFields.remove(fieldName)
          ..forbiddenFields.add(fieldName);
      } else if (isOptional) {
        child
          ..forbiddenFields.remove(fieldName)
          ..requiredFields.remove(fieldName);
      } else if (isRequired) {
        child
          ..forbiddenFields.remove(fieldName)
          ..requiredFields.add(fieldName);
      }

      if (overwrite) {
        if (child.rules.containsKey(fieldName)) child.rules.remove(fieldName);
      }

      tmpSchema[fieldName] = schema[key];
    }

    return child.._importSchema(tmpSchema);
  }

  /// Adds a [rule].
  void addRule(String key, Matcher rule) {
    if (!rules.containsKey(key)) {
      rules[key] = [rule];
      return;
    }

    if (rules[key] == null) {
      rules[key] = List.empty(growable: true);
    }
    rules[key]?.add(rule);
  }

  /// Adds all given [rules].
  void addRules(String key, Iterable<Matcher> rules) {
    for (var rule in rules) {
      addRule(key, rule);
    }
  }

  /// Removes a [rule].
  void removeRule(String key, Matcher rule) {
    if (rules.containsKey(key)) {
      rules[key]?.remove(rule);
    }
  }

  /// Removes all given [rules].
  void removeRules(String key, Iterable<Matcher> rules) {
    for (var rule in rules) {
      removeRule(key, rule);
    }
  }

  @override
  Description describe(Description description) =>
      description.add(' passes the provided validation schema: $rules');

  @override
  bool matches(item, Map matchState) {
    enforce(item as Map);
    return true;
  }

  @override
  String toString() => 'Validation schema: $rules';
}

/// The result of attempting to validate input data.
class ValidationResult {
  final Map<String, dynamic> _data = {};
  final List<String> _errors = [];

  /// The successfully validated data, filtered from the original input.
  Map<String, dynamic> get data => Map<String, dynamic>.unmodifiable(_data);

  /// A list of errors that resulted in the given data being marked invalid.
  ///
  /// This is empty if validation was successful.
  List<String> get errors => List<String>.unmodifiable(_errors);

  ValidationResult withData(Map<String, dynamic> data) => ValidationResult()
    .._data.addAll(data)
    .._errors.addAll(_errors);

  ValidationResult withErrors(Iterable<String> errors) => ValidationResult()
    .._data.addAll(_data)
    .._errors.addAll(errors);
}

/// Occurs when user-provided data is invalid.
class ValidationException extends PlatformHttpException {
  /// A list of errors that resulted in the given data being marked invalid.
  //@override
  //final List<String> errors = [];

  /// A descriptive message describing the error.
  //@override
  //final String message;

  ValidationException(String message, {Iterable<String> errors = const []})
      : super(
            message: message,
            statusCode: 400,
            errors: (errors).toSet().toList(),
            stackTrace: StackTrace.current) {
    //this.errors.addAll(errors.toSet());
  }

  @override
  String toString() {
    if (errors.isEmpty) {
      return message;
    }

    if (errors.length == 1) {
      return 'Validation error: ${errors.first}';
    }

    var messages = [
      '${errors.length} validation errors:\n',
      ...errors.map((error) => '* $error')
    ];

    return messages.join('\n');
  }
}
