/**********************************************************************
 *
 * This file is part of HBCI4Java.
 * Copyright (c) 2001-2008 Stefan Palme
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **********************************************************************/

package org.kapott.hbci.passport;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

import org.kapott.hbci.callback.HBCICallback;
import org.kapott.hbci.exceptions.HBCI_Exception;
import org.kapott.hbci.manager.HBCIKey;
import org.kapott.hbci.manager.HBCIUtils;
import org.kapott.hbci.manager.HBCIUtilsInternal;
import org.kapott.hbci.manager.LogFilter;
import org.kapott.hbci.passport.rdhXfile.HBCIAccount;
import org.kapott.hbci.passport.rdhXfile.RDHXFile;
import org.kapott.hbci.passport.rdhXfile.TLV;
import org.kapott.hbci.tools.IOUtils;

/**<p>
 * Passport-Klasse für die Verwendung von RDH-2- und RDH-10-Schlüsseldateien mit
 * <em>HBCI4Java</em>. RDH-2/10-Schlüsseldateien sind Schlüsseldateien für
 * RDH-Zugänge, die von anderer HBCI-Software erzeugt und verwendet werden (z.B.
 * von <em>VR-NetWorld</em>). Soll eine solche Schlüsseldatei sowohl mit der
 * anderen Software als auch mit <em>HBCI4Java</em> verwendet werden, so kann
 * das mit dieser Passport-Variante geschehen.</p> */    
public class HBCIPassportRDHXFile
    extends AbstractRDHSWFileBasedPassport
{
    private byte[]   passphrase;
    private RDHXFile filecontent;
    private int      entryIdx;    
    private String   forcedProfileVersion;
    
    public HBCIPassportRDHXFile(Object init, int dummy)
    {
        super(init);
        this.forcedProfileVersion=null;
    }
    
    protected String getCompatName()
    {
        return "RDHXFile";
    }
    
    public HBCIPassportRDHXFile(Object initObject)
    {
        this(initObject,0);
        setParamHeader("client.passport."+getCompatName());

        String fname=HBCIUtils.getParam(getParamHeader()+".filename");
        if (fname==null) {
            throw new NullPointerException(getParamHeader()+".filename must not be null");
        }
        
        HBCIUtils.log("loading passport data from file "+fname,HBCIUtils.LOG_DEBUG);
        setFilename(fname);

        boolean init=HBCIUtils.getParam(getParamHeader()+".init","1").equals("1");
        if (init) {
            HBCIUtils.log("loading data from file "+getFilename(),HBCIUtils.LOG_DEBUG);

            setFilterType("None");
            setPort(new Integer(3000));

            if (!new File(getFilename()).canRead()) {
                HBCIUtils.log("have to create new passport file",HBCIUtils.LOG_WARN);
                askForMissingData(true,true,true,false,false,true,true);
                saveChanges();
            }
            
            InputStream is = null;
            
            try {
                if (this.passphrase==null) {
                    StringBuffer retData=new StringBuffer();
                    HBCIUtilsInternal.getCallback().callback(this,
                            HBCICallback.NEED_PASSPHRASE_LOAD,
                            HBCIUtilsInternal.getLocMsg("CALLB_NEED_PASS"),
                            HBCICallback.TYPE_SECRET,
                            retData);
                    LogFilter.getInstance().addSecretData(retData.toString(),"X",LogFilter.FILTER_SECRETS);
                    setPassphrase(retData.toString().getBytes());
                }
            	
                is = new BufferedInputStream(new FileInputStream(fname));
                byte[] data = IOUtils.read(is);
                
                // Wichtig. File-Handle schliessen. Sonst schlaegt das Speichern fehl. Siehe https://homebanking-hilfe.de/forum/topic.php?p=151578#real151578 sowie die Mail von Simon vom 28.10.2019
                is.close();
                
                this.filecontent=new RDHXFile(data, passphrase);
                this.entryIdx=0;
                
                TLV[] hbciAccounts=filecontent.getFields(HBCIAccount.class);
                if (hbciAccounts.length>1) {
                    // wenn mehrere bankverbindungen existieren, callback für auswahl der "richtigen"
                    StringBuffer possibilities=new StringBuffer();
                    for (int i=0;i<hbciAccounts.length;i++) {
                        HBCIAccount hbciAccount=(HBCIAccount)hbciAccounts[i];
                        if (i!=0) {
                            possibilities.append("|");
                        }
                        possibilities.append(i);
                        possibilities.append(";"+hbciAccount.getBLZ());
                        possibilities.append(";"+hbciAccount.getUserId());
                    }
                    
                    HBCIUtilsInternal.getCallback().callback(
                        this,
                        HBCICallback.NEED_SIZENTRY_SELECT,
                        "*** select one of the following entries",
                        HBCICallback.TYPE_TEXT,
                        possibilities);
                     
                    this.entryIdx=Integer.parseInt(possibilities.toString());
                }
                
                TLV[] accountFields=filecontent.getFields(HBCIAccount.class);
                if (accountFields.length!=0) {
                    // set all passport values
                    HBCIAccount hbciAccount=(HBCIAccount)(accountFields[entryIdx]);

                    setBLZ(hbciAccount.getBLZ());
                    setCountry(hbciAccount.getCountry());
                    setHost(hbciAccount.getHost());
                    setUserId(hbciAccount.getUserId());
                    setCustomerId(hbciAccount.getCustomerId());
                    setSysId(hbciAccount.getSysId());
                    setSigId(new Long(hbciAccount.getSigId()));

                    // setInstKeys()
                    setInstSigKey(filecontent.getBankSigKey(hbciAccount));
                    setInstEncKey(filecontent.getBankEncKey(hbciAccount));

                    // setUserSigKeys()
                    HBCIKey[] userkeys=hbciAccount.getUserSigKeys();
                    if (userkeys!=null) {
                        setMyPublicSigKey(userkeys[0]);
                        setMyPrivateSigKey(userkeys[1]);
                    }

                    // setUserEncKeys()
                    userkeys=hbciAccount.getUserEncKeys();
                    if (userkeys!=null) {
                        setMyPublicEncKey(userkeys[0]);
                        setMyPrivateEncKey(userkeys[1]);
                    }
                }
                                
                if (askForMissingData(true,true,true,false,false,true,true))
                    saveChanges();
            } catch (Exception e) {
                throw new HBCI_Exception("*** error while reading passport file",e);
            }
            finally
            {
              // Nur zur Sicherheit, falls das Schliessen oben fehlschlug
                IOUtils.close(is);
            }
        }
    }
    
    public void saveChanges()
    {
        try {
            if (this.passphrase == null) {
                StringBuffer retData = new StringBuffer();
                HBCIUtilsInternal.getCallback().callback(this,
                        HBCICallback.NEED_PASSPHRASE_SAVE,
                        HBCIUtilsInternal.getLocMsg("CALLB_NEED_PASS"),
                        HBCICallback.TYPE_SECRET, retData);
                // TODO: passwort-bedingungen nach spez. prüfen
                LogFilter.getInstance().addSecretData(retData.toString(),"X",LogFilter.FILTER_SECRETS);
                setPassphrase(retData.toString().getBytes());
            }

            // create temp file
            File passportfile = new File(getFilename());
            File directory = passportfile.getAbsoluteFile().getParentFile();
            String prefix = passportfile.getName() + "_";
            File tempfile = File.createTempFile(prefix, "", directory);
            
            if (filecontent==null) {
                filecontent=new RDHXFile(passphrase);
            }

            TLV[]       accountFields=filecontent.getFields(HBCIAccount.class);
            HBCIAccount hbciAccount;
            if (accountFields.length==0) {
                hbciAccount = new HBCIAccount();
                filecontent.addField(hbciAccount);
            } else {
                hbciAccount = (HBCIAccount)(accountFields[entryIdx]); 
            }
            
            // save changed values in filecontent object
            hbciAccount.setCountry(getCountry());
            hbciAccount.setBLZ(getBLZ());
            hbciAccount.setHost(getHost());
            hbciAccount.setUserId(getUserId());
            hbciAccount.setCustomerId(getCustomerId());
            hbciAccount.setSysId(getSysId());
            hbciAccount.setSigId(getSigId().longValue());

            filecontent.setBankSigKey(hbciAccount, getInstSigKey());
            filecontent.setBankEncKey(hbciAccount, getInstEncKey());

            hbciAccount.setUserSigKeys(new HBCIKey[] { getMyPublicSigKey(),
                    getMyPrivateSigKey() });
            hbciAccount.setUserEncKeys(new HBCIKey[] { getMyPublicEncKey(),
                    getMyPrivateEncKey() });

            int              pversion = Integer.parseInt(getProfileVersion());
            byte[]           data = filecontent.getFileData(pversion);
            FileOutputStream fo = new FileOutputStream(tempfile);
            fo.write(data);
            fo.close();

            IOUtils.safeReplace(passportfile,tempfile);
            
        } catch (Exception e) {
            throw new HBCI_Exception("*** saving of passport file failed", e);
        }
    }
    
    public String getProfileVersion()
    {
        String result=this.forcedProfileVersion;
        
        if (result==null) {
            HBCIUtils.log("no RDH profile version explicity specified - starting autodetection", HBCIUtils.LOG_DEBUG);
            
            /* TODO: do not use the hbci-version stored in the passport, but the
             * hbci version of the current HBCIHandler associated with this passport.
             * This will be easy in HBCI4Java-3, but in HBCI4Java-2 this is an
             * ugly problem - broken by design */
            if (getHBCIVersion().length()!=0 && !getHBCIVersion().startsWith("3")) { // TODO: support FinTS-4, too
                result="1";
                setProfileVersion(result);
                HBCIUtils.log("this is HBCI version '"+getHBCIVersion()+"', which only supports RDH-1", HBCIUtils.LOG_DEBUG);

            } else {
                HBCIKey key=getMyPublicSigKey();
                if (key!=null) {
                    // profil-erkennung anhand schluesselnummer
                    result=key.num;
		    setProfileVersion(result);
                    HBCIUtils.log("using user sig key num '"+result+"' as profile version" ,HBCIUtils.LOG_DEBUG);
                    
                } else {
                    key=getInstEncKey();
                    if (key!=null && (key.num.equals("1") || key.num.equals("2") || key.num.equals("10"))) {
                        // found a sig key with a valid key num - so we use this as the profile version
                        result=key.num;
                        HBCIUtils.log("using inst enc key num '"+result+"' as RDH profile version", HBCIUtils.LOG_DEBUG);
                        
                    } else {
                        // neither user keys nor inst keys present - using highest available profile
                        HBCIUtils.log(
                            "no keys found in passport - so we use the highest available profile",
                            HBCIUtils.LOG_DEBUG);

                        // es gibt noch gar keine schlüssel - also nehmen wir die
                        // höchste unterstützte profil-nummer

                        String[][] methods=getSuppSecMethods();
                        int        maxVersion=0;
                        for (int i=0;i<methods.length;i++) {
                            String method=methods[i][0];
                            int    version=Integer.parseInt(methods[i][1]);
    
                            if (method.equals("RDH") && 
                                    (version==1 || version==2 || version==10)) 
                            {
                                // es werden nur RDH-1, RDH-2 und RDH-10 betrachtet, weil
                                // alle anderen rdh-profile nicht für software-lösungen
                                // zugelassen sind
                                if (version>maxVersion) {
                                    maxVersion=version;
                                }
                            }
                        }
    
                        if (maxVersion!=0) {
                            result=Integer.toString(maxVersion);
                            setProfileVersion(result);
                        }
                        HBCIUtils.log(
                            "using RDH profile '"+result+"' taken from supported profiles (BPD)",
                            HBCIUtils.LOG_DEBUG);
                    }
                }
            }
            
        } else {
            HBCIUtils.log("using forced RDH profile version '"+result+"'", HBCIUtils.LOG_DEBUG);
        }
        
        return result;
    }
    
    public void setProfileVersion(String version)
    {
        if (version!=null) {
            Integer.parseInt(version);   // check for valid integer value
        }
        this.forcedProfileVersion = version;
    }

    public void resetPassphrase() 
    {
        this.passphrase=null;
    }
    
    public void setPassphrase(byte[] passphrase) 
    {
        this.passphrase=passphrase;
        if (this.filecontent!=null) {
            this.filecontent.setPassphrase(passphrase);
        }
    }
}
