/*
 *
 * SPDX-License-Identifier: Apache-2.0
 */

import * as grpc from '@grpc/grpc-js';
import { connect, Contract, Identity, Signer, signers, Network, CloseableAsyncIterable, ChaincodeEvent, GatewayError } from '@hyperledger/fabric-gateway';
import * as crypto from 'crypto';
import { promises as fs } from 'fs';
import * as path from 'path';
import { TextDecoder } from 'util';
import * as dotenv from 'dotenv';

dotenv.config();
const channelName = envOrDefault('CHANNEL_NAME', 'channel1');
const chaincodeName = envOrDefault('CHAINCODE_NAME', 'adrenalineDLT');
const mspId = envOrDefault('MSP_ID', 'Org1MSP');

// Path to crypto materials.
const cryptoPath = envOrDefault('CRYPTO_PATH', path.resolve(__dirname, '..', '..', '..', 'test-network', 'organizations', 'peerOrganizations', 'org1.adrenaline.com'));

// Path to user private key directory.
const keyDirectoryPath = envOrDefault('KEY_DIRECTORY_PATH', path.resolve(cryptoPath, 'users', 'User1@org1.adrenaline.com', 'msp', 'keystore'));

// Path to user certificate directory.
const certDirectoryPath = envOrDefault('CERT_DIRECTORY_PATH', path.resolve(cryptoPath, 'users', 'User1@org1.adrenaline.com', 'msp', 'signcerts'));

// Path to peer tls certificate.
const tlsCertPath = envOrDefault('TLS_CERT_PATH', path.resolve(cryptoPath, 'peers', 'peer0.org1.adrenaline.com', 'tls', 'ca.crt'));

// Gateway peer endpoint.
const peerEndpoint = envOrDefault('PEER_ENDPOINT', 'localhost:7051');

// Gateway peer SSL host name override.
const peerHostAlias = envOrDefault('PEER_HOST_ALIAS', 'peer0.org1.adrenaline.com');

const utf8Decoder = new TextDecoder();
const assetId = `asset${Date.now()}`;

async function main(): Promise<void> {

    await displayInputParameters();

    // The gRPC client connection should be shared by all Gateway connections to this endpoint.
    const client = await newGrpcConnection();

    const gateway = connect({
        client,
        identity: await newIdentity(),
        signer: await newSigner(),
        // Default timeouts for different gRPC calls
        evaluateOptions: () => {
            return { deadline: Date.now() + 5000 }; // 5 seconds
        },
        endorseOptions: () => {
            return { deadline: Date.now() + 15000 }; // 15 seconds
        },
        submitOptions: () => {
            return { deadline: Date.now() + 5000 }; // 5 seconds
        },
        commitStatusOptions: () => {
            return { deadline: Date.now() + 60000 }; // 1 minute
        },
    });

    let events: CloseableAsyncIterable<ChaincodeEvent> | undefined;

    try {
        // Get a network instance representing the channel where the smart contract is deployed.
        const network = gateway.getNetwork(channelName);

        // Get the smart contract from the network.
        const contract = network.getContract(chaincodeName);

        //Listen for events emitted by transactions
        events = await startEventListening(network);

        // Initialize the ledger.
        await initLedger(contract);

// Create a new asset on the ledger and gets the blocknumber.
        const firstBlockNumber = await StoreTopoData(contract);  

        // Get the asset details by key.
        await RetrieveTopoData(contract);
        
        //Updates existing record of a topology
        await updateRecord(contract); 

        // Verifies the changes.
        await RetrieveTopoData(contract);

        //Deletes existing topology
        await deleteRecordByID(contract);

        // Return all the current assets on the ledger.
        await GetAllInfo(contract);

        // Update an asset which does not exist.
        await updateNonExistentRecord(contract)

        // Replay events from the block containing the first transactions
        await replayChaincodeEvents(network, firstBlockNumber);
        
    } finally {
        events?.close();
        gateway.close();
        client.close();
    }
}

main().catch(error => {
    console.error('******** FAILED to run the application:', error);
    process.exitCode = 1;
});

async function newGrpcConnection(): Promise<grpc.Client> {
    const tlsRootCert = await fs.readFile(tlsCertPath);
    const tlsCredentials = grpc.credentials.createSsl(tlsRootCert);
    return new grpc.Client(peerEndpoint, tlsCredentials, {
        'grpc.ssl_target_name_override': peerHostAlias,
    });
}

async function newIdentity(): Promise<Identity> {
    const certPath = await getFirstDirFileName(certDirectoryPath);
    const credentials = await fs.readFile(certPath);
    return { mspId, credentials };
}

async function getFirstDirFileName(dirPath: string): Promise<string> {
    const files = await fs.readdir(dirPath);
    return path.join(dirPath, files[0]);
}

async function newSigner(): Promise<Signer> {
    const keyPath = await getFirstDirFileName(keyDirectoryPath);
    const privateKeyPem = await fs.readFile(keyPath);
    const privateKey = crypto.createPrivateKey(privateKeyPem);
    return signers.newPrivateKeySigner(privateKey);
}

/**
 * This type of transaction would typically only be run once by an application the first time it was started after its
 * initial deployment. A new version of the chaincode deployed later would likely not need to run an "init" function.
 */
async function initLedger(contract: Contract): Promise<void> {
    try {
        console.log('\n--> Submit Transaction: InitLedger, function activates the chaincode');

        await contract.submitTransaction('InitLedger');
    
        console.log('*** Transaction committed successfully');
    } catch (error) {
        console.error('Failed to submit InitLedger transaction:', error);
        throw error; 
    }
}



/**
 * Evaluate a transaction to query ledger state.
 */
async function GetAllInfo(contract: Contract): Promise<void> {
    console.log('\n--> Evaluate Transaction: GetAllInfo, function returns all the current topologies on the ledger');

    const resultBytes = await contract.evaluateTransaction('GetAllInfo');

    const resultJson = utf8Decoder.decode(resultBytes);
    const result = JSON.parse(resultJson);
    console.log('*** Result:', result);
}

/**
 * Submit a transaction Asynchronously.
 */
async function StoreTopoData(contract: Contract): Promise<bigint> {
    console.log('\n--> Submit Transaction: StoreTopoData, records a new topology structure by Key values');
    
    const storeTopoDataPath = path.resolve(__dirname, '..', '..', '..', 'samples', 'sampleTopo.json');
    const jsonData = await fs.readFile(storeTopoDataPath, 'utf8');
    const result = await contract.submitAsync('StoreTopoData', {
        arguments: [assetId, jsonData]
    });

    const status = await result.getStatus();
    if (!status.successful) {
        throw new Error(`failed to commit transaction ${status.transactionId} with status code ${status.code}`);
    }

    console.log('*** Transaction committed successfully');
    return status.blockNumber;
}

/**
 * updateRecord(), updates topology record by key synchronously.
 */
async function updateRecord(contract: Contract): Promise<void> {
    console.log(`\n--> Submit transaction: UpdateTopoData, ${assetId}`);

    
    try {
        const updateTopoDataPath = path.resolve(__dirname,  '..', '..', '..', 'samples', 'updatedTopo.json');
        const jsonData = await fs.readFile(updateTopoDataPath, 'utf8');
        await contract.submitTransaction('UpdateTopoData', assetId, jsonData);
        console.log('UpdateTopoData committed successfully');
    } catch (error) {
        console.log('*** Successfully caught the error: \n', error);
    }
}

/**
 * RetrieveTopoData(), gets the topology information by key.
 */
async function RetrieveTopoData(contract: Contract): Promise<void> {
    console.log('\n--> Evaluate Transaction: RetrieveTopoData, function returns topology attributes');

    const resultBytes = await contract.evaluateTransaction('RetrieveTopoData', assetId);

    const resultJson = utf8Decoder.decode(resultBytes);
    const result = JSON.parse(resultJson);
    console.log('*** Result:', result);
}

/**
 * submitTransaction() will throw an error containing details of any error responses from the smart contract.
 */
async function updateNonExistentRecord(contract: Contract): Promise<void>{
    console.log('\n--> Submit Transaction: UpdateAsset asset70, asset70 does not exist and should return an error');

    try {
        const updateTopoDataPath = path.resolve(__dirname,  '..', '..', '..', 'samples', 'updatedTopo.json');
        const jsonData = await fs.readFile(updateTopoDataPath, 'utf8');
        await contract.submitTransaction('UpdateTopoData', 'nonExist', jsonData)
        console.log('******** FAILED to return an error');
    } catch (error) {
        console.log('*** Successfully caught the error: \n', error);
    }
}

/**
 * deleteRecordByID() removes a record from the ledger.
 */

async function deleteRecordByID(contract: Contract): Promise<void>{
    console.log(`\n--> Submit transaction: deleteRecordByID, ${assetId}`);
    try {
   
        await contract.submitTransaction('DeleteTopo', assetId);
        console.log('\n*** deleteRecordByID committed successfully');
    } catch (error) {
        console.log('*** Successfully caught the error: \n', error);
    }
}


/**
 * envOrDefault() will return the value of an environment variable, or a default value if the variable is undefined.
 */
function envOrDefault(key: string, defaultValue: string): string {
    return process.env[key] || defaultValue;
}

/**
 * displayInputParameters() will print the global scope parameters used by the main driver routine.
 */
async function displayInputParameters(): Promise<void> {
    console.log(`channelName:       ${channelName}`);
    console.log(`chaincodeName:     ${chaincodeName}`);
    console.log(`mspId:             ${mspId}`);
    console.log(`cryptoPath:        ${cryptoPath}`);
    console.log(`keyDirectoryPath:  ${keyDirectoryPath}`);
    console.log(`certDirectoryPath: ${certDirectoryPath}`);
    console.log(`tlsCertPath:       ${tlsCertPath}`);
    console.log(`peerEndpoint:      ${peerEndpoint}`);
    console.log(`peerHostAlias:     ${peerHostAlias}`);
}

/**
 * startEventListening() will initiate the event listener for chaincode events.
 */
async function startEventListening(network: Network): Promise<CloseableAsyncIterable<ChaincodeEvent>> {
    console.log('\n*** Start chaincode event listening');

    const events = await network.getChaincodeEvents(chaincodeName);

    void readEvents(events); // Don't await - run asynchronously
    return events;
}

/**
 * readEvents() format and display the events as a JSON.
 */
async function readEvents(events: CloseableAsyncIterable<ChaincodeEvent>): Promise<void> {
    try {
        for await (const event of events) {
            const payload = parseJson(event.payload);
            console.log(`\n<-- Chaincode event received: ${event.eventName} -`, payload);
        }
    } catch (error: unknown) {
        // Ignore the read error when events.close() is called explicitly
        if (!(error instanceof GatewayError) || error.code !== grpc.status.CANCELLED.valueOf()) {
            throw error;
        }
    }
}

/**
 * parseJson() formats a JSON.
 */
function parseJson(jsonBytes: Uint8Array): unknown {
    const json = utf8Decoder.decode(jsonBytes);
    return JSON.parse(json);
}


/**
 * replayChaincodeEvents()
 */
async function replayChaincodeEvents(network: Network, startBlock: bigint): Promise<void> {
    console.log('\n*** Start chaincode event replay');
    
    const events = await network.getChaincodeEvents(chaincodeName, {
        startBlock,
    });

    try {
        for await (const event of events) {
            const payload = parseJson(event.payload);
            console.log(`\n<-- Chaincode event replayed: ${event.eventName} -`, payload);

            if (event.eventName === 'DeleteTopo') {
                // Reached the last submitted transaction so break to stop listening for events
                break;
            }
        }
    } finally {
        events.close();
    }
}