package sigcred

import (
	"bytes"
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/sha256"
	"dehub/fs"
	"dehub/yamlutil"
	"fmt"
	"io"
	"io/ioutil"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"golang.org/x/crypto/openpgp/armor"
	"golang.org/x/crypto/openpgp/packet"
)

// CredentialPGPSignature describes a PGP signature which has been used to sign
// a commit.
type CredentialPGPSignature struct {
	PubKeyID string        `yaml:"pub_key_id"`
	Body     yamlutil.Blob `yaml:"body"`
}

type pgpPubKey struct {
	pubKey *packet.PublicKey
}

func newPGPPubKey(r io.Reader) (pgpPubKey, error) {
	// TODO support non-armored keys as well
	block, err := armor.Decode(r)
	if err != nil {
		return pgpPubKey{}, fmt.Errorf("could not decode armored PGP public key: %w", err)
	}

	pkt, err := packet.Read(block.Body)
	if err != nil {
		return pgpPubKey{}, fmt.Errorf("could not read PGP public key: %w", err)
	}

	pubKey, ok := pkt.(*packet.PublicKey)
	if !ok {
		return pgpPubKey{}, fmt.Errorf("packet is not a public key, it's a %T", pkt)
	}

	return pgpPubKey{pubKey: pubKey}, nil
}

func (s pgpPubKey) Signed(_ fs.FS, cred Credential) (bool, error) {
	if cred.PGPSignature == nil {
		return false, nil
	}

	return cred.PGPSignature.PubKeyID == s.pubKey.KeyIdString(), nil
}

func (s pgpPubKey) Verify(_ fs.FS, data []byte, cred Credential) error {
	credSig := cred.PGPSignature
	if credSig == nil {
		return fmt.Errorf("SignifierPGPFile cannot verify %+v", cred)
	}

	pkt, err := packet.Read(bytes.NewBuffer(credSig.Body))
	if err != nil {
		return fmt.Errorf("could not read signature packet: %w", err)
	}

	sigPkt, ok := pkt.(*packet.Signature)
	if !ok {
		return fmt.Errorf("signature bytes were parsed as a %T, not a signature", pkt)
	}

	// The gpg process which is invoked during normal signing automatically
	// hashes whatever is piped to it. The VerifySignature method in the openpgp
	// package expects you to do it yourself.
	h := sigPkt.Hash.New()
	h.Write(data)
	return s.pubKey.VerifySignature(h, sigPkt)
}

func (s pgpPubKey) encode() ([]byte, error) {
	body := new(bytes.Buffer)
	armorEncoder, err := armor.Encode(body, "PGP PUBLIC KEY", nil)
	if err != nil {
		return nil, fmt.Errorf("error initializing armor encoder: %w", err)
	} else if err := s.pubKey.Serialize(armorEncoder); err != nil {
		return nil, fmt.Errorf("error encoding public key: %w", err)
	} else if err := armorEncoder.Close(); err != nil {
		return nil, fmt.Errorf("error closing armor encoder: %w", err)
	}
	return body.Bytes(), nil
}

func (s pgpPubKey) asSignfier() (SignifierPGP, error) {
	body, err := s.encode()
	if err != nil {
		return SignifierPGP{}, err
	}

	return SignifierPGP{
		Body: string(body),
	}, nil
}

type pgpPrivKey struct {
	pgpPubKey
	privKey *packet.PrivateKey
}

// SignifierPGPTmp returns a direct implementation of the SignifierInterface
// which uses a random private key generated in memory, as well as an armored
// version of its public key.
func SignifierPGPTmp(randReader io.Reader) (SignifierInterface, []byte) {
	rawPrivKey, err := ecdsa.GenerateKey(elliptic.P521(), randReader)
	if err != nil {
		panic(err)
	}

	privKeyRaw := packet.NewECDSAPrivateKey(time.Now(), rawPrivKey)
	privKey := pgpPrivKey{
		pgpPubKey: pgpPubKey{
			pubKey: &privKeyRaw.PublicKey,
		},
		privKey: privKeyRaw,
	}

	pubKeyBody, err := privKey.pgpPubKey.encode()
	if err != nil {
		panic(err)
	}
	return privKey, pubKeyBody
}

func (s pgpPrivKey) Sign(_ fs.FS, data []byte) (Credential, error) {
	h := sha256.New()
	h.Write(data)
	var sig packet.Signature
	sig.Hash = crypto.SHA256
	sig.PubKeyAlgo = s.pubKey.PubKeyAlgo
	if err := sig.Sign(h, s.privKey, nil); err != nil {
		return Credential{}, fmt.Errorf("failed to sign data: %w", err)
	}

	body := new(bytes.Buffer)
	if err := sig.Serialize(body); err != nil {
		return Credential{}, fmt.Errorf("failed to serialize signature: %w", err)
	}

	return Credential{
		PGPSignature: &CredentialPGPSignature{
			PubKeyID: s.pubKey.KeyIdString(),
			Body:     body.Bytes(),
		},
	}, nil
}

// SignifierPGP describes a pgp public key whose corresponding private key will
// be used as a signing key.
type SignifierPGP struct {
	Body string `yaml:"body"`
}

var _ SignifierInterface = SignifierPGP{}

func (s SignifierPGP) load() (pgpPubKey, error) {
	return newPGPPubKey(strings.NewReader(s.Body))
}

// Sign will sign the given arbitrary bytes using the private key corresponding
// to the pgp public key embedded in this Signifier.
func (s SignifierPGP) Sign(fs fs.FS, data []byte) (Credential, error) {
	sigPGP, err := s.load()
	if err != nil {
		return Credential{}, err
	}

	stderr := new(bytes.Buffer)
	cmd := exec.Command("gpg",
		"--openpgp",
		"--detach-sign",
		"--local-user", sigPGP.pubKey.KeyIdString())
	cmd.Stdin = bytes.NewBuffer(data)
	cmd.Stderr = stderr
	sig, err := cmd.Output()
	if err != nil {
		return Credential{}, fmt.Errorf("error signing with gpg (%v): %s", err, stderr.String())
	}

	return Credential{
		PGPSignature: &CredentialPGPSignature{
			PubKeyID: sigPGP.pubKey.KeyIdString(),
			Body:     sig,
		},
	}, nil
}

// Signed returns true if the private key corresponding to the pgp public key
// embedded in this Signifier was used to produce the given Credential.
func (s SignifierPGP) Signed(fs fs.FS, cred Credential) (bool, error) {
	sigPGP, err := s.load()
	if err != nil {
		return false, err
	}

	return sigPGP.Signed(fs, cred)
}

// Verify asserts that the given signature was produced by this key signing the
// given piece of data.
func (s SignifierPGP) Verify(fs fs.FS, data []byte, cred Credential) error {
	sigPGP, err := s.load()
	if err != nil {
		return err
	}
	return sigPGP.Verify(fs, data, cred)
}

// SignifierPGPFile is the same as SignifierPGP, except that the public key is
// found in the repo rather than encoded into the object.
type SignifierPGPFile struct {
	Path string `yaml:"path"`
}

var _ SignifierInterface = SignifierPGPFile{}

func (s SignifierPGPFile) load(fs fs.FS) (SignifierPGP, error) {
	path := filepath.Clean(s.Path)
	fr, err := fs.Open(path)
	if err != nil {
		return SignifierPGP{}, fmt.Errorf("could not open PGP public key file at %q: %w", path, err)
	}
	defer fr.Close()

	pubKeyB, err := ioutil.ReadAll(fr)
	if err != nil {
		return SignifierPGP{}, fmt.Errorf("could not read PGP public key from file blob at %q: %w", s.Path, err)
	}

	return SignifierPGP{Body: string(pubKeyB)}, nil
}

// Sign will sign the given arbitrary bytes using the private key corresponding
// to the pgp public key located by this Signifier.
func (s SignifierPGPFile) Sign(fs fs.FS, data []byte) (Credential, error) {
	sigPGP, err := s.load(fs)
	if err != nil {
		return Credential{}, err
	}
	return sigPGP.Sign(fs, data)
}

// Signed returns true if the private key corresponding to the pgp public key
// located by this Signifier was used to produce the given Credential.
func (s SignifierPGPFile) Signed(fs fs.FS, cred Credential) (bool, error) {
	if cred.PGPSignature == nil {
		return false, nil
	}

	sigPGP, err := s.load(fs)
	if err != nil {
		return false, err
	}
	return sigPGP.Signed(fs, cred)
}

// Verify asserts that the given signature was produced by this key signing the
// given piece of data.
func (s SignifierPGPFile) Verify(fs fs.FS, data []byte, cred Credential) error {
	sigPGP, err := s.load(fs)
	if err != nil {
		return err
	}
	return sigPGP.Verify(fs, data, cred)
}