package ca

import (
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"encoding/pem"
	"fmt"
	"io"
	"net/http"
	"strings"

	"github.com/pkg/errors"
	"github.com/smallstep/certificates/acme"
	acmeAPI "github.com/smallstep/certificates/acme/api"
	"go.step.sm/crypto/jose"
)

// ACMEClient implements an HTTP client to an ACME API.
type ACMEClient struct {
	client *http.Client
	dirLoc string
	dir    *acmeAPI.Directory
	acc    *acme.Account
	Key    *jose.JSONWebKey
	kid    string
}

// NewACMEClient initializes a new ACMEClient.
func NewACMEClient(endpoint string, contact []string, opts ...ClientOption) (*ACMEClient, error) {
	// Retrieve transport from options.
	o := new(clientOptions)
	if err := o.apply(opts); err != nil {
		return nil, err
	}
	tr, err := o.getTransport(endpoint)
	if err != nil {
		return nil, err
	}
	ac := &ACMEClient{
		client: &http.Client{
			Transport: tr,
		},
		dirLoc: endpoint,
	}
	req, err := http.NewRequest("GET", endpoint, http.NoBody)
	if err != nil {
		return nil, errors.Wrapf(err, "creating GET request %s failed", endpoint)
	}
	req.Header.Set("User-Agent", UserAgent)
	resp, err := ac.client.Do(req)
	if err != nil {
		return nil, errors.Wrapf(err, "client GET %s failed", endpoint)
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}
	var dir acmeAPI.Directory
	if err := readJSON(resp.Body, &dir); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", endpoint)
	}

	ac.dir = &dir

	ac.Key, err = jose.GenerateJWK("EC", "P-256", "ES256", "sig", "", 0)
	if err != nil {
		return nil, err
	}

	nar := &acmeAPI.NewAccountRequest{
		Contact:              contact,
		TermsOfServiceAgreed: true,
	}
	payload, err := json.Marshal(nar)
	if err != nil {
		return nil, errors.Wrap(err, "error marshaling new account request")
	}

	resp, err = ac.post(payload, ac.dir.NewAccount, withJWK(ac))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}
	var acc acme.Account
	if err := readJSON(resp.Body, &acc); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", dir.NewAccount)
	}
	ac.acc = &acc
	ac.kid = resp.Header.Get("Location")

	return ac, nil
}

// GetDirectory makes a directory request to the ACME api and returns an
// ACME directory object.
func (c *ACMEClient) GetDirectory() (*acmeAPI.Directory, error) {
	return c.dir, nil
}

// GetNonce makes a nonce request to the ACME api and returns an
// ACME directory object.
func (c *ACMEClient) GetNonce() (string, error) {
	req, err := http.NewRequest("GET", c.dir.NewNonce, http.NoBody)
	if err != nil {
		return "", errors.Wrapf(err, "creating GET request %s failed", c.dir.NewNonce)
	}
	req.Header.Set("User-Agent", UserAgent)
	resp, err := c.client.Do(req)
	if err != nil {
		return "", errors.Wrapf(err, "client GET %s failed", c.dir.NewNonce)
	}
	if resp.StatusCode >= 400 {
		return "", readACMEError(resp.Body)
	}
	return resp.Header.Get("Replay-Nonce"), nil
}

type withHeaderOption func(so *jose.SignerOptions)

func withJWK(c *ACMEClient) withHeaderOption {
	return func(so *jose.SignerOptions) {
		so.WithHeader("jwk", c.Key.Public())
	}
}

func withKid(c *ACMEClient) withHeaderOption {
	return func(so *jose.SignerOptions) {
		so.WithHeader("kid", c.kid)
	}
}

// serialize serializes a json web signature and doesn't omit empty fields.
func serialize(obj *jose.JSONWebSignature) (string, error) {
	raw, err := obj.CompactSerialize()
	if err != nil {
		return "", errors.Wrap(err, "error serializing JWS")
	}
	parts := strings.Split(raw, ".")
	msg := struct {
		Protected string `json:"protected"`
		Payload   string `json:"payload"`
		Signature string `json:"signature"`
	}{Protected: parts[0], Payload: parts[1], Signature: parts[2]}
	b, err := json.Marshal(msg)
	if err != nil {
		return "", errors.Wrap(err, "error marshaling jws message")
	}
	return string(b), nil
}

func (c *ACMEClient) post(payload []byte, url string, headerOps ...withHeaderOption) (*http.Response, error) {
	if c.Key == nil {
		return nil, errors.New("acme client not configured with account")
	}
	nonce, err := c.GetNonce()
	if err != nil {
		return nil, err
	}
	so := new(jose.SignerOptions)
	so.WithHeader("nonce", nonce)
	so.WithHeader("url", url)
	for _, hop := range headerOps {
		hop(so)
	}
	signer, err := jose.NewSigner(jose.SigningKey{
		Algorithm: jose.SignatureAlgorithm(c.Key.Algorithm),
		Key:       c.Key.Key,
	}, so)
	if err != nil {
		return nil, errors.Wrap(err, "error creating JWS signer")
	}
	signed, err := signer.Sign(payload)
	if err != nil {
		return nil, errors.Errorf("error signing payload: %s", strings.TrimPrefix(err.Error(), "square/go-jose: "))
	}
	raw, err := serialize(signed)
	if err != nil {
		return nil, err
	}
	req, err := http.NewRequest("POST", url, strings.NewReader(raw))
	if err != nil {
		return nil, errors.Wrapf(err, "creating POST request %s failed", url)
	}
	req.Header.Set("Content-Type", "application/jose+json")
	req.Header.Set("User-Agent", UserAgent)
	resp, err := c.client.Do(req)
	if err != nil {
		return nil, errors.Wrapf(err, "client POST %s failed", c.dir.NewOrder)
	}
	return resp, nil
}

// NewOrder creates and returns the information for a new ACME order.
func (c *ACMEClient) NewOrder(payload []byte) (*acme.Order, error) {
	resp, err := c.post(payload, c.dir.NewOrder, withKid(c))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}

	var o acme.Order
	if err := readJSON(resp.Body, &o); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", c.dir.NewOrder)
	}
	o.ID = resp.Header.Get("Location")
	return &o, nil
}

// GetChallenge returns the Challenge at the given path.
// With the validate parameter set to True this method will attempt to validate the
// challenge before returning it.
func (c *ACMEClient) GetChallenge(url string) (*acme.Challenge, error) {
	resp, err := c.post(nil, url, withKid(c))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}

	var ch acme.Challenge
	if err := readJSON(resp.Body, &ch); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", url)
	}
	return &ch, nil
}

// ValidateChallenge returns the Challenge at the given path.
// With the validate parameter set to True this method will attempt to validate the
// challenge before returning it.
func (c *ACMEClient) ValidateChallenge(url string) error {
	resp, err := c.post([]byte("{}"), url, withKid(c))
	if err != nil {
		return err
	}
	if resp.StatusCode >= 400 {
		return readACMEError(resp.Body)
	}
	return nil
}

// GetAuthz returns the Authz at the given path.
func (c *ACMEClient) GetAuthz(url string) (*acme.Authorization, error) {
	resp, err := c.post(nil, url, withKid(c))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}

	var az acme.Authorization
	if err := readJSON(resp.Body, &az); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", url)
	}
	return &az, nil
}

// GetOrder returns the Order at the given path.
func (c *ACMEClient) GetOrder(url string) (*acme.Order, error) {
	resp, err := c.post(nil, url, withKid(c))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}

	var o acme.Order
	if err := readJSON(resp.Body, &o); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", url)
	}
	return &o, nil
}

// FinalizeOrder makes a finalize request to the ACME api.
func (c *ACMEClient) FinalizeOrder(url string, csr *x509.CertificateRequest) error {
	payload, err := json.Marshal(acmeAPI.FinalizeRequest{
		CSR: base64.RawURLEncoding.EncodeToString(csr.Raw),
	})
	if err != nil {
		return errors.Wrap(err, "error marshaling finalize request")
	}
	resp, err := c.post(payload, url, withKid(c))
	if err != nil {
		return err
	}
	if resp.StatusCode >= 400 {
		return readACMEError(resp.Body)
	}
	return nil
}

// GetCertificate retrieves the certificate along with all intermediates.
func (c *ACMEClient) GetCertificate(url string) (*x509.Certificate, []*x509.Certificate, error) {
	resp, err := c.post(nil, url, withKid(c))
	if err != nil {
		return nil, nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, nil, readACMEError(resp.Body)
	}
	defer resp.Body.Close()
	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, nil, errors.Wrap(err, "error reading GET certificate response")
	}

	var certs []*x509.Certificate

	block, rest := pem.Decode(bodyBytes)
	if block == nil {
		return nil, nil, errors.New("failed to parse any certificates from response")
	}
	for block != nil {
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, nil, errors.Wrap(err, "error parsing certificate pem response")
		}
		certs = append(certs, cert)
		block, rest = pem.Decode(rest)
	}

	return certs[0], certs[1:], nil
}

// GetAccountOrders retrieves the orders belonging to the given account.
func (c *ACMEClient) GetAccountOrders() ([]string, error) {
	if c.acc == nil {
		return nil, errors.New("acme client not configured with account")
	}
	resp, err := c.post(nil, c.acc.OrdersURL, withKid(c))
	if err != nil {
		return nil, err
	}
	if resp.StatusCode >= 400 {
		return nil, readACMEError(resp.Body)
	}

	var orders []string
	if err := readJSON(resp.Body, &orders); err != nil {
		return nil, errors.Wrapf(err, "error reading %s", c.acc.OrdersURL)
	}

	return orders, nil
}

func readACMEError(r io.ReadCloser) error {
	defer r.Close()
	b, err := io.ReadAll(r)
	if err != nil {
		return errors.Wrap(err, "error reading from body")
	}
	ae := new(acme.Error)
	err = json.Unmarshal(b, &ae)
	// If we successfully marshaled to an ACMEError then return the ACMEError.
	if err != nil || ae.Error() == "" {
		fmt.Printf("b = %s\n", b)
		// Throw up our hands.
		return errors.Errorf("%s", b)
	}
	return ae
}
