import 'dart:collection';
import 'package:angel3_framework/angel3_framework.dart';
import 'dart:convert';
import 'package:crypto/crypto.dart';

/// Calls [BASE64URL], but also works for strings with lengths
/// that are *not* multiples of 4.
String decodeBase64(String str) {
  var output = str.replaceAll('-', '+').replaceAll('_', '/');

  switch (output.length % 4) {
    case 0:
      break;
    case 2:
      output += '==';
      break;
    case 3:
      output += '=';
      break;
    default:
      throw 'Illegal base64url string!"';
  }

  return utf8.decode(base64Url.decode(output));
}

class AuthToken {
  final SplayTreeMap<String, String> _header =
      SplayTreeMap.from({'alg': 'HS256', 'typ': 'JWT'});

  String? ipAddress;
  late DateTime issuedAt;
  num lifeSpan;
  var userId;
  Map<String, dynamic> payload = {};

  AuthToken(
      {this.ipAddress,
      this.lifeSpan = -1,
      this.userId,
      DateTime? issuedAt,
      Map payload = const {}}) {
    this.issuedAt = issuedAt ?? DateTime.now();
    this.payload.addAll(payload.keys
            .fold({}, ((out, k) => out?..[k.toString()] = payload[k])) ??
        {});
    /*
    this.payload.addAll(payload.keys.fold(
            {},
            ((out, k) => out..[k.toString()] = payload[k])
                as Map<String, dynamic>? Function(
                    Map<String, dynamic>?, dynamic)) ??
        {});
    */
  }

  factory AuthToken.fromJson(String jsons) =>
      AuthToken.fromMap(json.decode(jsons) as Map);

  factory AuthToken.fromMap(Map data) {
    return AuthToken(
        ipAddress: data['aud'].toString(),
        lifeSpan: data['exp'] as num,
        issuedAt: DateTime.parse(data['iat'].toString()),
        userId: data['sub'],
        payload: data['pld'] as Map);
  }

  factory AuthToken.parse(String jwt) {
    var split = jwt.split('.');

    if (split.length != 3) {
      throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
    }

    var payloadString = decodeBase64(split[1]);
    return AuthToken.fromMap(json.decode(payloadString) as Map);
  }

  factory AuthToken.validate(String jwt, Hmac hmac) {
    var split = jwt.split('.');

    if (split.length != 3) {
      throw AngelHttpException.notAuthenticated(message: 'Invalid JWT.');
    }

    // var headerString = decodeBase64(split[0]);
    var payloadString = decodeBase64(split[1]);
    var data = split[0] + '.' + split[1];
    var signature = base64Url.encode(hmac.convert(data.codeUnits).bytes);

    if (signature != split[2]) {
      throw AngelHttpException.notAuthenticated(
          message: 'JWT payload does not match hashed version.');
    }

    return AuthToken.fromMap(json.decode(payloadString) as Map);
  }

  String serialize(Hmac hmac) {
    var headerString = base64Url.encode(json.encode(_header).codeUnits);
    var payloadString = base64Url.encode(json.encode(toJson()).codeUnits);
    var data = headerString + '.' + payloadString;
    var signature = hmac.convert(data.codeUnits).bytes;
    return data + '.' + base64Url.encode(signature);
  }

  Map toJson() {
    return _splayify({
      "iss": "angel_auth",
      "aud": ipAddress,
      "exp": lifeSpan,
      "iat": issuedAt.toIso8601String(),
      "sub": userId,
      "pld": _splayify(payload)
    });
  }
}

SplayTreeMap _splayify(Map map) {
  var data = {};
  map.forEach((k, v) {
    data[k] = _splay(v);
  });
  return SplayTreeMap.from(data);
}

dynamic _splay(value) {
  if (value is Iterable) {
    return value.map(_splay).toList();
  } else if (value is Map) {
    return _splayify(value);
  } else {
    return value;
  }
}