/*
 * Copyright (C) 2017 jcgarner
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.pikatimer.util.fileTransports;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpProgressMonitor;
import com.pikatimer.results.ReportDestination;
import com.pikatimer.util.DurationFormatter;
import com.pikatimer.util.FileTransport;
import java.io.IOException;
import java.io.InputStream;
import java.math.RoundingMode;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.concurrent.Task;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;


/**
 *
 * @author jcgarner
 */
public class SFTPTransport implements FileTransport{
    
    String basePath;
    ReportDestination parent;
    Boolean stripAccents = false;

    Thread transferThread;
    JSch sshClient;
    Session sshSession;
    ChannelSftp sftpChannel;
    
    private static final BlockingQueue<String> transferQueue = new ArrayBlockingQueue(100000);

    private static final Map<String,String> transferMap = new ConcurrentHashMap();
    
    StringProperty transferStatus = new SimpleStringProperty("Idle");
    
    String hostname;
    String username;
    String password;

    Boolean fatalError = false;
    Boolean needConfigRefresh = true;
    Long lastTransferTimestamp = 0L;

    public SFTPTransport() {
        
        Task transferTask = new Task<Void>() {

                @Override 
                public Void call() {
                   

                    System.out.println("SFTPTransport: new result processing thread started");
                    String filename = null;
                    while(true) {
                        try {
                            System.out.println("SFTPTransport Thread: Waiting for the first file...");
                            if (filename == null) filename = transferQueue.take();
                            
                            while(true) {
                                System.out.println("SFTPTransport Thread: Waiting for a file...");
                                //filename = transferQueue.poll(60, TimeUnit.SECONDS);
                                if (sshClient == null || sshSession == null || !sshSession.isConnected()) Platform.runLater(() -> {transferStatus.set("Idle");});
                                else {
                                    sshSession.sendKeepAliveMsg();
                                    
                                    Platform.runLater(() -> {
                                        transferStatus.set("Connected");
                                    });
                                }
                                
                                //filename = transferQueue.take(); // blocks until
                                if (filename == null) filename = transferQueue.poll(15, TimeUnit.SECONDS);
                                if (filename == null) {
                                    // If we have been idle for more than 2 minutes, be nice and drop the connection
                                    if (TimeUnit.NANOSECONDS.toSeconds((System.nanoTime()-lastTransferTimestamp))> 120 ) break;
                                    else continue;
                                }

                                System.out.println("SFTPTransport Thread: Preping for transfer of  " + filename);
                                String contents = transferMap.get(filename);
                                
                                while (fatalError || sshSession == null || !sshSession.isConnected() || sftpChannel == null || !sftpChannel.isConnected()) {
                                    if (!fatalError ) openConnection();
                                    if (fatalError || !sftpChannel.isConnected()) {
                                        System.out.println("SFTPTransport Thread: Still not connected, sleeping for 10 seconds...");
                                        Thread.sleep(10000);
                                    }
                                    
                                }
                                System.out.println("SFTPTransport Thread: Transfering " + filename);




                                InputStream data = IOUtils.toInputStream(contents, "UTF-8");
                                //InputStream data = IOUtils.toInputStream(contents);
                                String fn = filename;
                                Platform.runLater(() -> { 
                                    transferStatus.set("Transfering " + fn);
                                });
                                long startTime = System.nanoTime();
                                
                                
                                try {
                                    SFTPTransferMonitor monitor = new SFTPTransferMonitor();
                                    sftpChannel.put(data, filename, monitor);
                                    monitor.await();

                                    long endTime = System.nanoTime();
                                    lastTransferTimestamp = endTime;

                                    data.close();
                                    transferMap.remove(filename, contents); 
                                    System.out.println("SFTPTransport Thread: transfer of " + filename + " done in " + DurationFormatter.durationToString(Duration.ofNanos(endTime-startTime), 3, false, RoundingMode.HALF_EVEN));
                                    filename = null;
                                } catch (SftpException ex) {
                                    System.out.println("SftpException: " + ex.getLocalizedMessage());
                                    throw new IOException(ex.getLocalizedMessage());
                                } 
                                
                                
                            }

                        } catch (InterruptedException ex) {
                            System.out.println("SFTPTransport Thread: InterruptedException thrown");
                            //if (filename!= null) transferQueue.put(filename);

                            //Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
                        } catch (IOException ex) {
                            System.out.println("SFTPTransport Thread: IOException thrown");
                            //if (filename!= null) transferQueue.put(filename);
                            //Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
                        } catch (Exception ex) {
                            System.out.println("SFTPTransport Thread: Generic Exception tossed: " );
                            ex.printStackTrace();
                            
                        } finally {
                            if (sftpChannel != null && sftpChannel.isConnected()) sftpChannel.disconnect();
                            if (sshSession != null && sshSession.isConnected()) {
                                System.out.println("SFTPTransport Thread: calling sshSession.disconnect()"); // do nothing
                                sshSession.disconnect();
                                Platform.runLater(() -> {transferStatus.set("Disconnected");});
                            }
                        }
                    }
                    
                }
            };
            transferThread = new Thread(transferTask);
            transferThread.setName("Thread-SFTP-Transfer");
            transferThread.setDaemon(true);
            transferThread.start();
        
    }
    
    private void openConnection(){
            
        if (needConfigRefresh) refreshConfig();
        System.out.println("SFTP Not connected, connecting...");
        Platform.runLater(() -> {
            transferStatus.set("Connecting SFTP...");
        });
        // connect to the remote server

        sshClient = new JSch();

        // only for public key authentication
        try {
            if(hostname.contains(":")){
                System.out.println("Explicit Port Specified...");
                String[] h = hostname.split(":");
                int port = Integer.parseInt(h[1]);
                sshSession = sshClient.getSession(username, h[0],port);
            } else {
                sshSession = sshClient.getSession(username, hostname);
            }
            
            sshSession.setTimeout(10000); // 10 seconds

            // we only support password authentication for now
            sshSession.setPassword(password);

            Platform.runLater(() -> {transferStatus.set("Loging in...");});
            sshSession.setConfig("StrictHostKeyChecking", "no");
            sshSession.connect();

            Platform.runLater(() -> {transferStatus.set("Connected");});

            sftpChannel = (ChannelSftp) sshSession.openChannel("sftp");
            sftpChannel.connect();
            try {
                sftpChannel.cd(basePath);
                fatalError=false;
            } catch (SftpException ex) {
                try {
                    sftpChannel.mkdir(basePath);
                    sftpChannel.cd(basePath);
                    fatalError=false;
                } catch (SftpException ex1) {
                    Platform.runLater(() -> {transferStatus.set("Error: Unabe to make target directory");});
                    fatalError=true;
                    Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex1);
                }
                Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
            }

        } catch (Exception ex) {
            Platform.runLater(() -> {transferStatus.set("Error: " + ex.getLocalizedMessage());});
            System.out.println(ex.getLocalizedMessage());
            fatalError=true;
            Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    @Override
    public StringProperty statusProperty() {
        return transferStatus;
    }
     @Override
    public boolean isOK() {
        if (password.isEmpty() || username.isEmpty() || hostname.isEmpty() || basePath.isEmpty()) return false;
        return true;
    }

    @Override
    public void save(String filename, String contents) {
        System.out.println("SFTPTransport.save() called for " + filename);
        if (stripAccents) contents = StringUtils.stripAccents(contents);
        transferMap.put(filename,contents);
        if (! transferQueue.contains(filename)) transferQueue.add(filename);
    }

    @Override
    public void setOutputPortal(ReportDestination op) {
        parent=op;
    }

    @Override
    public void refreshConfig() {
        
        // Get the hostname, username, password, basePath
        password=parent.getPassword();
        username=parent.getUsername();
        hostname=parent.getServer();
        basePath=parent.getBasePath();
        if (sshSession != null && sshSession.isConnected()) {
            System.out.println("SFTPTransport::refreshConfig: calling ftpClient.disconnect()"); // do nothing
            sshSession.disconnect();
        }
        
        stripAccents = parent.getStripAccents();
        

                    
        fatalError=false;
        needConfigRefresh = false;
    
    }    

    @Override
    public void test(ReportDestination parent, StringProperty output) {
        Task transferTask = new Task<Void>() {

                @Override 
                public Void call() {
                    
                    password=parent.getPassword();
                    username=parent.getUsername();
                    hostname=parent.getServer();
                    basePath=parent.getBasePath();

                    Platform.runLater(() -> output.set(output.getValueSafe() + "Connecting to " + hostname +"..." ));
                    sshClient = new JSch();

                    // only for public key authentication
                    try {
                        if(hostname.contains(":")){
                            System.out.println("Explicit Port Specified...");
                            String[] h = hostname.split(":");
                            int port = Integer.parseInt(h[1]);
                            sshSession = sshClient.getSession(username, h[0],port);
                        } else {
                            sshSession = sshClient.getSession(username, hostname);
                        }
                        sshSession.setTimeout(10000); // 10 seconds

                        // we only support password authentication for now
                        sshSession.setPassword(password);

                        Platform.runLater(() -> {transferStatus.set("Loging in...");});
                        sshSession.setConfig("StrictHostKeyChecking", "no");
                        sshSession.connect();

                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nConnected" ));
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nOpening SFTP Channel..." ));

                        sftpChannel = (ChannelSftp) sshSession.openChannel("sftp");
                        sftpChannel.connect();
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nOpened" ));
                        try {
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nChanging Directories..." ));
                            sftpChannel.cd(basePath);
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nSuccess!" ));
                            sftpChannel.disconnect();

                        } catch (SftpException ex) {
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nDirectory does not exist!" ));
                            try {
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nAttempting to make the target directory..." ));

                                sftpChannel.mkdir(basePath);
                                
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nCreated target directory" ));
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nChanging Directories..." ));

                                sftpChannel.cd(basePath);
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nSuccess!" ));
                            } catch (SftpException ex1) {
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: " + ex.getLocalizedMessage()+"\n\nTest Failed!"));
                            }
                            sftpChannel.disconnect();
                        }

                    } catch (Exception ex) {
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: " + ex.getLocalizedMessage()+"\n\nTest Failed!"));
                        //System.out.println(ex.getLocalizedMessage());
                        //Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    sshSession.disconnect();
                    return null;
                }
        
        };
        transferThread = new Thread(transferTask);
        transferThread.setName("Thread-SFTP-Transfer-Test");
        transferThread.setDaemon(true);
        transferThread.start();
    
    }
    
    class SFTPTransferMonitor implements SftpProgressMonitor {
        CountDownLatch latch = new CountDownLatch(1);
        long transferedBytes = 0L;

        public SFTPTransferMonitor() {;}

        public void init(int op, String src, String dest, long max) 
        {
            System.out.println("SFTP Transfer Starting: "+op+" "+src+" -> "+dest+" total: "+max);
        }

        public boolean count(long bytes){
            transferedBytes = bytes;
            return(true);
        }

        public void end()
        {
            latch.countDown();
            System.out.println("\nSFTP Transfer: DONE!");
        }
        
        public void await() throws IOException{
            long counter = 0L;
            try {
                while (latch.getCount() > 0) {
                    latch.await(30, TimeUnit.SECONDS);
                    if (counter == transferedBytes) // timeout 
                        throw new IOException("SFTP Transfer Timeout");
                    else counter = transferedBytes;
                }
            } catch (InterruptedException ex) {
                Logger.getLogger(SFTPTransport.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}