/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package main

import (
	"bytes"
	"crypto/tls"
	"crypto/x509"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"

	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/internal/osnadmin"
	"github.com/hyperledger/fabric/protoutil"
	"google.golang.org/protobuf/proto"
	"gopkg.in/alecthomas/kingpin.v2"
)

func main() {
	kingpin.Version("0.0.1")

	output, exit, err := executeForArgs(os.Args[1:])
	if err != nil {
		kingpin.Fatalf("parsing arguments: %s. Try --help", err)
	}
	fmt.Println(output)
	os.Exit(exit)
}

func executeForArgs(args []string) (output string, exit int, err error) {
	//
	// command line flags
	//
	app := kingpin.New("osnadmin", "Orderer Service Node (OSN) administration")
	orderer := app.Flag("orderer-address", "Admin endpoint of the OSN").Short('o').Required().String()
	caFile := app.Flag("ca-file", "Path to file containing PEM-encoded TLS CA certificate(s) for the OSN").String()
	clientCert := app.Flag("client-cert", "Path to file containing PEM-encoded X509 public key to use for mutual TLS communication with the OSN").String()
	clientKey := app.Flag("client-key", "Path to file containing PEM-encoded private key to use for mutual TLS communication with the OSN").String()
	noStatus := app.Flag("no-status", "Remove the HTTP status message from the command output").Default("false").Bool()

	channel := app.Command("channel", "Channel actions")

	join := channel.Command("join", "Join an Ordering Service Node (OSN) to a channel. If the channel does not yet exist, it will be created.")
	joinChannelID := join.Flag("channelID", "Channel ID").Short('c').Required().String()
	configBlockPath := join.Flag("config-block", "Path to the file containing an up-to-date config block for the channel").Short('b').Required().String()

	list := channel.Command("list", "List channel information for an Ordering Service Node (OSN). If the channelID flag is set, more detailed information will be provided for that channel.")
	listChannelID := list.Flag("channelID", "Channel ID").Short('c').String()

	remove := channel.Command("remove", "Remove a channel from an Ordering Service Node (OSN).")
	removeChannelID := remove.Flag("channelID", "Channel ID").Short('c').Required().String()

	command, err := app.Parse(args)
	if err != nil {
		return "", 1, err
	}

	//
	// flag validation
	//
	var (
		osnURL        string
		caCertPool    *x509.CertPool
		tlsClientCert tls.Certificate
	)
	// TLS enabled
	if *caFile != "" {
		osnURL = fmt.Sprintf("https://%s", *orderer)
		var err error
		caCertPool = x509.NewCertPool()
		caFilePEM, err := os.ReadFile(*caFile)
		if err != nil {
			return "", 1, fmt.Errorf("reading orderer CA certificate: %s", err)
		}
		if !caCertPool.AppendCertsFromPEM(caFilePEM) {
			return "", 1, fmt.Errorf("failed to add ca-file PEM to cert pool")
		}

		tlsClientCert, err = tls.LoadX509KeyPair(*clientCert, *clientKey)
		if err != nil {
			return "", 1, fmt.Errorf("loading client cert/key pair: %s", err)
		}
	} else { // TLS disabled
		osnURL = fmt.Sprintf("http://%s", *orderer)
	}

	var marshaledConfigBlock []byte
	if *configBlockPath != "" {
		marshaledConfigBlock, err = os.ReadFile(*configBlockPath)
		if err != nil {
			return "", 1, fmt.Errorf("reading config block: %s", err)
		}

		err = validateBlockChannelID(marshaledConfigBlock, *joinChannelID)
		if err != nil {
			return "", 1, err
		}
	}

	//
	// call the underlying implementations
	//
	var resp *http.Response

	switch command {
	case join.FullCommand():
		resp, err = osnadmin.Join(osnURL, marshaledConfigBlock, caCertPool, tlsClientCert)
	case list.FullCommand():
		if *listChannelID != "" {
			resp, err = osnadmin.ListSingleChannel(osnURL, *listChannelID, caCertPool, tlsClientCert)
			break
		}
		resp, err = osnadmin.ListAllChannels(osnURL, caCertPool, tlsClientCert)
	case remove.FullCommand():
		resp, err = osnadmin.Remove(osnURL, *removeChannelID, caCertPool, tlsClientCert)
	}
	if err != nil {
		return errorOutput(err), 1, nil
	}

	bodyBytes, err := readBodyBytes(resp.Body)
	if err != nil {
		return errorOutput(err), 1, nil
	}

	output, err = responseOutput(!*noStatus, resp.StatusCode, bodyBytes)
	if err != nil {
		return errorOutput(err), 1, nil
	}

	return output, 0, nil
}

func responseOutput(showStatus bool, statusCode int, responseBody []byte) (string, error) {
	var buffer bytes.Buffer
	if showStatus {
		fmt.Fprintf(&buffer, "Status: %d\n", statusCode)
	}
	if len(responseBody) != 0 {
		if err := json.Indent(&buffer, responseBody, "", "\t"); err != nil {
			return "", err
		}
	}
	return buffer.String(), nil
}

func readBodyBytes(body io.ReadCloser) ([]byte, error) {
	bodyBytes, err := io.ReadAll(body)
	if err != nil {
		return nil, fmt.Errorf("reading http response body: %s", err)
	}
	body.Close()

	return bodyBytes, nil
}

func errorOutput(err error) string {
	return fmt.Sprintf("Error: %s\n", err)
}

func validateBlockChannelID(blockBytes []byte, channelID string) error {
	block := &common.Block{}
	err := proto.Unmarshal(blockBytes, block)
	if err != nil {
		return fmt.Errorf("unmarshalling block: %s", err)
	}

	blockChannelID, err := protoutil.GetChannelIDFromBlock(block)
	if err != nil {
		return err
	}

	// quick sanity check that the orderer admin is joining
	// the channel they think they're joining.
	if channelID != blockChannelID {
		return fmt.Errorf("specified --channelID %s does not match channel ID %s in config block", channelID, blockChannelID)
	}

	return nil
}
