/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

package org.apache.jena.http.auth;

import static org.apache.jena.http.auth.AuthHttp.A1;
import static org.apache.jena.http.auth.AuthHttp.A2_auth;
import static org.apache.jena.http.auth.AuthHttp.H;
import static org.apache.jena.http.auth.AuthHttp.KD;

import java.net.Authenticator;
import java.net.Authenticator.RequestorType;
import java.net.PasswordAuthentication;
import java.net.http.HttpClient;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.Optional;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;

import org.apache.jena.atlas.lib.Pair;
import org.apache.jena.atlas.web.HttpException;
import org.apache.jena.riot.web.HttpNames;

class DigestLib {

    /** From the challenge, username and password, calculate the response.field. */
    public static String calcDigestChallengeResponse(AuthChallenge auth,
                                                     String username, String password,
                                                     String method, String requestTarget,
                                                     String cnonce, String nc, String authType) {
        DigestAlgorithm dAlgo = getAlgorithm(auth);
        if ( dAlgo == null )
            // Unrecognized algorithm name.
            return null;
        String a1 = A1(username, auth.realm, password, dAlgo) ;
        if ( auth.qop == null ) {
            // RFC 2069
            // Firefox seems to prefer this form??
            return KD(H(a1, dAlgo), auth.nonce+":"+H(A2_auth(method, requestTarget), dAlgo), dAlgo) ;
        }
        else {
            Objects.nonNull(cnonce) ;
            Objects.nonNull(nc) ;
            return KD(H(a1, dAlgo),
                      auth.nonce+":"+nc+":"+cnonce+":"+authType+":"+H(A2_auth(method, requestTarget), dAlgo),
                      dAlgo
                    ) ;
        }
    }

    public static DigestAlgorithm getAlgorithm(AuthChallenge challenge) {
        if ( challenge.algorithm == null )
            // Default by RFC 7616
            return DigestAlgorithm.MD5;
        // null means ignore.
        return DigestAlgorithm.fromString(challenge.algorithm);
    }

    private static Random nonceGenerator = new SecureRandom();

    /**
     * Generate a nonce for the client to use in a digest auth session. Each call of
     * this static method returns a new string (to within the limits of random number
     * generation).
     */
    public static String generateNonce() {
        return String.format("%08X",nonceGenerator.nextLong());
    }

    /** Extract user and password from a {@link HttpClient}. */
    private static Pair<String, String> getUserNameAndPassword(HttpClient httpClient) {
        Optional<Authenticator> optAuth = httpClient.authenticator();
        if ( optAuth.isEmpty() )
            throw HttpException.error("Username/password required but not present in HttpClient");
        // We just want the PasswordAuthentication!
        PasswordAuthentication x = optAuth.orElseThrow().requestPasswordAuthenticationInstance(null,
                                                                                               null,
                                                                                               -1,   //port,
                                                                                               null, //protocol,
                                                                                               null, //prompt,
                                                                                               null, //scheme,
                                                                                               null, //url,
                                                                                               RequestorType.SERVER);
        String user = x.getUserName();
        String password = new String(x.getPassword());
        return Pair.create(user, password);
    }

    /**
     * Function to modify a {@link java.net.http.HttpRequest.Builder} for digest authentication.
     * One instance of this function is used for each digest session.
     */
    public static AuthRequestModifier digestAuthModifier(AuthChallenge aHeader, String user, String password, String method, String requestTarget) {
        String clientNonce = DigestLib.generateNonce();
        AtomicLong ncCounter = new AtomicLong(0);
        return request->{
            // Bump nc
            String nc = String.format("%08X", ncCounter.getAndIncrement());
            String responseField = calcDigestChallengeResponse(aHeader, user, password,
                                                               method, requestTarget,
                                                               clientNonce, nc, "auth");
            if ( responseField == null ) {
                return null;
                // Unrecognized algorithm. RFC says "ignore request"
            }

            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("Digest ");
            field(stringBuilder, true, "username", user, true);
            field(stringBuilder, false, "realm", aHeader.realm, true);
            field(stringBuilder, false, "algorithm", aHeader.algorithm, false);
            field(stringBuilder, false, "nonce", aHeader.nonce, true);
            field(stringBuilder, false, "uri", requestTarget, true);
            field(stringBuilder, false, "qop", "auth", false);
            field(stringBuilder, false, "cnonce", clientNonce, true);
            field(stringBuilder, false, "nc", nc, false);
            field(stringBuilder, false, "response", responseField, true);
            field(stringBuilder, false, "opaque", aHeader.opaque, true);
            String x = stringBuilder.toString();
            // setHeader - replace previous
            request.setHeader(HttpNames.hAuthorization , x);
            return request;
        };
    }

    private static void field(StringBuilder stringBuilder, boolean first, String name, String value, boolean quotes) {
        if ( value == null )
            return;
        if ( ! first )
            stringBuilder.append(", ");
        stringBuilder.append(name);
        stringBuilder.append("=");
        if ( quotes )
            stringBuilder.append('"');
        stringBuilder.append(value);
        if ( quotes )
            stringBuilder.append('"');
    }
}
