package acme

import (
	"crypto/x509"
	"encoding/base64"
	"errors"
	"fmt"
	"net/http"
	"time"
)

// NewOrder initiates a new order for a new certificate. This method does not use ACME Renewal Info.
func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) {
	return c.ReplacementOrder(account, nil, identifiers)
}

// NewOrderDomains takes a list of domain dns identifiers for a new certificate. Essentially a helper function.
func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) {
	var identifiers []Identifier
	for _, d := range domains {
		identifiers = append(identifiers, Identifier{Type: "dns", Value: d})
	}
	return c.ReplacementOrder(account, nil, identifiers)
}

// ReplacementOrder takes an existing *x509.Certificate and initiates a new
// order for a new certificate, but with the order being marked as a
// replacement. Replacement orders which are valid replacements are (currently)
// exempt from Let's Encrypt NewOrder rate limits, but may not be exempt from
// other ACME CAs ACME Renewal Info implementations. At least one identifier
// must match the list of identifiers from the parent order to be considered as
// a valid replacement order.
// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, identifiers []Identifier) (Order, error) {
	// If an old cert being replaced is present and the acme directory doesn't list a RenewalInfo endpoint,
	// throw an error. This endpoint being present indicates support for ARI.
	if oldCert != nil && c.dir.RenewalInfo == "" {
		return Order{}, ErrRenewalInfoNotSupported
	}

	// 'replaces' is specifically listed as 'omitempty' so the json encoder doesn't include this key
	// if the ari oldCert is nil
	newOrderReq := struct {
		Identifiers []Identifier `json:"identifiers"`
		Replaces    string       `json:"replaces,omitempty"`
	}{
		Identifiers: identifiers,
	}

	newOrderResp := Order{}

	// If present, add the ari cert ID from the original/old certificate
	if oldCert != nil {
		replacesCertID, err := GenerateARICertID(oldCert)
		if err != nil {
			return Order{}, fmt.Errorf("acme: error generating replacement certificate id: %v", err)
		}

		newOrderReq.Replaces = replacesCertID
		newOrderResp.Replaces = replacesCertID // server does not appear to set this currently?
	}

	// Submit the order
	resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated)
	if err != nil {
		return newOrderResp, err
	}
	defer resp.Body.Close()

	newOrderResp.URL = resp.Header.Get("Location")
	return newOrderResp, nil
}

// FetchOrder fetches an existing order given an order url.
func (c Client) FetchOrder(account Account, orderURL string) (Order, error) {
	orderResp := Order{
		URL: orderURL, // boulder response doesn't seem to contain location header for this request
	}
	_, err := c.post(orderURL, account.URL, account.PrivateKey, "", &orderResp, http.StatusOK)

	return orderResp, err
}

// Helper function to determine whether an order is "finished" by its status.
func checkFinalizedOrderStatus(order Order) (bool, error) {
	switch order.Status {
	case "invalid":
		// "invalid": The certificate will not be issued.  Consider this
		//      order process abandoned.
		if order.Error.Type != "" {
			return true, order.Error
		}
		return true, errors.New("acme: finalized order is invalid, no error provided")

	case "pending":
		// "pending": The server does not believe that the client has
		//      fulfilled the requirements.  Check the "authorizations" array for
		//      entries that are still pending.
		return true, errors.New("acme: authorizations not fulfilled")

	case "ready":
		// "ready": The server agrees that the requirements have been
		//      fulfilled, and is awaiting finalization.  Submit a finalization
		//      request.
		return true, errors.New("acme: unexpected 'ready' state")

	case "processing":
		// "processing": The certificate is being issued.  Send a GET request
		//      after the time given in the "Retry-After" header field of the
		//      response, if any.
		return false, nil

	case "valid":
		// "valid": The server has issued the certificate and provisioned its
		//      URL to the "certificate" field of the order.  Download the
		//      certificate.
		return true, nil

	default:
		return true, fmt.Errorf("acme: unknown order status: %s", order.Status)
	}
}

// FinalizeOrder indicates to the acme server that the client considers an order complete and "finalizes" it.
// If the server believes the authorizations have been filled successfully, a certificate should then be available.
// This function assumes that the order status is "ready".
func (c Client) FinalizeOrder(account Account, order Order, csr *x509.CertificateRequest) (Order, error) {
	finaliseReq := struct {
		Csr string `json:"csr"`
	}{
		Csr: base64.RawURLEncoding.EncodeToString(csr.Raw),
	}

	resp, err := c.post(order.Finalize, account.URL, account.PrivateKey, finaliseReq, &order, http.StatusOK)
	if err != nil {
		return order, err
	}

	order.URL = resp.Header.Get("Location")

	updateOrder := func(resp *http.Response) (bool, error) {
		if finished, err := checkFinalizedOrderStatus(order); finished {
			return true, err
		}

		retryAfter, err := parseRetryAfter(resp.Header.Get("Retry-After"))
		if err != nil {
			return false, fmt.Errorf("acme: error parsing retry-after header: %v", err)
		}
		order.RetryAfter = retryAfter

		return false, nil
	}

	if finished, err := updateOrder(resp); finished || err != nil {
		return order, err
	}

	fetchOrder := func() (bool, error) {
		resp, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK)
		if err != nil {
			return false, nil
		}

		return updateOrder(resp)
	}

	if !c.IgnoreRetryAfter && !order.RetryAfter.IsZero() {
		_, pollTimeout := c.getPollingDurations()
		end := time.Now().Add(pollTimeout)

		for {
			if time.Now().After(end) {
				return order, errors.New("acme: finalized order timeout")
			}

			diff := time.Until(order.RetryAfter)
			_, pollTimeout := c.getPollingDurations()
			if diff > pollTimeout {
				return order, fmt.Errorf("acme: Retry-After (%v) longer than poll timeout (%v)", diff, c.PollTimeout)
			}
			if diff > 0 {
				time.Sleep(diff)
			}

			if finished, err := fetchOrder(); finished || err != nil {
				return order, err
			}
		}
	}

	if !c.IgnoreRetryAfter {
		pollInterval, pollTimeout := c.getPollingDurations()
		end := time.Now().Add(pollTimeout)
		for {
			if time.Now().After(end) {
				return order, errors.New("acme: finalized order timeout")
			}
			time.Sleep(pollInterval)

			if finished, err := fetchOrder(); finished || err != nil {
				return order, err
			}
		}
	}

	return order, err
}
