/*
 * Copyright 2016 Red Hat, Inc. and/or its affiliates
 * and other contributors as indicated by the @author tags.
 *
 * Licensed 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.keycloak.connections.mongo.lock;

import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.DuplicateKeyException;
import com.mongodb.WriteResult;
import org.jboss.logging.Logger;
import org.keycloak.common.util.HostUtils;
import org.keycloak.common.util.Time;
import org.keycloak.models.dblock.DBLockProvider;

/**
 * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
 */
public class MongoDBLockProvider implements DBLockProvider {

    private static final String DB_LOCK_COLLECTION = "dblock";
    private static final Logger logger = Logger.getLogger(MongoDBLockProvider .class);

    private final MongoDBLockProviderFactory factory;
    private final DB db;

    public MongoDBLockProvider(MongoDBLockProviderFactory factory, DB db) {
        this.factory = factory;
        this.db = db;
    }


    @Override
    public void waitForLock() {
        boolean locked = false;
        long startTime = Time.toMillis(Time.currentTime());
        long timeToGiveUp = startTime + (factory.getLockWaitTimeoutMillis());

        while (!locked && Time.toMillis(Time.currentTime()) < timeToGiveUp) {
            locked = acquireLock();
            if (!locked) {
                int remainingTime = ((int)(timeToGiveUp / 1000)) - Time.currentTime();
                logger.debugf("Waiting for changelog lock... Remaining time: %d seconds", remainingTime);
                try {
                    Thread.sleep(factory.getLockRecheckTimeMillis());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        if (!locked) {
            DBObject query = new BasicDBObject("_id", 1);
            DBCursor cursor = db.getCollection(DB_LOCK_COLLECTION).find(query);
            String lockedBy;
            if (cursor.hasNext()) {
                DBObject dbObj = cursor.next();
                lockedBy = dbObj.get("lockedBy") + " since " + Time.toDate(((int)((long) dbObj.get("lockedSince") / 1000)));
            } else {
                lockedBy = "UNKNOWN";
            }
            throw new IllegalStateException("Could not acquire change log lock.  Currently locked by " + lockedBy);
        }
    }


    private boolean acquireLock() {
        DBObject query = new BasicDBObject("locked", false);

        BasicDBObject update = new BasicDBObject("locked", true);
        update.append("_id", 1);
        update.append("lockedSince", Time.toMillis(Time.currentTime()));
        update.append("lockedBy", HostUtils.getHostName()); // Maybe replace with something better, but doesn't matter for now

        try {
            WriteResult wr = db.getCollection(DB_LOCK_COLLECTION).update(query, update, true, false);
            if (wr.getN() == 1) {
                logger.debugf("Successfully acquired DB lock");
                return true;
            } else {
                return false;
            }
        } catch (DuplicateKeyException dke) {
            logger.debugf("Failed acquire lock. Reason: %s", dke.getMessage());
        }

        return false;
    }


    @Override
    public void releaseLock() {
        DBObject query = new BasicDBObject("locked", true);

        BasicDBObject update = new BasicDBObject("locked", false);
        update.append("_id", 1);
        update.append("lockedBy", null);
        update.append("lockedSince", null);

        try {
            WriteResult wr = db.getCollection(DB_LOCK_COLLECTION).update(query, update, true, false);
            if (wr.getN() > 0) {
                logger.debugf("Successfully released DB lock");
            } else {
                logger.warnf("Attempt to release DB lock, but nothing was released");
            }
        } catch (DuplicateKeyException dke) {
            logger.debugf("Failed release lock. Reason: %s", dke.getMessage());
        }
    }

    @Override
    public void destroyLockInfo() {
        db.getCollection(DB_LOCK_COLLECTION).remove(new BasicDBObject());
        logger.debugf("Destroyed lock collection");
    }

    @Override
    public void close() {

    }
}
