/**
 * 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.zookeeper.book.recovery;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.zookeeper.AsyncCallback.ChildrenCallback;
import org.apache.zookeeper.AsyncCallback.DataCallback;
import org.apache.zookeeper.AsyncCallback.StringCallback;
import org.apache.zookeeper.AsyncCallback.VoidCallback;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.ZooDefs.Ids;
import org.apache.zookeeper.KeeperException.Code;
import org.apache.zookeeper.book.Master;
import org.apache.zookeeper.data.Stat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This class implements a task to recover assignments after 
 * a primary master crash. The main idea is to determine the 
 * tasks that have already been assigned and assign the ones
 * that haven't 
 *
 */
public class RecoveredAssignments {
    private static final Logger LOG = LoggerFactory.getLogger(RecoveredAssignments.class);
    
    /*
     * Various lists wew need to keep track of.
     */
    List<String> tasks;
    List<String> assignments;
    List<String> status;
    List<String> activeWorkers;
    List<String> assignedWorkers;
    
    RecoveryCallback cb;
    
    ZooKeeper zk;
    
    /**
     * Callback interface. Called once 
     * recovery completes or fails.
     *
     */
    public interface RecoveryCallback {
        final static int OK = 0;
        final static int FAILED = -1;
        
        public void recoveryComplete(int rc, List<String> tasks);
    }
    
    /**
     * Recover unassigned tasks.
     * 
     * @param zk
     */
    public RecoveredAssignments(ZooKeeper zk){
        this.zk = zk;
        this.assignments = new ArrayList<String>();
    }
    
    /**
     * Starts recovery.
     * 
     * @param recoveryCallback
     */
    public void recover(RecoveryCallback recoveryCallback){
        // Read task list with getChildren
        cb = recoveryCallback;
        getTasks();
    }
    
    private void getTasks(){
        zk.getChildren("/tasks", false, tasksCallback, null);
    }
    
    ChildrenCallback tasksCallback = new ChildrenCallback(){
        public void processResult(int rc, String path, Object ctx, List<String> children){
            switch (Code.get(rc)) { 
            case CONNECTIONLOSS:
                getTasks();
                
                break;
            case OK:
                LOG.info("Getting tasks for recovery");
                tasks = children;
                getAssignedWorkers();
                
                break;
            default:
                LOG.error("getChildren failed",  KeeperException.create(Code.get(rc), path));
                cb.recoveryComplete(RecoveryCallback.FAILED, null);
            }
        }
    };
    
    private void getAssignedWorkers(){
        zk.getChildren("/assign", false, assignedWorkersCallback, null);
    }
    
    ChildrenCallback assignedWorkersCallback = new ChildrenCallback(){
        public void processResult(int rc, String path, Object ctx, List<String> children){    
            switch (Code.get(rc)) { 
            case CONNECTIONLOSS:
                getAssignedWorkers();
                
                break;
            case OK:  
                assignedWorkers = children;
                getWorkers(children);

                break;
            default:
                LOG.error("getChildren failed",  KeeperException.create(Code.get(rc), path));
                cb.recoveryComplete(RecoveryCallback.FAILED, null);
            }
        }
    };
        
    private void getWorkers(Object ctx){
        zk.getChildren("/workers", false, workersCallback, ctx);
    }
    
    
    ChildrenCallback workersCallback = new ChildrenCallback(){
        public void processResult(int rc, String path, Object ctx, List<String> children){    
            switch (Code.get(rc)) { 
            case CONNECTIONLOSS:
                getWorkers(ctx);
                break;
            case OK:
                LOG.info("Getting worker assignments for recovery: " + children.size());
                
                /*
                 * No worker available yet, so the master is probably let's just return an empty list.
                 */
                if(children.size() == 0) {
                    LOG.warn( "Empty list of workers, possibly just starting" );
                    cb.recoveryComplete(RecoveryCallback.OK, new ArrayList<String>());
                    
                    break;
                }
                
                /*
                 * Need to know which of the assigned workers are active.
                 */
                        
                activeWorkers = children;
                
                for(String s : assignedWorkers){
                    getWorkerAssignments("/assign/" + s);
                }
                
                break;
            default:
                LOG.error("getChildren failed",  KeeperException.create(Code.get(rc), path));
                cb.recoveryComplete(RecoveryCallback.FAILED, null);
            }
        }
    };
    
    private void getWorkerAssignments(String s) {
        zk.getChildren(s, false, workerAssignmentsCallback, null);
    }
    
    ChildrenCallback workerAssignmentsCallback = new ChildrenCallback(){
        public void processResult(int rc, 
                String path, 
                Object ctx, 
                List<String> children) {    
            switch (Code.get(rc)) { 
            case CONNECTIONLOSS:
                getWorkerAssignments(path);
                break;
            case OK:
                String worker = path.replace("/assign/", "");
                
                /*
                 * If the worker is in the list of active
                 * workers, then we add the tasks to the
                 * assignments list. Otherwise, we need to 
                 * re-assign those tasks, so we add them to
                 * the list of tasks.
                 */
                if(activeWorkers.contains(worker)) {
                    assignments.addAll(children);
                } else {
                    for( String task : children ) {
                        if(!tasks.contains( task )) {
                            tasks.add( task );
                            getDataReassign( path, task );
                        } else {
                            /*
                             * If the task is still in the list
                             * we delete the assignment.
                             */
                            deleteAssignment(path + "/" + task);
                        }
                        
                        /*
                         * Delete the assignment parent. 
                         */
                        deleteAssignment(path);
                    }
                    
                }
                   
                assignedWorkers.remove(worker);
                
                /*
                 * Once we have checked all assignments,
                 * it is time to check the status of tasks
                 */
                if(assignedWorkers.size() == 0){
                    LOG.info("Getting statuses for recovery");
                    getStatuses();
                 }
                
                break;
            case NONODE:
                LOG.info( "No such znode exists: " + path );
                
                break;
            default:
                LOG.error("getChildren failed",  KeeperException.create(Code.get(rc), path));
                cb.recoveryComplete(RecoveryCallback.FAILED, null);
            }
        }
    };
    
    /**
     * Get data of task being reassigned.
     * 
     * @param path
     * @param task
     */
    void getDataReassign(String path, String task) {
        zk.getData(path, 
                false, 
                getDataReassignCallback, 
                task);
    }
    
    /**
     * Context for recreate operation.
     *
     */
    class RecreateTaskCtx {
        String path; 
        String task;
        byte[] data;
        
        RecreateTaskCtx(String path, String task, byte[] data) {
            this.path = path;
            this.task = task;
            this.data = data;
        }
    }

    /**
     * Get task data reassign callback.
     */
    DataCallback getDataReassignCallback = new DataCallback() {
        public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat)  {
            switch(Code.get(rc)) {
            case CONNECTIONLOSS:
                getDataReassign(path, (String) ctx); 
                
                break;
            case OK:
                recreateTask(new RecreateTaskCtx(path, (String) ctx, data));
                
                break;
            default:
                LOG.error("Something went wrong when getting data ",
                        KeeperException.create(Code.get(rc)));
            }
        }
    };
    
    /**
     * Recreate task znode in /tasks
     * 
     * @param ctx Recreate text context
     */
    void recreateTask(RecreateTaskCtx ctx) {
        zk.create("/tasks/" + ctx.task,
                ctx.data,
                Ids.OPEN_ACL_UNSAFE, 
                CreateMode.PERSISTENT,
                recreateTaskCallback,
                ctx);
    }
    
    /**
     * Recreate znode callback
     */
    StringCallback recreateTaskCallback = new StringCallback() {
        public void processResult(int rc, String path, Object ctx, String name) {
            switch(Code.get(rc)) {
            case CONNECTIONLOSS:
                recreateTask((RecreateTaskCtx) ctx);
       
                break;
            case OK:
                deleteAssignment(((RecreateTaskCtx) ctx).path);
                
                break;
            case NODEEXISTS:
                LOG.warn("Node shouldn't exist: " + path);
                
                break;
            default:
                LOG.error("Something wwnt wrong when recreating task", 
                        KeeperException.create(Code.get(rc)));
            }
        }
    };
    
    /**
     * Delete assignment of absent worker
     * 
     * @param path Path of znode to be deleted
     */
    void deleteAssignment(String path){
        zk.delete(path, -1, taskDeletionCallback, null);
    }
    
    VoidCallback taskDeletionCallback = new VoidCallback(){
        public void processResult(int rc, String path, Object rtx){
            switch(Code.get(rc)) {
            case CONNECTIONLOSS:
                deleteAssignment(path);
                break;
            case OK:
                LOG.info("Task correctly deleted: " + path);
                break;
            default:
                LOG.error("Failed to delete task data" + 
                        KeeperException.create(Code.get(rc), path));
            } 
        }
    };
    
    
    void getStatuses(){
        zk.getChildren("/status", false, statusCallback, null); 
    }
    
    ChildrenCallback statusCallback = new ChildrenCallback(){
        public void processResult(int rc, 
                String path, 
                Object ctx, 
                List<String> children){    
            switch (Code.get(rc)) { 
            case CONNECTIONLOSS:
                getStatuses();
                
                break;
            case OK:
                LOG.info("Processing assignments for recovery");
                status = children;
                processAssignments();
                
                break;
            default:
                LOG.error("getChildren failed",  KeeperException.create(Code.get(rc), path));
                cb.recoveryComplete(RecoveryCallback.FAILED, null);
            }
        }
    };
    
    private void processAssignments(){
        LOG.info("Size of tasks: " + tasks.size());
        // Process list of pending assignments
        for(String s: assignments){
            LOG.info("Assignment: " + s);
            deleteAssignment("/tasks/" + s);
            tasks.remove(s);
        }
        
        LOG.info("Size of tasks after assignment filtering: " + tasks.size());
        
        for(String s: status){
            LOG.info( "Checking task: {} ", s );
            deleteAssignment("/tasks/" + s);
            tasks.remove(s);
        }
        LOG.info("Size of tasks after status filtering: " + tasks.size());
        
        // Invoke callback
        cb.recoveryComplete(RecoveryCallback.OK, tasks);     
    }
}