/**
 *  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
 *
 *    http://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.
 *
 */
package org.apache.kerby.has.server.kdc;

import org.apache.kerby.has.common.util.HasUtil;
import org.apache.kerby.has.server.HasServer;
import org.apache.kerby.kerberos.kerb.KrbCodec;
import org.apache.kerby.kerberos.kerb.KrbErrorCode;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.client.KrbContext;
import org.apache.kerby.kerberos.kerb.common.EncryptionUtil;
import org.apache.kerby.kerberos.kerb.common.KrbUtil;
import org.apache.kerby.kerberos.kerb.server.KdcConfigKey;
import org.apache.kerby.kerberos.kerb.server.KdcContext;
import org.apache.kerby.kerberos.kerb.server.KdcRecoverableException;
import org.apache.kerby.kerberos.kerb.server.KdcServer;
import org.apache.kerby.kerberos.kerb.server.preauth.PreauthHandler;
import org.apache.kerby.kerberos.kerb.server.request.AsRequest;
import org.apache.kerby.kerberos.kerb.server.request.KdcRequest;
import org.apache.kerby.kerberos.kerb.type.KerberosTime;
import org.apache.kerby.kerberos.kerb.type.base.AuthToken;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionKey;
import org.apache.kerby.kerberos.kerb.type.base.EncryptionType;
import org.apache.kerby.kerberos.kerb.type.base.HostAddress;
import org.apache.kerby.kerberos.kerb.type.base.HostAddresses;
import org.apache.kerby.kerberos.kerb.type.base.KrbError;
import org.apache.kerby.kerberos.kerb.type.base.KrbMessage;
import org.apache.kerby.kerberos.kerb.type.base.KrbToken;
import org.apache.kerby.kerberos.kerb.type.base.PrincipalName;
import org.apache.kerby.kerberos.kerb.type.base.TokenFormat;
import org.apache.kerby.kerberos.kerb.type.kdc.AsReq;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcOption;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcOptions;
import org.apache.kerby.kerberos.kerb.type.kdc.KdcReqBody;
import org.apache.kerby.kerberos.kerb.type.pa.PaData;
import org.apache.kerby.kerberos.kerb.type.pa.PaDataEntry;
import org.apache.kerby.kerberos.kerb.type.pa.PaDataType;
import org.apache.kerby.kerberos.kerb.type.pa.token.PaTokenRequest;
import org.apache.kerby.kerberos.kerb.type.pa.token.TokenInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;

public class HasKdcHandler {
    private static final Logger LOG = LoggerFactory.getLogger(HasKdcHandler.class);

    private KdcContext kdcContext;
    private KrbContext krbContext;
    private KdcServer kdcServer;

    /**
     * Constructor with has server.
     *
     * @param hasServer has server
     */
    public HasKdcHandler(HasServer hasServer) {
        this.krbContext = new KrbContext();
        this.krbContext.init(hasServer.getKrbSetting());
        this.kdcServer = hasServer.getKdcServer();
        prepareHandler(kdcServer);
    }

    public KrbContext getKrbContext() {
        return krbContext;
    }

    public KdcContext getKdcContext() {
        return kdcContext;
    }

    private KdcServer getKdcServer() {
        return kdcServer;
    }

    private void prepareHandler(KdcServer kdcServer) {
        this.kdcContext = new KdcContext(kdcServer.getKdcSetting());
        this.kdcContext.setIdentityService(kdcServer.getIdentityService());
        PreauthHandler preauthHandler = new PreauthHandler();
        preauthHandler.init();
        this.kdcContext.setPreauthHandler(preauthHandler);
    }

    private String getAudience(String name) {
        return name + "/" + getKdcContext().getKdcRealm() + "@" + getKdcContext().getKdcRealm();
    }

    public KrbMessage getResponse(AuthToken authToken, String passPhrase) {
        KrbMessage krbMessage = null;
        try {
            krbMessage = handleMessage(authToken, passPhrase);
        } catch (KrbException e) {
            LOG.error("Failed to handle message. " + e.getMessage());
        }
        return krbMessage;
    }

    /**
     * Process the client request message.
     *
     * @param authToken the auth token
     * @param passPhrase the password phrase
     * @return KrbMessage KRB message
     * @throws KrbException KRB exception when handling message
     */
    public KrbMessage handleMessage(AuthToken authToken, String passPhrase) throws KrbException {

        // set the audiences
        List<String> auds = new ArrayList<>();
        String audience = getAudience("krbtgt");
        auds.add(audience);
        authToken.setAudiences(auds);

        AsReq asReq = createAsReq(authToken);
        KdcRequest kdcRequest = new AsRequest(asReq, kdcContext);
        kdcRequest.setHttps(true);
        List<EncryptionType> requestedTypes = getEncryptionTypes();
        EncryptionType bestType = EncryptionUtil.getBestEncryptionType(requestedTypes,
                kdcContext.getConfig().getEncryptionTypes());

        if (bestType == null) {
            LOG.error("Can't get the best encryption type.");
            throw new KrbException(KrbErrorCode.KDC_ERR_ETYPE_NOSUPP);
        }

        PrincipalName clientPrincipal = new PrincipalName(authToken.getSubject());
        String clientRealm = asReq.getReqBody().getRealm();
        if (clientRealm == null || clientRealm.isEmpty()) {
            clientRealm = getKdcContext().getKdcRealm();
        }
        clientPrincipal.setRealm(clientRealm);

        // Set the client key
        EncryptionKey clientKey = HasUtil.getClientKey(clientPrincipal.getName(),
            passPhrase, bestType);
        kdcRequest.setClientKey(clientKey);

        // Set the token issuers
        getKdcServer().getKdcConfig().setString(KdcConfigKey.TOKEN_ISSUERS, "has");

        KrbMessage krbResponse;

        try {
            kdcRequest.process();
            krbResponse = kdcRequest.getReply();
        } catch (KrbException e) {
            LOG.error("Error occurred when request tgt. " + e.getMessage());
            if (e instanceof KdcRecoverableException) {
                krbResponse = handleRecoverableException(
                        (KdcRecoverableException) e, kdcRequest);
            } else {
                KrbError krbError = new KrbError();
                krbError.setStime(KerberosTime.now());
                krbError.setSusec(100);
                if (e.getKrbErrorCode() != null) {
                    krbError.setErrorCode(e.getKrbErrorCode());
                } else {
                    krbError.setErrorCode(KrbErrorCode.UNKNOWN_ERR);
                }
                krbError.setCrealm(kdcContext.getKdcRealm());
                if (kdcRequest.getClientPrincipal() != null) {
                    krbError.setCname(kdcRequest.getClientPrincipal());
                }
                krbError.setRealm(kdcContext.getKdcRealm());
                if (kdcRequest.getServerPrincipal() != null) {
                    krbError.setSname(kdcRequest.getServerPrincipal());
                } else {
                    PrincipalName serverPrincipal = kdcRequest.getKdcReq().getReqBody().getSname();
                    serverPrincipal.setRealm(kdcRequest.getKdcReq().getReqBody().getRealm());
                    krbError.setSname(serverPrincipal);
                }
                if (KrbErrorCode.KRB_AP_ERR_BAD_INTEGRITY.equals(e.getKrbErrorCode())) {
                    krbError.setEtext("PREAUTH_FAILED");
                } else {
                    krbError.setEtext(e.getMessage());
                }
                krbResponse = krbError;
            }
        }
        return krbResponse;
    }

    /**
     * Process the recoverable exception.
     *
     * @param e The exception return by kdc
     * @param kdcRequest kdc request
     * @return The KrbError
     */
    private KrbMessage handleRecoverableException(KdcRecoverableException e,
                                                  KdcRequest kdcRequest) {
        LOG.info("KRB error occurred while processing request:"
                + e.getMessage());

        KrbError error = e.getKrbError();
        error.setStime(KerberosTime.now());
        error.setSusec(100);
        error.setErrorCode(e.getKrbError().getErrorCode());
        error.setRealm(kdcContext.getKdcRealm());
        if (kdcRequest != null) {
            error.setSname(kdcRequest.getKdcReq().getReqBody().getCname());
        } else {
            error.setSname(new PrincipalName("NONE"));
        }
        error.setEtext(e.getMessage());
        return error;
    }

    public AsReq createAsReq(AuthToken authToken) throws KrbException {
        AsReq asReq = new AsReq();
        KdcReqBody body = makeReqBody();
        asReq.setReqBody(body);

        PaTokenRequest tokenPa = new PaTokenRequest();
        KrbToken krbToken = new KrbToken(authToken, TokenFormat.JWT);
        tokenPa.setToken(krbToken);
        TokenInfo info = new TokenInfo();
        info.setTokenVendor(authToken.getIssuer());
        tokenPa.setTokenInfo(info);

        PaDataEntry paDataEntry = new PaDataEntry();
        paDataEntry.setPaDataType(PaDataType.TOKEN_REQUEST);
        paDataEntry.setPaDataValue(KrbCodec.encode(tokenPa));

        PaData paData = new PaData();
        paData.addElement(paDataEntry);
        asReq.setPaData(paData);
        return asReq;
    }

    /**
     * Create the KdcReqBody
     *
     * @return KdcReqBody the KDC req body
     */
     protected KdcReqBody makeReqBody() {
        KdcReqBody body = new KdcReqBody();

        long startTime = System.currentTimeMillis();
        body.setFrom(new KerberosTime(startTime));

         // set the client principal as null
        PrincipalName cName = null;
        body.setCname(cName);

        body.setRealm(getKrbContext().getKrbSetting().getKdcRealm());

        PrincipalName sName = getServerPrincipal();
        body.setSname(sName);

        body.setTill(new KerberosTime(startTime + krbContext.getTicketValidTime()));

        int nonce = krbContext.generateNonce();
        body.setNonce(nonce);
//        setChosenNonce(nonce);

        body.setKdcOptions(getKdcOptions());

        HostAddresses addresses = getHostAddresses();
        if (addresses != null) {
            body.setAddresses(addresses);
        }

        body.setEtypes(getEncryptionTypes());

        return body;
    }

    private PrincipalName getServerPrincipal() {
        return KrbUtil.makeTgsPrincipal(getKrbContext().getKrbSetting().getKdcRealm());
    }

    private KdcOptions getKdcOptions() {
        KdcOptions kdcOptions = new KdcOptions();
        // By default enforce these flags
        kdcOptions.setFlag(KdcOption.FORWARDABLE);
        kdcOptions.setFlag(KdcOption.PROXIABLE);
        kdcOptions.setFlag(KdcOption.RENEWABLE_OK);
        return kdcOptions;
    }

    public HostAddresses getHostAddresses() {
        List<HostAddress> hostAddresses = new ArrayList<>();
        HostAddresses addresses = null;
        //empty
        if (!hostAddresses.isEmpty()) {
            addresses = new HostAddresses();
            for (HostAddress ha : hostAddresses) {
                addresses.addElement(ha);
            }
        }
        return addresses;
    }

    public List<EncryptionType> getEncryptionTypes() {
        List<EncryptionType> encryptionTypes = krbContext.getConfig().getEncryptionTypes();
        return EncryptionUtil.orderEtypesByStrength(encryptionTypes);
    }
}
