/**
 * Crypto config contexts - crypto suite

The challenge is to have cryptographic primitives be upgradable seamlessly.

One facet of this we have to acknowledge is that a user may use numerous different devices and client versions to
access the system. This means having dynamic crypto suite negotiation between peers or even of oneself for messages
or stored keys cannot be done while making sure all the user's clients agree. Not all clients are online all the time
to prevent premature upgrade.

So to make sure a newer client won't lock out an older one for the same user we need a current preferred version that
will be replaced by an already available newer one after some time. Basically introduce newer/better/bigger
cryptographic primitives in advance so when a change has to be made all clients will reasonably be supporting that.
The window for such a transition shall be at least a few months to allow all clients to be upgraded.

At the same time of a preferred suite upgrade the older crypto primitives can be deprecated, meaning no encryption
shall be done with them. For decrypting old data they shall not be removed from the context though. The new suite shall
warn anyone if new data is to be processed with an obsolete primitive.

In terms of new suite deployment order follow the general major upgrade policy: TODO: write this down somewhere
    - all client and nano apps support the current preferred suite: v1.0
    - create client and nano apps for all platforms with the new suite while the preferred is still the old one: v1.1
    - wait a sufficient time so clients can be upgraded before enforcing the new suite
    - create client and nano apps for all platforms with the new suite as preferred and the old one deprecated: v2.0
    - any client or nano that failed to upgrade in the time window will no longer work
 */

import { base64 } from "rfc4648";
import { MissingKey } from "../keyring/base";

/**
 * The dump() of AbstractCryptoConfig shall raise this if the provided arguments are not sufficient to perform the
    implementation. The context will try to fall back to older versions.
 */
export class ArgumentsError extends Error{
}

export class UnknownCryptoConfigVersion extends Error{
}

    
export class CryptoConfigVersioning{

    public static config_version_from_int(v: number): Uint8Array{
        if (v>0 && v<128){
            return new Uint8Array([v]);
        }
        throw "config_version_from_int Error";
    }

    public static config_version_from_data(d: Uint8Array): Uint8Array{
        if (d && d.byteLength > 0){
            return d.slice(0,1);
        }
        
        return null;
    }

    public static config_version_to_int(v: Uint8Array): number{
        if (v.byteLength == 1){
            return v[0];
        }
        throw "config_version_to_int Error"
    }

}

export abstract class AbstractConfigLoadArgs{

    public cipher: Uint8Array;

    constructor(cipher: Uint8Array){
        this.cipher = cipher;
    }
}

export abstract class AbstractConfigDumpArgs{

}


/**
 * Versioning of production configs shall start from 11, because having development configs with preceding versions
    is beneficial.
 */
export abstract class AbstractCryptoConfig<_TLoad extends AbstractConfigLoadArgs, _TDump extends AbstractConfigDumpArgs> extends CryptoConfigVersioning{

    public version: Uint8Array;
    public never_dump: boolean;

    constructor(version: number, never_dump: boolean = false){
        super();
        this.version = AbstractCryptoConfig.config_version_from_int(version);
        this.never_dump = never_dump;
    }

    /**
     * Decrypt content and/or verify signature.
        The first argument will be the packed version of this config that the payload is prefixed with. Depending
        on the implementation the payload shall be wrapped in a memory-view to use zero-copy operations.
     */
    public abstract load(args: _TLoad): Promise<Uint8Array>;

    /**
     * 
     * Encrypt content and/or create signature.
        The first argument will be the packed version of this config. The returned value must be prefixed with it!
        Arguments of older version may get expanded so it is still callable with new/current arguments.

     */
    public abstract dump(args: _TDump): Promise<Uint8Array>;
}

export class CryptoContext<_TLoad extends AbstractConfigLoadArgs, _TDump extends AbstractConfigDumpArgs> extends CryptoConfigVersioning{

    private _configs: AbstractCryptoConfig<_TLoad, _TDump>[] = [];

    constructor(config?: AbstractCryptoConfig<_TLoad, _TDump>[]){
        super();
        if (config !== undefined){
            let sortedConfig = config.sort((a, b)=>{
                return CryptoContext.config_version_to_int(b.version) - CryptoContext.config_version_to_int(a.version)
            })

            sortedConfig.forEach((c)=>{
                
                if (this._configs.indexOf(c) > -1){
                    throw "Config version conflict for context";
                }
                this._configs.push(c)
            })
        }
        else{
            throw "CryptoContext invalid config"
        }
        
    }

    public top_config_version(): Uint8Array{
        return this._configs[0].version;
    }

    public top_config(): [Uint8Array, AbstractCryptoConfig<_TLoad, _TDump>]{
        return [this._configs[0].version, this._configs[0]];
    }

    /**
     * Decrypt or verify data.
        For special (raw) payloads the version can also be returned so the processing can be updated by
        a new version of the payload.
     */
    public load(args: _TLoad): Promise<Uint8Array>{
        return new Promise(async(resolve, reject)=>{
            let version = CryptoContext.config_version_from_data(args.cipher);
            
            if (version === null) reject(new UnknownCryptoConfigVersion(version+""))
            
            for(let i = 0; i < this._configs.length; i++){
                if (base64.stringify(version) == base64.stringify(this._configs[i].version)){
                    await this._configs[i].load(args).then((load)=>{
                        resolve(load);
                    }).catch((err)=>{
                        reject(err);
                    })
                }
                
            }
            
            reject(new UnknownCryptoConfigVersion(version+""))
        })
        
    }

    public dump(args: _TDump): Promise<Uint8Array>{
        let _dump = (index)=>{
            if (index >= this._configs.length) throw new ArgumentsError();

            if (this._configs[index].never_dump){
                return _dump(index+1);
            }
            
            return this._configs[index].dump(args).then((packed)=>{                
                if (packed[0] == this._configs[index].version[0]){
                    return packed;
                }
                else{
                    throw "AssertionError - version mismatch";
                }
            }).catch((err)=>{ 
                if (err instanceof ArgumentsError || err instanceof MissingKey){
                    return _dump(index+1)
                }
                throw err;
            });
            
        }

        return _dump(0);
    }

}

