/*
 * Copyright (C) 2016 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.pikatimer.results.ReportDestination;
import com.pikatimer.util.DurationFormatter;
import com.pikatimer.util.FileTransport;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.math.RoundingMode;
import java.time.Duration;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
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;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;

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

    Thread transferThread;
    FTPSClient ftpsClient;
    FTPClient ftpClient;
    
    String protMode="P";
    
    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;
    Boolean encrypted = true;
    Long lastTransferTimestamp = 0L;

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

                @Override 
                public Void call() {
                   

                    System.out.println("FTPSTransport: new result processing thread started");
                    String filename = null;
                    while(true) {
                        try {
                            System.out.println("FTPSTransport Thread: Waiting for the first file...");
                            if (filename == null) filename = transferQueue.take();
                            
                            while(true) {
                                System.out.println("FTPSTransport Thread: Waiting for a file...");
                                //filename = transferQueue.poll(60, TimeUnit.SECONDS);
                                if (ftpClient == null || !ftpClient.isConnected()) Platform.runLater(() -> {transferStatus.set("Idle");});
                                else {
                                    ftpClient.sendNoOp();
                                    Platform.runLater(() -> {
                                        if (encrypted) transferStatus.set("Connected FTPS");
                                        else 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("FTPSTransport Thread: Transfering " + filename);
                                String contents = transferMap.get(filename);
                                
                                while (fatalError || ftpClient == null || !ftpClient.isConnected()) {
                                    if (!fatalError) openConnection();
                                    if (!ftpClient.isConnected()) {
                                        System.out.println("FTPSTransport Thread: Still not connected, sleeping for 10 seconds...");
                                        Thread.sleep(10000);
                                    }
                                    
                                }

                                InputStream data = IOUtils.toInputStream(contents, "UTF-8");
                                //InputStream data = IOUtils.toInputStream(contents);
                                String fn = filename;
                                String tmpFn = fn + ".PikaTmp";
                                Platform.runLater(() -> { 
                                    if (encrypted && protMode.equalsIgnoreCase("P")) transferStatus.set("Transfering (Secure) " + fn);
                                    else if (encrypted ) transferStatus.set("Transfering (Clear) " + fn);
                                    else transferStatus.set("Transfering " + fn);
                                });
                                long startTime = System.nanoTime();
                                
                                // To get around file locking issues, 
                                // we upload to a temp fie and then do a rename
                                
                                ftpClient.storeFile(tmpFn, data);
                                
                                // Try a rename. If it fails, try a delete and then a rename
                                try {  
                                    ftpClient.rename(tmpFn, fn);
                                } catch (Exception ex){
                                    System.out.println("ftpClient.rename exception thrown");
                                    
                                    try { 
                                        ftpClient.dele(fn);// This may fail if the file does not exist
                                    } catch (Exception ex2){
                                        // noop
                                        System.out.println("ftpClient.dele exception thrown");
                                    }
                                    ftpClient.rename(tmpFn, fn);
                                }
                                
                                long endTime = System.nanoTime();
                                lastTransferTimestamp = endTime;

                                data.close();
                                transferMap.remove(filename, contents); 
                                
                                System.out.println("FTPSTransport Thread: transfer of " + filename + " done in " + DurationFormatter.durationToString(Duration.ofNanos(endTime-startTime), 3, false, RoundingMode.HALF_EVEN));
                                filename = null;
                            }

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

                            //Logger.getLogger(FTPSTransport.class.getName()).log(Level.SEVERE, null, ex);
                        } catch (IOException ex) {
                            System.out.println("FTPSTransport Thread: IOException thrown: "  + ex.getMessage() );
                            
                            if (encrypted) {
                                System.out.println("Setting protection mode to \"C\" to see if that fixes the problem");
                                protMode="C";
                            }
                            
                            ex.printStackTrace();
                            //if (filename!= null) transferQueue.put(filename);
                            //Logger.getLogger(FTPSTransport.class.getName()).log(Level.SEVERE, null, ex);
                        } catch (Exception ex) {
                            System.out.println("FTPSTransport Thread: Exception tossed: " + ex.getMessage() + ex.toString());
                            System.out.println(Arrays.toString(ex.getStackTrace()));
                        } finally {
                            if (ftpClient.isConnected()) {
                                try {
                                    System.out.println("FTPSTransport Thread: calling ftpClient.disconnect()");
                                    ftpClient.disconnect();
                                    Platform.runLater(() -> {transferStatus.set("Disconnected");});
                                } catch (IOException f) {
                                    // do nothing
                                }
                            }
                        }
                    }
                    
                }
            };
            transferThread = new Thread(transferTask);
            transferThread.setName("Thread-FTPS-Transfer");
            transferThread.setDaemon(true);
            transferThread.start();
        
    }
    
    private void openConnection(){
        try {
            
            if (needConfigRefresh) refreshConfig();
            System.out.println("FTPS Not connected, connecting...");
            Platform.runLater(() -> {
                if (encrypted) transferStatus.set("Connecting FTPS...");
                else transferStatus.set("Connecting...");
            });
            // connect to the remote server
            
            if (encrypted) {
                ftpsClient = new FTPSClient(false);
                ftpClient = ftpsClient;
            } else {
                ftpClient = new FTPClient();
                ftpsClient = null;
            }
            
            ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
            // Connect to host
            ftpClient.setConnectTimeout(10000); // 10 seconds
            ftpClient.connect(hostname);
            int reply = ftpClient.getReplyCode();
            if (FTPReply.isPositiveCompletion(reply)) {
                
                
                // Login
                Platform.runLater(() -> {
                    if (encrypted) transferStatus.set("Loging in...");
                    else transferStatus.set("Loging in (insecure)...");
                });
                
                

                if (ftpClient.login(username, password)) {

                    if (encrypted) {
                        // Set protection buffer size
                        ftpsClient.execPBSZ(0);
                        // Set data channel protection mode ("P" or "C")
                      
                        ftpsClient.execPROT(protMode);
                    }
                    // Enter local passive mode
                    ftpClient.enterLocalPassiveMode();
                    //ftpClient.setFileType(FTP.ASCII_FILE_TYPE);
                    ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                    //ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                    
                    Platform.runLater(() -> {transferStatus.set("Changing Directories...");});
                    if(!ftpClient.changeWorkingDirectory(basePath)) {
                        reply = ftpClient.mkd(basePath);
                        
                        if (!FTPReply.isPositiveCompletion(reply)) {
                            System.out.println("Unable to make remote dir " + basePath);
                            Platform.runLater(() -> {transferStatus.set("Error: Unabe to make target directory");});
                            fatalError=true;
                            ftpClient.disconnect();
                        } else {
                            ftpClient.changeWorkingDirectory(basePath);
                            fatalError=false;
                        }
                    }
                    Platform.runLater(() -> {transferStatus.set("Connected");});
                    fatalError=false;
                } else {
                  System.out.println("FTP login failed");
                  Platform.runLater(() -> {transferStatus.set("Error: Login Failed");});
                  fatalError=true;
                  try {ftpClient.disconnect();} catch (Exception e) {};
                }
            } else {
              System.out.println("FTP connect to host failed");
              Platform.runLater(() -> {transferStatus.set("Error: Unable to connect to host");});

              try {ftpClient.disconnect();} catch (Exception e) {};
            }
        } catch (IOException ioe) {
            if (encrypted) {
                try {ftpClient.disconnect();} catch (Exception e) {};
                
                // odds are we don't support encryption, 
                // let's disable the encryption and try again
                encrypted = false;
                openConnection();
            } else {
                System.out.println("FTP client received network error");
                Platform.runLater(() -> {transferStatus.set("Error: Network Error");});
            }
            

        }
    }

    @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("FTPSTransport.save() called for " + filename);
        if (stripAccents) contents = StringUtils.stripAccents(contents);
        transferMap.put(filename,contents);
        if (! transferQueue.contains(filename)) transferQueue.add(filename);
        
        //if (transferThread == null || ftpClient == null || !ftpClient.isConnected() ) transferFile(); // kicks off the thread
    }

    @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 (ftpClient != null && ftpClient.isConnected()) {
            try {
                System.out.println("FTPSTransport::refreshConfig: calling ftpClient.disconnect()");
                ftpClient.disconnect();
            } catch (IOException f) {
                // do nothing
            }
        }
        
        stripAccents = parent.getStripAccents();
        
        encrypted = true;
                    
        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 +" via FTPS..." ));
                    
                    
                    ftpsClient = new FTPSClient(false);
                    ftpClient = ftpsClient;
                    ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
                    

                    // Connect to host
                    
                    try {
                        ftpClient.connect(hostname);
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nConnected via via FTPS..." ));
                    } catch (IOException ex) {
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nConnection Failed, attempting to use FTP..." ));
                        ftpClient = new FTPClient();
                        ftpClient.setConnectTimeout(10000); // 10 seconds
                        ftpClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out), true));
                        try {
                            ftpClient.connect(hostname);
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nConnected via via FTP..." ));

                        } catch (IOException ex1) {
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nError: " + ex1.getLocalizedMessage() ));
                            try {
                                ftpClient.disconnect();
                            } catch (IOException ex2) {
                            }
                            return null;
                        }
                    }
                    
                    int reply = ftpClient.getReplyCode();
                    
                    if (FTPReply.isPositiveCompletion(reply)) {
                        try {
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nLogging in..." ));
                            
                            ftpClient.enterLocalPassiveMode(); 

                            if (ftpClient.login(username, password)) {
                                // try and CD to the target directory
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nLogin successful." ));

                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nChanging directories..." ));

                                if(!ftpClient.changeWorkingDirectory(basePath)) {
                                    Platform.runLater(() -> output.set(output.getValueSafe() + "\nTarget directory does not exist\n Attempting to create it..." ));

                                    int reply2 = ftpClient.mkd(basePath);
                                    

                                    if (!FTPReply.isPositiveCompletion(reply2)) {
                                        Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: Unable to make the target directory.\n\nTest Failed!" ));
                                        ftpClient.disconnect();
                                        return null;
                                    } else {
                                        
                                        if (!ftpClient.changeWorkingDirectory(basePath)) {
                                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: Unabe to change to target directory\n\nTest Failed!" ));
                                            ftpClient.disconnect();
                                            return null;
                                        } else {
                                            Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nSuccess!"));
                                            ftpClient.disconnect();
                                            return null;
                                        }
                                        
                                    }
                                } else {
                                    Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nSuccess!"));
                                    ftpClient.disconnect();
                                    return null;
                                }
                            } else {
                                Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: Invalid Username or Password\n\nTest Failed!" ));
                                ftpClient.disconnect();
                                return null;
                            }
                        } catch (IOException ex) {
                            Platform.runLater(() -> output.set(output.getValueSafe() + "\nError: IO Error " + ex.getLocalizedMessage()+ "\n\nTest Failed!"  ));
                            try {
                                ftpClient.disconnect();
                            } catch (IOException ex1) {
                                Logger.getLogger(FTPSTransport.class.getName()).log(Level.SEVERE, null, ex1);
                            }
                        }
                    } else {
                        Platform.runLater(() -> output.set(output.getValueSafe() + "\n\nError: Error code response from server: " + reply ));
                        try {
                            ftpClient.disconnect();
                        } catch (IOException ex) {
                            Logger.getLogger(FTPSTransport.class.getName()).log(Level.SEVERE, null, ex);
                        }
                        return null;
                    }
                    try {
                        ftpClient.disconnect();
                    } catch (IOException ex) {
                        Logger.getLogger(FTPSTransport.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    return null;
                }
        
        };
        transferThread = new Thread(transferTask);
        transferThread.setName("Thread-SFTP-Transfer-Test");
        transferThread.setDaemon(true);
        transferThread.start();
    }
}