/**
 *  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.kerberos.kerb.client;

import org.apache.kerby.KOptions;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.ccache.Credential;
import org.apache.kerby.kerberos.kerb.ccache.CredentialCache;
import org.apache.kerby.kerberos.kerb.client.impl.DefaultInternalKrbClient;
import org.apache.kerby.kerberos.kerb.client.impl.InternalKrbClient;
import org.apache.kerby.kerberos.kerb.type.kdc.EncAsRepPart;
import org.apache.kerby.kerberos.kerb.type.ticket.SgtTicket;
import org.apache.kerby.kerberos.kerb.type.ticket.TgtTicket;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;

/**
 * A Krb client API for applications to interact with KDC
 */
public class KrbClientBase {
    private final KrbConfig krbConfig;
    private final KOptions commonOptions;
    private final KrbSetting krbSetting;

    private InternalKrbClient innerClient;
    private static final Logger LOG = LoggerFactory.getLogger(KrbClientBase.class);

    /**
     * Default constructor.
     * @throws KrbException e
     */
    public KrbClientBase() throws KrbException {
        this.krbConfig = ClientUtil.getDefaultConfig();
        this.commonOptions = new KOptions();
        this.krbSetting = new KrbSetting(commonOptions, krbConfig);
    }

    /**
     * Construct with prepared KrbConfig.
     * @param krbConfig The krb config
     */
    public KrbClientBase(KrbConfig krbConfig) {
        this.krbConfig = krbConfig;
        this.commonOptions = new KOptions();
        this.krbSetting = new KrbSetting(commonOptions, krbConfig);
    }

    /**
     * Constructor with conf dir
     * @param confDir The conf dir
     * @throws KrbException e
     */
    public KrbClientBase(File confDir) throws KrbException {
        this.commonOptions = new KOptions();
        this.krbConfig = ClientUtil.getConfig(confDir);
        this.krbSetting = new KrbSetting(commonOptions, krbConfig);
    }

    /**
     * Constructor with prepared KrbClientBase.
     * @param krbClient The krb client
     */
    public KrbClientBase(KrbClientBase krbClient) {
        this.commonOptions = krbClient.commonOptions;
        this.krbConfig = krbClient.krbConfig;
        this.krbSetting = krbClient.krbSetting;
        this.innerClient = krbClient.innerClient;
    }


    /**
     * Set KDC realm for ticket request
     * @param realm The realm
     */
    public void setKdcRealm(String realm) {
        commonOptions.add(KrbOption.KDC_REALM, realm);
    }

    /**
     * Set KDC host.
     * @param kdcHost The kdc host
     */
    public void setKdcHost(String kdcHost) {
        commonOptions.add(KrbOption.KDC_HOST, kdcHost);
    }

    /**
     * Set KDC tcp port.
     * @param kdcTcpPort The kdc tcp port
     */
    public void setKdcTcpPort(int kdcTcpPort) {
        if (kdcTcpPort < 1) {
            throw new IllegalArgumentException("Invalid port");
        }
        commonOptions.add(KrbOption.KDC_TCP_PORT, kdcTcpPort);
        setAllowTcp(true);
    }

    /**
     * Set to allow UDP or not.
     * @param allowUdp true if allow udp
     */
    public void setAllowUdp(boolean allowUdp) {
        commonOptions.add(KrbOption.ALLOW_UDP, allowUdp);
    }

    /**
     * Set to allow TCP or not.
     * @param allowTcp true if allow tcp
     */
    public void setAllowTcp(boolean allowTcp) {
        commonOptions.add(KrbOption.ALLOW_TCP, allowTcp);
    }

    /**
     * Set KDC udp port. Only makes sense when allowUdp is set.
     * @param kdcUdpPort The kdc udp port
     */
    public void setKdcUdpPort(int kdcUdpPort) {
        if (kdcUdpPort < 1) {
            throw new IllegalArgumentException("Invalid port");
        }
        commonOptions.add(KrbOption.KDC_UDP_PORT, kdcUdpPort);
        setAllowUdp(true);
    }

    /**
     * Set time out for connection
     * @param timeout in seconds
     */
    public void setTimeout(int timeout) {
        commonOptions.add(KrbOption.CONN_TIMEOUT, timeout);
    }

    /**
     * Init the client.
     * @throws KrbException e
     */
    public void init() throws KrbException {
        innerClient = new DefaultInternalKrbClient(krbSetting);
        innerClient.init();
    }

    /**
     * Get krb client settings from options and configs.
     * @return setting
     */
    public KrbSetting getSetting() {
        return krbSetting;
    }

    public KrbConfig getKrbConfig() {
        return krbConfig;
    }

    /**
     * Request a TGT with using well prepared requestOptions.
     * @param requestOptions The request options
     * @return TGT
     * @throws KrbException e
     */
    public TgtTicket requestTgt(KOptions requestOptions) throws KrbException {
        if (requestOptions == null) {
            throw new IllegalArgumentException("Null requestOptions specified");
        }

        return innerClient.requestTgt(requestOptions);
    }

    /**
     * Request a service ticket with a TGT targeting for a server
     * @param tgt The tgt ticket
     * @param serverPrincipal The server principal
     * @return Service ticket
     * @throws KrbException e
     */
    public SgtTicket requestSgt(TgtTicket tgt,
                                String serverPrincipal) throws KrbException {
        KOptions requestOptions = new KOptions();
        requestOptions.add(KrbOption.USE_TGT, tgt);
        requestOptions.add(KrbOption.SERVER_PRINCIPAL, serverPrincipal);
        return innerClient.requestSgt(requestOptions);
    }

    /**
     * Request a service ticket provided request options
     * @param requestOptions The request options
     * @return service ticket
     * @throws KrbException e
     */
    public SgtTicket requestSgt(KOptions requestOptions) throws KrbException {
        return innerClient.requestSgt(requestOptions);
    }

    /**
     * Request a service ticket
     * @param ccFile The credential cache file
     * @param servicePrincipal The service principal
     * @return service ticket
     * @throws KrbException e
     */
    public SgtTicket requestSgt(File ccFile, String servicePrincipal) throws KrbException {
        Credential credential = getCredentialFromFile(ccFile);
        TgtTicket tgt = getTgtTicketFromCredential(credential);
        KOptions requestOptions = new KOptions();

        // Renew ticket if argument named servicePrincipal is null
        if (servicePrincipal == null) {
            requestOptions.add(KrbKdcOption.RENEW);
            servicePrincipal = credential.getServicePrincipal().getName();
        }

        requestOptions.add(KrbOption.USE_TGT, tgt);
        requestOptions.add(KrbOption.SERVER_PRINCIPAL, servicePrincipal);
        SgtTicket sgtTicket = innerClient.requestSgt(requestOptions);
        sgtTicket.setClientPrincipal(tgt.getClientPrincipal());
        return sgtTicket;
    }


    /**
     * Store tgt into the specified credential cache file.
     * @param tgtTicket The tgt ticket
     * @param ccacheFile The credential cache file
     * @throws KrbException e
     */
    public void storeTicket(TgtTicket tgtTicket,
                            File ccacheFile) throws KrbException {
        LOG.info("Storing the tgt to the credential cache file.");
        if (!ccacheFile.exists()) {
            createCacheFile(ccacheFile);
        }
        if (ccacheFile.exists() && ccacheFile.canWrite()) {
            CredentialCache cCache = new CredentialCache(tgtTicket);
            try {
                cCache.store(ccacheFile);
            } catch (IOException e) {
                throw new KrbException("Failed to store tgt", e);
            }
        } else {
            throw new IllegalArgumentException("Invalid ccache file, "
                    + "not exist or writable: " + ccacheFile.getAbsolutePath());
        }
    }

    /**
     * Store sgt into the specified credential cache file.
     * @param sgtTicket The sgt ticket
     * @param ccacheFile The credential cache file
     * @throws KrbException e
     */
    public void storeTicket(SgtTicket sgtTicket, File ccacheFile) throws KrbException {
        LOG.info("Storing the sgt to the credential cache file.");
        boolean createCache = !ccacheFile.exists() || ccacheFile.length() == 0;

        if (createCache) {
            createCacheFile(ccacheFile);
        }

        if (ccacheFile.exists() && ccacheFile.canWrite()) {
            try {
                CredentialCache cCache;

                if (!createCache) {
                    cCache = new CredentialCache();
                    cCache.load(ccacheFile);
                    cCache.addCredential(new Credential(sgtTicket, sgtTicket.getClientPrincipal()));
                } else {
                    //Remind: contructor sets the cCache client principal from the sgtTicket one
                    cCache = new CredentialCache(sgtTicket);
                }

                cCache.store(ccacheFile);
            } catch (IOException e) {
                throw new KrbException("Failed to store sgt", e);
            }
        } else {
            throw new IllegalArgumentException("Invalid ccache file, "
                    + "not exist or writable: " + ccacheFile.getAbsolutePath());
        }

    }

    /**
     * Store sgt into the specified credential cache file.
     * @param sgtTicket The sgt ticket
     * @param ccacheFile The credential cache file
     * @throws KrbException e
     */
    public void renewTicket(SgtTicket sgtTicket, File ccacheFile) throws KrbException {
        LOG.info("Renewing the ticket to the credential cache file.");
        if (!ccacheFile.exists()) {
            createCacheFile(ccacheFile);
        }
        if (ccacheFile.exists() && ccacheFile.canWrite()) {
            CredentialCache cCache = new CredentialCache(sgtTicket);
            try {
                cCache.store(ccacheFile);
            } catch (IOException e) {
                throw new KrbException("Failed to renew ticket", e);
            }
        } else {
            throw new IllegalArgumentException("Invalid ccache file, "
                + "not exist or writable: " + ccacheFile.getAbsolutePath());
        }
    }

    /**
     * Create the specified credential cache file.
     */
    private void createCacheFile(File ccacheFile) throws KrbException {
        try {
            if (!ccacheFile.createNewFile()) {
                throw new KrbException("Failed to create ccache file "
                    + ccacheFile.getAbsolutePath());
            }
            // sets read-write permissions to owner only
            ccacheFile.setReadable(true, true);
            if (!ccacheFile.setWritable(true, true)) {
                throw new KrbException("Cache file is not readable.");
            }
        } catch (IOException e) {
            throw new KrbException("Failed to create ccache file "
                + ccacheFile.getAbsolutePath(), e);
        }
    }

    public TgtTicket getTgtTicketFromCredential(Credential cc) {
        EncAsRepPart encAsRepPart = new EncAsRepPart();
        encAsRepPart.setAuthTime(cc.getAuthTime());
        encAsRepPart.setCaddr(cc.getClientAddresses());
        encAsRepPart.setEndTime(cc.getEndTime());
        encAsRepPart.setFlags(cc.getTicketFlags());
        encAsRepPart.setKey(cc.getKey());
//        encAsRepPart.setKeyExpiration();
//        encAsRepPart.setLastReq();
//        encAsRepPart.setNonce();
        encAsRepPart.setRenewTill(cc.getRenewTill());
        encAsRepPart.setSname(cc.getServerName());
        encAsRepPart.setSrealm(cc.getServerName().getRealm());
        encAsRepPart.setStartTime(cc.getStartTime());
        TgtTicket tgtTicket = new TgtTicket(cc.getTicket(), encAsRepPart, cc.getClientName());
        return tgtTicket;
    }

    public Credential getCredentialFromFile(File ccFile) throws KrbException {
        CredentialCache cc;
        try {
            cc = resolveCredCache(ccFile);
        } catch (IOException e) {
            throw new KrbException("Failed to load armor cache file");
        }
        return cc.getCredentials().iterator().next();
    }

    public CredentialCache resolveCredCache(File ccacheFile) throws IOException {
        CredentialCache cc = new CredentialCache();
        cc.load(ccacheFile);

        return cc;
    }
}
