// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'dart:math';

import 'package:wyatt_http_client/src/utils/convert.dart';
import 'package:wyatt_http_client/src/utils/crypto.dart';

class DigestAuth { // request counter

  DigestAuth(this.username, this.password);
  String username;
  String password;

  // must get from first response
  String? _algorithm;
  String? _qop;
  String? _realm;
  String? _nonce;
  String? _opaque;

  int _nc = 0;

  /// Splits WWW-Authenticate header into a map.
  Map<String, String>? splitWWWAuthenticateHeader(String header) {
    if (!header.startsWith('Digest ')) {
      throw ArgumentError.value(
        header,
        'header',
        'Header must start with "Digest "',
      );
    }
    final h = header.substring(7); // remove 'Digest '
    final ret = <String, String>{};

    final components = h.split(',').map((token) => token.trim());
    for (final component in components) {
      final kv = component.split('=');
      ret[kv[0]] = kv.getRange(1, kv.length).join('=').replaceAll('"', '');
    }
    return ret;
  }

  String _computeNonce() {
    final rnd = Random.secure();
    final values = List<int>.generate(16, (i) => rnd.nextInt(256));

    return Convert.toHex(values);
  }

  String _formatNonceCount(int nc) => nc.toRadixString(16).padLeft(8, '0');

  String _computeHA1(
    String realm,
    String? algorithm,
    String username,
    String password,
    String? nonce,
    String? cnonce,
  ) {
    if (algorithm == null || algorithm == 'MD5') {
      final token1 = '$username:$realm:$password';
      return Crypto.md5Hash(token1);
    } else if (algorithm == 'MD5-sess') {
      final token1 = '$username:$realm:$password';
      final md51 = Crypto.md5Hash(token1);
      final token2 = '$md51:$nonce:$cnonce';
      return Crypto.md5Hash(token2);
    } else {
      throw ArgumentError.value(
        algorithm,
        'algorithm',
        'Unsupported algorithm',
      );
    }
  }

  Map<String, String?> _computeResponse(
    String method,
    String path,
    String body,
    String? algorithm,
    String? qop,
    String? opaque,
    String realm,
    String? cnonce,
    String? nonce,
    int nc,
    String username,
    String password,
  ) {
    final ret = <String, String?>{};

    final ha1 =
        _computeHA1(realm, algorithm, username, password, nonce, cnonce);

    String ha2;

    if (qop == 'auth-int') {
      final bodyHash = Crypto.md5Hash(body);
      final token2 = '$method:$path:$bodyHash';
      ha2 = Crypto.md5Hash(token2);
    } else {
      // qop in [null, auth]
      final token2 = '$method:$path';
      ha2 = Crypto.md5Hash(token2);
    }

    final nonceCount = _formatNonceCount(nc);
    ret['username'] = username;
    ret['realm'] = realm;
    ret['nonce'] = nonce;
    ret['uri'] = path;
    if (qop != null) {
      ret['qop'] = qop;
    }
    ret['nc'] = nonceCount;
    ret['cnonce'] = cnonce;
    if (opaque != null) {
      ret['opaque'] = opaque;
    }
    ret['algorithm'] = algorithm;

    if (qop == null) {
      final token3 = '$ha1:$nonce:$ha2';
      ret['response'] = Crypto.md5Hash(token3);
    } else if (qop == 'auth' || qop == 'auth-int') {
      final token3 = '$ha1:$nonce:$nonceCount:$cnonce:$qop:$ha2';
      ret['response'] = Crypto.md5Hash(token3);
    }

    return ret;
  }

  String getAuthString(String method, Uri url) {
    final cnonce = _computeNonce();
    _nc += 1;
    // if url has query parameters, append query to path
    final path = url.hasQuery ? '${url.path}?${url.query}' : url.path;

    // after the first request we have the nonce, so we can provide credentials
    final authValues = _computeResponse(
      method,
      path,
      '',
      _algorithm,
      _qop,
      _opaque,
      _realm!,
      cnonce,
      _nonce,
      _nc,
      username,
      password,
    );
    final authValuesString = authValues.entries
        .where((e) => e.value != null)
        .map((e) => [e.key, '="', e.value, '"'].join())
        .toList()
        .join(', ');
    final authString = 'Digest $authValuesString';
    return authString;
  }

  void initFromAuthenticateHeader(String? authInfo) {
    if (authInfo == null) {
      throw ArgumentError.notNull('authInfo');
    }
    final values = splitWWWAuthenticateHeader(authInfo);
    if (values != null) {
      _algorithm = values['algorithm'] ?? _algorithm;
      _qop = values['qop'] ?? _qop;
      _realm = values['realm'] ?? _realm;
      _nonce = values['nonce'] ?? _nonce;
      _opaque = values['opaque'] ?? _opaque;
      _nc = 0;
    }
  }

  bool isReady() => _nonce != null && (_nc == 0 || _qop != null);
}
