// 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 (
	"fmt"

	"go.uber.org/zap/zapcore"
)

// Problem carries the details of an error from HTTP APIs as
// defined in RFC 7807: https://tools.ietf.org/html/rfc7807
// and as extended by RFC 8555 §6.7:
// https://tools.ietf.org/html/rfc8555#section-6.7
type Problem struct {
	// "type" (string) - A URI reference [RFC3986] that identifies the
	// problem type.  This specification encourages that, when
	// dereferenced, it provide human-readable documentation for the
	// problem type (e.g., using HTML [W3C.REC-html5-20141028]).  When
	// this member is not present, its value is assumed to be
	// "about:blank". §3.1
	Type string `json:"type"`

	// "title" (string) - A short, human-readable summary of the problem
	// type.  It SHOULD NOT change from occurrence to occurrence of the
	// problem, except for purposes of localization (e.g., using
	// proactive content negotiation; see [RFC7231], Section 3.4). §3.1
	Title string `json:"title,omitempty"`

	// "status" (number) - The HTTP status code ([RFC7231], Section 6)
	// generated by the origin server for this occurrence of the problem.
	// §3.1
	Status int `json:"status,omitempty"`

	// "detail" (string) - A human-readable explanation specific to this
	// occurrence of the problem. §3.1
	//
	// RFC 8555 §6.7: "Clients SHOULD display the 'detail' field of all
	// errors."
	Detail string `json:"detail,omitempty"`

	// "instance" (string) - A URI reference that identifies the specific
	// occurrence of the problem.  It may or may not yield further
	// information if dereferenced. §3.1
	Instance string `json:"instance,omitempty"`

	// "Sometimes a CA may need to return multiple errors in response to a
	// request.  Additionally, the CA may need to attribute errors to
	// specific identifiers.  For instance, a newOrder request may contain
	// multiple identifiers for which the CA cannot issue certificates.  In
	// this situation, an ACME problem document MAY contain the
	// 'subproblems' field, containing a JSON array of problem documents."
	// RFC 8555 §6.7.1
	Subproblems []Subproblem `json:"subproblems,omitempty"`

	// For convenience, we've added this field to associate with a value
	// that is related to or caused the problem. It is not part of the
	// spec, but, if a challenge fails for example, we can associate the
	// error with the problematic authz object by setting this field.
	// Challenge failures will have this set to an Authorization type.
	Resource any `json:"-"`
}

func (p Problem) Error() string {
	// TODO: 7.3.3: Handle changes to Terms of Service (notice it uses the Instance field and Link header)

	// RFC 8555 §6.7: "Clients SHOULD display the 'detail' field of all errors."
	s := fmt.Sprintf("HTTP %d %s - %s", p.Status, p.Type, p.Detail)
	if len(p.Subproblems) > 0 {
		for _, v := range p.Subproblems {
			s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail)
			if v.Identifier.Type != "" || v.Identifier.Value != "" {
				s += fmt.Sprintf(" (%s_identifier=%s)", v.Identifier.Type, v.Identifier.Value)
			}
		}
	}
	if p.Instance != "" {
		s += ", url: " + p.Instance
	}
	return s
}

// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
// This allows problems to be serialized by the zap logger.
func (p Problem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	enc.AddString("type", p.Type)
	enc.AddString("title", p.Title)
	enc.AddString("detail", p.Detail)
	enc.AddString("instance", p.Instance)
	enc.AddArray("subproblems", loggableSubproblems(p.Subproblems))
	return nil
}

// Subproblem describes a more specific error in a problem according to
// RFC 8555 §6.7.1: "An ACME problem document MAY contain the
// 'subproblems' field, containing a JSON array of problem documents,
// each of which MAY contain an 'identifier' field."
type Subproblem struct {
	Problem

	// "If present, the 'identifier' field MUST contain an ACME
	// identifier (Section 9.7.7)." §6.7.1
	Identifier Identifier `json:"identifier,omitempty"`
}

// MarshalLogObject satisfies the zapcore.ObjectMarshaler interface.
// This allows subproblems to be serialized by the zap logger.
func (sp Subproblem) MarshalLogObject(enc zapcore.ObjectEncoder) error {
	enc.AddString("identifier_type", sp.Identifier.Type)
	enc.AddString("identifier", sp.Identifier.Value)
	enc.AddObject("subproblem", sp.Problem)
	return nil
}

type loggableSubproblems []Subproblem

// MarshalLogArray satisfies the zapcore.ArrayMarshaler interface.
// This allows a list of subproblems to be serialized by the zap logger.
func (ls loggableSubproblems) MarshalLogArray(enc zapcore.ArrayEncoder) error {
	for _, sp := range ls {
		enc.AppendObject(sp)
	}
	return nil
}

// Standard token values for the "type" field of problems, as defined
// in RFC 8555 §6.7: https://tools.ietf.org/html/rfc8555#section-6.7
//
// "To facilitate automatic response to errors, this document defines the
// following standard tokens for use in the 'type' field (within the
// ACME URN namespace 'urn:ietf:params:acme:error:') ... This list is not
// exhaustive.  The server MAY return errors whose 'type' field is set to
// a URI other than those defined above."
const (
	// The ACME error URN prefix.
	ProblemTypeNamespace = "urn:ietf:params:acme:error:"

	ProblemTypeAccountDoesNotExist     = ProblemTypeNamespace + "accountDoesNotExist"
	ProblemTypeAlreadyRevoked          = ProblemTypeNamespace + "alreadyRevoked"
	ProblemTypeBadCSR                  = ProblemTypeNamespace + "badCSR"
	ProblemTypeBadNonce                = ProblemTypeNamespace + "badNonce"
	ProblemTypeBadPublicKey            = ProblemTypeNamespace + "badPublicKey"
	ProblemTypeBadRevocationReason     = ProblemTypeNamespace + "badRevocationReason"
	ProblemTypeBadSignatureAlgorithm   = ProblemTypeNamespace + "badSignatureAlgorithm"
	ProblemTypeCAA                     = ProblemTypeNamespace + "caa"
	ProblemTypeCompound                = ProblemTypeNamespace + "compound"
	ProblemTypeConnection              = ProblemTypeNamespace + "connection"
	ProblemTypeDNS                     = ProblemTypeNamespace + "dns"
	ProblemTypeExternalAccountRequired = ProblemTypeNamespace + "externalAccountRequired"
	ProblemTypeIncorrectResponse       = ProblemTypeNamespace + "incorrectResponse"
	ProblemTypeInvalidContact          = ProblemTypeNamespace + "invalidContact"
	ProblemTypeMalformed               = ProblemTypeNamespace + "malformed"
	ProblemTypeOrderNotReady           = ProblemTypeNamespace + "orderNotReady"
	ProblemTypeRateLimited             = ProblemTypeNamespace + "rateLimited"
	ProblemTypeRejectedIdentifier      = ProblemTypeNamespace + "rejectedIdentifier"
	ProblemTypeServerInternal          = ProblemTypeNamespace + "serverInternal"
	ProblemTypeTLS                     = ProblemTypeNamespace + "tls"
	ProblemTypeUnauthorized            = ProblemTypeNamespace + "unauthorized"
	ProblemTypeUnsupportedContact      = ProblemTypeNamespace + "unsupportedContact"
	ProblemTypeUnsupportedIdentifier   = ProblemTypeNamespace + "unsupportedIdentifier"
	ProblemTypeUserActionRequired      = ProblemTypeNamespace + "userActionRequired"
)
