/**
 *  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.identity.backend;

import java.io.IOException;
import java.util.Collections;

import org.apache.kerby.config.Configured;
import org.apache.kerby.kerberos.kerb.KrbException;
import org.apache.kerby.kerberos.kerb.identity.BatchTrans;
import org.apache.kerby.kerberos.kerb.request.KdcClientRequest;
import org.apache.kerby.kerberos.kerb.request.KrbIdentity;
import org.apache.kerby.kerberos.kerb.type.ad.AdToken;
import org.apache.kerby.kerberos.kerb.type.ad.AuthorizationData;
import org.apache.kerby.kerberos.kerb.type.ad.AuthorizationDataEntry;
import org.apache.kerby.kerberos.kerb.type.ad.AuthorizationType;
import org.apache.kerby.kerberos.kerb.type.base.KrbToken;
import org.apache.kerby.kerberos.kerb.type.base.TokenFormat;
import org.apache.kerby.kerberos.kerb.type.ticket.EncTicketPart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * An abstract identity backend that provides default behaviors and a cache
 * with FIFO and size limit. Note only limited recently active identities are
 * kept in the cache, and other identities are meant to be loaded from
 * persistent storage by specific backend, like memory, file, SQL DB, LDAP,
 * and etc.
 */
public abstract class AbstractIdentityBackend
        extends Configured implements IdentityBackend {

    private static Logger logger =
            LoggerFactory.getLogger(AbstractIdentityBackend.class);

    /**
     * Get the Backend Config.
     * @return The backend config
     */
    protected BackendConfig getBackendConfig() {
        return (BackendConfig) getConfig();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void initialize() throws KrbException {
        logger.debug("initialize called");
        doInitialize();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean supportBatchTrans() {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public BatchTrans startBatchTrans() throws KrbException {
        throw new KrbException("Transaction isn't supported");
    }

    /**
     * Perform the real initialization work for the backend.
     * @throws KrbException e
     */
    protected void doInitialize() throws KrbException { }

    /**
     * {@inheritDoc}
     */
    @Override
    public void start() {
        doStart();
        logger.debug("start called");
    }

    /**
     * Perform the real start work for the backend.
     */
    protected void doStart() { }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stop() throws KrbException {
        doStop();
        logger.debug("stop called");
    }

    /**
     * Perform the real stop work for the backend.
     * @throws KrbException e
     */
    protected void doStop() throws KrbException { }

    /**
     * {@inheritDoc}
     */
    @Override
    public void release() {
        doRelease();
        logger.debug("release called");
    }

    /**
     * Perform the real release work for the backend.
     */
    protected void doRelease() { }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterable<String> getIdentities() throws KrbException {
        logger.debug("getIdentities called");
        return doGetIdentities();
    }

    /**
     * Perform the real work to get identities.
     * @return The identities
     * @throws KrbException e
     */
    protected abstract Iterable<String> doGetIdentities() throws KrbException;

    /**
     * {@inheritDoc}
     */
    @Override
    public KrbIdentity getIdentity(String principalName) throws KrbException {
        if (principalName == null || principalName.isEmpty()) {
            throw new IllegalArgumentException("Invalid principal name");
        }

        logger.debug("getIdentity called, principalName = {}", principalName);

        KrbIdentity identity = doGetIdentity(principalName);
        logger.debug("getIdentity {}, principalName = {}",
                identity != null ? "successful" : "failed", principalName);

        return identity;
    }

    /**
     * Add an identity, invoked by addIdentity.
     * @param principalName The principal name
     * @return The added identity
     * @throws  KrbException e
     */
    protected abstract KrbIdentity doGetIdentity(String principalName) throws KrbException;

    /**
     * {@inheritDoc}
     */
    @Override
    public AuthorizationData getIdentityAuthorizationData(KdcClientRequest kdcClientRequest,
            EncTicketPart encTicketPart) throws KrbException {
        if (kdcClientRequest == null) {
            throw new IllegalArgumentException("Invalid identity");
        }

        logger.debug("getIdentityAuthorizationData called, krbIdentity = {}",
                     kdcClientRequest.getClientPrincipal());

        AuthorizationData authData = doGetIdentityAuthorizationData(kdcClientRequest,
                encTicketPart);
        logger.debug("getIdentityAuthorizationData {}, authData = {}",
                authData != null ? "successful" : "failed", authData);

        return authData;
    }

    /**
     * Get an identity's Authorization Data, invoked by getIdentityAuthorizationData.
     * @param kdcClientRequest The KdcClientRequest
     * @param encTicketPart The EncTicketPart being built for the KrbIdentity
     * @return The Authorization Data
     * @throws KrbException e
     */
    protected AuthorizationData doGetIdentityAuthorizationData(
            KdcClientRequest kdcClientRequest, EncTicketPart encTicketPart)
                throws KrbException {
        if (kdcClientRequest.isToken()) {
            KrbToken krbToken = new KrbToken(kdcClientRequest.getToken(), TokenFormat.JWT);
            AdToken adToken = new AdToken();
            adToken.setToken(krbToken);

            AuthorizationData authzData = new AuthorizationData();
            AuthorizationDataEntry authzDataEntry = new AuthorizationDataEntry();
            try {
                authzDataEntry.setAuthzData(adToken.encode());
            } catch (IOException e) {
                throw new KrbException("Error encoding AdToken", e);
            }

            authzDataEntry.setAuthzType(AuthorizationType.AD_TOKEN);
            authzData.setElements(Collections.singletonList(authzDataEntry));
            return authzData;
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public KrbIdentity addIdentity(KrbIdentity identity) throws KrbException {
        if (identity == null) {
            throw new IllegalArgumentException("null identity to add");
        }
        if (doGetIdentity(identity.getPrincipalName()) != null) {
            throw new KrbException("Principal already exists: "
                    + identity.getPrincipalName());
        }

        KrbIdentity added = doAddIdentity(identity);
        logger.debug("addIdentity {}, principalName = {}",
                added != null ? "successful" : "failed", identity.getPrincipalName());

        return added;
    }

    /**
     * Add an identity, invoked by addIdentity, and return added identity.
     * @param identity The identity to be added
     * @return The added identity
     * @throws KrbException e
     */
    protected abstract KrbIdentity doAddIdentity(KrbIdentity identity) throws KrbException;

    /**
     * {@inheritDoc}
     */
    @Override
    public KrbIdentity updateIdentity(KrbIdentity identity) throws KrbException {
        if (identity == null) {
            throw new IllegalArgumentException("null identity to update");
        }

        if (doGetIdentity(identity.getPrincipalName()) == null) {
            logger.error("Error occurred while updating identity, principal "
                + identity.getPrincipalName() + " does not exists.");
            throw new KrbException("Principal does not exist.");
        }

        KrbIdentity updated = doUpdateIdentity(identity);
        logger.debug("updateIdentity {}, principalName = {}",
                updated != null ? "successful" : "failed", identity.getPrincipalName());

        return updated;
    }

    /**
     * Update an identity, invoked by updateIdentity, and return updated identity.
     * @param identity The origin identity
     * @return The updated identity
     * @throws KrbException e
     */
    protected abstract KrbIdentity doUpdateIdentity(KrbIdentity identity) throws KrbException;

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteIdentity(String principalName) throws KrbException {
        logger.debug("deleteIdentity called, principalName = {}", principalName);

        if (principalName == null) {
            throw new IllegalArgumentException("null identity to remove");
        }

        if (doGetIdentity(principalName) == null) {
            logger.error("Error occurred while deleting identity, principal "
                    + principalName + " does not exist.");
            throw new KrbException("Principal does not exist.");
        }

        doDeleteIdentity(principalName);
    }

    /**
     * Delete an identity, invoked by deleteIndentity.
     * @param principalName The principal name
     * @throws KrbException e
     */
    protected abstract void doDeleteIdentity(String principalName) throws KrbException;
}
