// Copyright (c) 2015, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:collection';

import 'package:string_scanner/string_scanner.dart';

import 'case_insensitive_map.dart';
import 'scan.dart';
import 'utils.dart';

/// A single challenge in a WWW-Authenticate header, parsed as per [RFC 2617][].
///
/// [RFC 2617]: http://tools.ietf.org/html/rfc2617
///
/// Each WWW-Authenticate header contains one or more challenges, representing
/// valid ways to authenticate with the server.
class AuthenticationChallenge {
  /// The scheme describing the type of authentication that's required, for
  /// example "basic" or "digest".
  ///
  /// This is normalized to always be lower-case.
  final String scheme;

  /// The parameters describing how to authenticate.
  ///
  /// The semantics of these parameters are scheme-specific. The keys of this
  /// map are case-insensitive.
  final Map<String, String> parameters;

  /// Parses a WWW-Authenticate header, which should contain one or more
  /// challenges.
  ///
  /// Throws a [FormatException] if the header is invalid.
  static List<AuthenticationChallenge> parseHeader(String header) =>
      wrapFormatException('authentication header', header, () {
        final scanner = StringScanner(header);
        scanner.scan(whitespace);
        final challenges = parseList(scanner, () {
          final scheme = _scanScheme(scanner, whitespaceName: '" " or "="');

          // Manually parse the inner list. We need to do some lookahead to
          // disambiguate between an auth param and another challenge.
          final params = <String, String>{};

          // Consume initial empty values.
          while (scanner.scan(',')) {
            scanner.scan(whitespace);
          }

          _scanAuthParam(scanner, params);

          var beforeComma = scanner.position;
          while (scanner.scan(',')) {
            scanner.scan(whitespace);

            // Empty elements are allowed, but excluded from the results.
            if (scanner.matches(',') || scanner.isDone) continue;

            scanner.expect(token, name: 'a token');
            final name = scanner.lastMatch![0]!;
            scanner.scan(whitespace);

            // If there's no "=", then this is another challenge rather than a
            // parameter for the current challenge.
            if (!scanner.scan('=')) {
              scanner.position = beforeComma;
              break;
            }

            scanner.scan(whitespace);

            if (scanner.scan(token)) {
              params[name] = scanner.lastMatch![0]!;
            } else {
              params[name] = expectQuotedString(scanner,
                  name: 'a token or a quoted string');
            }

            scanner.scan(whitespace);
            beforeComma = scanner.position;
          }

          return AuthenticationChallenge(scheme, params);
        });

        scanner.expectDone();
        return challenges;
      });

  /// Parses a single WWW-Authenticate challenge value.
  ///
  /// Throws a [FormatException] if the challenge is invalid.
  factory AuthenticationChallenge.parse(String challenge) =>
      wrapFormatException('authentication challenge', challenge, () {
        final scanner = StringScanner(challenge);
        scanner.scan(whitespace);
        final scheme = _scanScheme(scanner);

        final params = <String, String>{};
        parseList(scanner, () => _scanAuthParam(scanner, params));

        scanner.expectDone();
        return AuthenticationChallenge(scheme, params);
      });

  /// Scans a single scheme name and asserts that it's followed by a space.
  ///
  /// If [whitespaceName] is passed, it's used as the name for exceptions thrown
  /// due to invalid trailing whitespace.
  static String _scanScheme(StringScanner scanner, {String? whitespaceName}) {
    scanner.expect(token, name: 'a token');
    final scheme = scanner.lastMatch![0]!.toLowerCase();

    scanner.scan(whitespace);

    // The spec specifically requires a space between the scheme and its
    // params.
    if (scanner.lastMatch == null || !scanner.lastMatch![0]!.contains(' ')) {
      scanner.expect(' ', name: whitespaceName);
    }

    return scheme;
  }

  /// Scans a single authentication parameter and stores its result in [params].
  static void _scanAuthParam(
      StringScanner scanner, Map<String, String> params) {
    scanner.expect(token, name: 'a token');
    final name = scanner.lastMatch![0]!;
    scanner.scan(whitespace);
    scanner.expect('=');
    scanner.scan(whitespace);

    if (scanner.scan(token)) {
      params[name] = scanner.lastMatch![0]!;
    } else {
      params[name] =
          expectQuotedString(scanner, name: 'a token or a quoted string');
    }

    scanner.scan(whitespace);
  }

  /// Creates a new challenge value with [scheme] and [parameters].
  AuthenticationChallenge(this.scheme, Map<String, String> parameters)
      : parameters = UnmodifiableMapView(CaseInsensitiveMap.from(parameters));
}
