// cfssljson splits out JSON with cert, csr, and key fields to separate
// files.
package main

import (
	"encoding/base64"
	"encoding/json"
	"flag"
	"fmt"
	"io"
	"os"

	"github.com/cloudflare/cfssl/cli/version"
)

func readFile(filespec string) ([]byte, error) {
	if filespec == "-" {
		return io.ReadAll(os.Stdin)
	}
	return os.ReadFile(filespec)
}

func writeFile(filespec, contents string, perms os.FileMode) {
	err := os.WriteFile(filespec, []byte(contents), perms)
	if err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

// ResponseMessage represents the format of a CFSSL output for an error or message
type ResponseMessage struct {
	Code    int    `json:"int"`
	Message string `json:"message"`
}

// Response represents the format of a CFSSL output
type Response struct {
	Success  bool                   `json:"success"`
	Result   map[string]interface{} `json:"result"`
	Errors   []ResponseMessage      `json:"errors"`
	Messages []ResponseMessage      `json:"messages"`
}

type outputFile struct {
	Filename string
	Contents string
	IsBinary bool
	Perms    os.FileMode
}

func main() {
	bare := flag.Bool("bare", false, "the response from CFSSL is not wrapped in the API standard response")
	inFile := flag.String("f", "-", "JSON input")
	output := flag.Bool("stdout", false, "output the response instead of saving to a file")
	printVersion := flag.Bool("version", false, "print version and exit")
	flag.Parse()

	if *printVersion {
		fmt.Printf("%s", version.FormatVersion())
		return
	}

	var baseName string
	if flag.NArg() == 0 {
		baseName = "cert"
	} else {
		baseName = flag.Arg(0)
	}

	var input = map[string]interface{}{}
	var outs []outputFile
	var cert string
	var key string
	var csr string

	fileData, err := readFile(*inFile)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Failed to read input: %v\n", err)
		os.Exit(1)
	}

	if *bare {
		err = json.Unmarshal(fileData, &input)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to parse input: %v\n", err)
			os.Exit(1)
		}
	} else {
		var response Response
		err = json.Unmarshal(fileData, &response)
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to parse input: %v\n", err)
			os.Exit(1)
		}

		if !response.Success {
			fmt.Fprintf(os.Stderr, "Request failed:\n")
			for _, msg := range response.Errors {
				fmt.Fprintf(os.Stderr, "\t%s\n", msg.Message)
			}
			os.Exit(1)
		}

		input = response.Result
	}

	if contents, ok := input["cert"]; ok {
		cert = contents.(string)
	} else if contents, ok = input["certificate"]; ok {
		cert = contents.(string)
	}
	if cert != "" {
		outs = append(outs, outputFile{
			Filename: baseName + ".pem",
			Contents: cert,
			Perms:    0664,
		})
	}

	if contents, ok := input["key"]; ok {
		key = contents.(string)
	} else if contents, ok = input["private_key"]; ok {
		key = contents.(string)
	}
	if key != "" {
		outs = append(outs, outputFile{
			Filename: baseName + "-key.pem",
			Contents: key,
			Perms:    0600,
		})
	}

	if contents, ok := input["encrypted_key"]; ok {
		encKey := contents.(string)
		outs = append(outs, outputFile{
			Filename: baseName + "-key.enc",
			Contents: encKey,
			IsBinary: true,
			Perms:    0600,
		})
	}

	if contents, ok := input["csr"]; ok {
		csr = contents.(string)
	} else if contents, ok = input["certificate_request"]; ok {
		csr = contents.(string)
	}
	if csr != "" {
		outs = append(outs, outputFile{
			Filename: baseName + ".csr",
			Contents: csr,
			Perms:    0644,
		})
	}

	if result, ok := input["result"].(map[string]interface{}); ok {
		if bundle, ok := result["bundle"].(map[string]interface{}); ok {

			// if we've gotten this deep then we're trying to parse out
			// a bundle, now we fail if we can't find the keys we need.

			certificateBundle, ok := bundle["bundle"].(string)
			if !ok {
				fmt.Fprintf(os.Stderr, "inner bundle parsing failed!\n")
				os.Exit(1)
			}
			rootCertificate, ok := bundle["root"].(string)
			if !ok {
				fmt.Fprintf(os.Stderr, "root parsing failed!\n")
				os.Exit(1)
			}
			outs = append(outs, outputFile{
				Filename: baseName + "-bundle.pem",
				Contents: certificateBundle + "\n" + rootCertificate,
				Perms:    0644,
			})
			outs = append(outs, outputFile{
				Filename: baseName + "-root.pem",
				Contents: rootCertificate,
				Perms:    0644,
			})
		}
	}

	if contents, ok := input["ocspResponse"]; ok {
		// ocspResponse is base64 encoded
		resp, err := base64.StdEncoding.DecodeString(contents.(string))
		if err != nil {
			fmt.Fprintf(os.Stderr, "Failed to parse ocspResponse: %v\n", err)
			os.Exit(1)
		}
		outs = append(outs, outputFile{
			Filename: baseName + "-response.der",
			Contents: string(resp),
			IsBinary: true,
			Perms:    0644,
		})
	}

	for _, e := range outs {
		if *output {
			if e.IsBinary {
				e.Contents = base64.StdEncoding.EncodeToString([]byte(e.Contents))
			}
			fmt.Fprintf(os.Stdout, "%s\n", e.Contents)
		} else {
			writeFile(e.Filename, e.Contents, e.Perms)
		}
	}
}
