// Copyright 2020 Matthew Holt
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package acme

import (
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"time"
)

// Order is an object that "represents a client's request for a certificate
// and is used to track the progress of that order through to issuance.
// Thus, the object contains information about the requested
// certificate, the authorizations that the server requires the client
// to complete, and any certificates that have resulted from this order."
// §7.1.3
type Order struct {
	// status (required, string):  The status of this order.  Possible
	// values are "pending", "ready", "processing", "valid", and
	// "invalid".  See Section 7.1.6.
	Status string `json:"status"`

	// expires (optional, string):  The timestamp after which the server
	// will consider this order invalid, encoded in the format specified
	// in [RFC3339].  This field is REQUIRED for objects with "pending"
	// or "valid" in the status field.
	Expires time.Time `json:"expires,omitempty"`

	// identifiers (required, array of object):  An array of identifier
	// objects that the order pertains to.
	Identifiers []Identifier `json:"identifiers"`

	// notBefore (optional, string):  The requested value of the notBefore
	// field in the certificate, in the date format defined in [RFC3339].
	NotBefore *time.Time `json:"notBefore,omitempty"`

	// notAfter (optional, string):  The requested value of the notAfter
	// field in the certificate, in the date format defined in [RFC3339].
	NotAfter *time.Time `json:"notAfter,omitempty"`

	// error (optional, object):  The error that occurred while processing
	// the order, if any.  This field is structured as a problem document
	// [RFC7807].
	Error *Problem `json:"error,omitempty"`

	// authorizations (required, array of string):  For pending orders, the
	// authorizations that the client needs to complete before the
	// requested certificate can be issued (see Section 7.5), including
	// unexpired authorizations that the client has completed in the past
	// for identifiers specified in the order.  The authorizations
	// required are dictated by server policy; there may not be a 1:1
	// relationship between the order identifiers and the authorizations
	// required.  For final orders (in the "valid" or "invalid" state),
	// the authorizations that were completed.  Each entry is a URL from
	// which an authorization can be fetched with a POST-as-GET request.
	Authorizations []string `json:"authorizations"`

	// finalize (required, string):  A URL that a CSR must be POSTed to once
	// all of the order's authorizations are satisfied to finalize the
	// order.  The result of a successful finalization will be the
	// population of the certificate URL for the order.
	Finalize string `json:"finalize"`

	// certificate (optional, string):  A URL for the certificate that has
	// been issued in response to this order.
	Certificate string `json:"certificate"`

	// Similar to new-account, the server returns a 201 response with
	// the URL to the order object in the Location header.
	//
	// We transfer the value from the header to this field for
	// storage and recall purposes.
	Location string `json:"-"`
}

// Identifier is used in order and authorization (authz) objects.
type Identifier struct {
	// type (required, string):  The type of identifier.  This document
	// defines the "dns" identifier type.  See the registry defined in
	// Section 9.7.7 for any others.
	Type string `json:"type"`

	// value (required, string):  The identifier itself.
	Value string `json:"value"`
}

// NewOrder creates a new order with the server.
//
// "The client begins the certificate issuance process by sending a POST
// request to the server's newOrder resource." §7.4
func (c *Client) NewOrder(ctx context.Context, account Account, order Order) (Order, error) {
	if err := c.provision(ctx); err != nil {
		return order, err
	}
	resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, c.dir.NewOrder, order, &order)
	if err != nil {
		return order, err
	}
	order.Location = resp.Header.Get("Location")
	return order, nil
}

// GetOrder retrieves an order from the server. The Order's Location field must be populated.
func (c *Client) GetOrder(ctx context.Context, account Account, order Order) (Order, error) {
	if err := c.provision(ctx); err != nil {
		return order, err
	}
	_, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order)
	return order, err
}

// FinalizeOrder finalizes the order with the server and polls under the server has
// updated the order status. The CSR must be in ASN.1 DER-encoded format. If this
// succeeds, the certificate is ready to download once this returns.
//
// "Once the client believes it has fulfilled the server's requirements,
// it should send a POST request to the order resource's finalize URL." §7.4
func (c *Client) FinalizeOrder(ctx context.Context, account Account, order Order, csrASN1DER []byte) (Order, error) {
	if err := c.provision(ctx); err != nil {
		return order, err
	}

	body := struct {
		// csr (required, string):  A CSR encoding the parameters for the
		// certificate being requested [RFC2986].  The CSR is sent in the
		// base64url-encoded version of the DER format.  (Note: Because this
		// field uses base64url, and does not include headers, it is
		// different from PEM.) §7.4
		CSR string `json:"csr"`
	}{
		CSR: base64.RawURLEncoding.EncodeToString(csrASN1DER),
	}

	resp, err := c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Finalize, body, &order)
	if err != nil {
		// "A request to finalize an order will result in error if the order is
		// not in the 'ready' state.  In such cases, the server MUST return a
		// 403 (Forbidden) error with a problem document of type
		// 'orderNotReady'.  The client should then send a POST-as-GET request
		// to the order resource to obtain its current state.  The status of the
		// order will indicate what action the client should take (see below)."
		// §7.4
		var problem Problem
		if errors.As(err, &problem) {
			if problem.Type != ProblemTypeOrderNotReady {
				return order, err
			}
		} else {
			return order, err
		}
	}

	// unlike with accounts and authorizations, the spec isn't clear on whether
	// the server MUST set this on finalizing the order, but their example shows a
	// Location header, so I guess if it's set in the response, we should keep it
	if newLocation := resp.Header.Get("Location"); newLocation != "" {
		order.Location = newLocation
	}

	if finished, err := orderIsFinished(order); finished {
		return order, err
	}

	// TODO: "The elements of the "authorizations" and "identifiers" arrays are
	// immutable once set. If a client observes a change
	// in the contents of either array, then it SHOULD consider the order
	// invalid."

	maxDuration := c.pollTimeout()
	start := time.Now()
	for time.Since(start) < maxDuration {
		// querying an order is expensive on the server-side, so we
		// shouldn't do it too frequently; honor server preference
		interval, err := retryAfter(resp, c.pollInterval())
		if err != nil {
			return order, err
		}
		select {
		case <-time.After(interval):
		case <-ctx.Done():
			return order, ctx.Err()
		}

		resp, err = c.httpPostJWS(ctx, account.PrivateKey, account.Location, order.Location, nil, &order)
		if err != nil {
			return order, fmt.Errorf("polling order status: %w", err)
		}

		// (same reasoning as above)
		if newLocation := resp.Header.Get("Location"); newLocation != "" {
			order.Location = newLocation
		}

		if finished, err := orderIsFinished(order); finished {
			return order, err
		}
	}

	return order, fmt.Errorf("order took too long")
}

// orderIsFinished returns true if the order processing is complete,
// regardless of success or failure. If this function returns true,
// polling an order status should stop. If there is an error with the
// order, an error will be returned. This function should be called
// only after a request to finalize an order. See §7.4.
func orderIsFinished(order Order) (bool, error) {
	switch order.Status {
	case StatusInvalid:
		// "invalid": The certificate will not be issued.  Consider this
		//      order process abandoned.
		return true, fmt.Errorf("final order is invalid: %w", order.Error)

	case StatusPending:
		// "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, fmt.Errorf("order pending, authorizations remaining: %v", order.Authorizations)

	case StatusReady:
		// "ready": The server agrees that the requirements have been
		//      fulfilled, and is awaiting finalization.  Submit a finalization
		//      request.
		// (we did just submit a finalization request, so this is an error)
		return true, fmt.Errorf("unexpected state: %s - order already finalized", order.Status)

	case StatusProcessing:
		// "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 StatusValid:
		// "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("unrecognized order status: %s", order.Status)
	}
}
