// Copyright (c) 2016, 2018, 2026, Oracle and/or its affiliates.  All rights reserved.
// This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.

package common

import (
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"strings"
	"syscall"

	"github.com/sony/gobreaker/v2"
)

// ServiceError models all potential errors generated the service call
type ServiceError interface {
	// The http status code of the error
	GetHTTPStatusCode() int

	// The human-readable error string as sent by the service
	GetMessage() string

	// A short error code that defines the error, meant for programmatic parsing.
	// See https://docs.oracle.com/iaas/Content/API/References/apierrors.htm
	GetCode() string

	// Unique Oracle-assigned identifier for the request.
	// If you need to contact Oracle about a particular request, please provide the request ID.
	GetOpcRequestID() string
}

// ServiceErrorRichInfo models all potential errors generated the service call and contains rich info for debugging purpose
type ServiceErrorRichInfo interface {
	ServiceError
	// The service this service call is sending to
	GetTargetService() string

	// The API name this service call is sending to
	GetOperationName() string

	// The timestamp when this request is made
	GetTimestamp() SDKTime

	// The endpoint and the Http method of this service call
	GetRequestTarget() string

	// The client version, in this case the oci go sdk version
	GetClientVersion() string

	// The API reference doc link for this API, optional and maybe empty
	GetOperationReferenceLink() string

	// Troubleshooting doc link
	GetErrorTroubleshootingLink() string
}

// ServiceErrorLocalizationMessage models all potential errors generated the service call and has localized error message info
type ServiceErrorLocalizationMessage interface {
	ServiceErrorRichInfo
	// The original error message string as sent by the service
	GetOriginalMessage() string

	// The values to be substituted into the originalMessageTemplate, expressed as a string-to-string map.
	GetMessageArgument() map[string]string

	// Template in ICU MessageFormat for the human-readable error string in English, but without the values replaced
	GetOriginalMessageTemplate() string
}

type servicefailure struct {
	StatusCode              int
	Code                    string            `json:"code,omitempty"`
	Message                 string            `json:"message,omitempty"`
	OriginalMessage         string            `json:"originalMessage"`
	OriginalMessageTemplate string            `json:"originalMessageTemplate"`
	MessageArgument         map[string]string `json:"messageArguments"`
	OpcRequestID            string            `json:"opc-request-id"`
	// debugging information
	TargetService string  `json:"target-service"`
	OperationName string  `json:"operation-name"`
	Timestamp     SDKTime `json:"timestamp"`
	RequestTarget string  `json:"request-target"`
	ClientVersion string  `json:"client-version"`

	// troubleshooting guidance
	OperationReferenceLink   string `json:"operation-reference-link"`
	ErrorTroubleshootingLink string `json:"error-troubleshooting-link"`
}

func newServiceFailureFromResponse(response *http.Response) error {
	var err error
	var timestamp SDKTime
	t, err := tryParsingTimeWithValidFormatsForHeaders([]byte(response.Header.Get("Date")), "Date")

	if err != nil {
		timestamp = *now()
	} else {
		timestamp = sdkTimeFromTime(t)
	}

	se := servicefailure{
		StatusCode:    response.StatusCode,
		Code:          "BadErrorResponse",
		OpcRequestID:  response.Header.Get("opc-request-id"),
		Timestamp:     timestamp,
		ClientVersion: defaultSDKMarker + "/" + Version(),
		RequestTarget: fmt.Sprintf("%s %s", response.Request.Method, response.Request.URL),
	}

	//If there is an error consume the body, entirely
	body, err := ioutil.ReadAll(response.Body)
	if err != nil {
		se.Message = fmt.Sprintf("The body of the response was not readable, due to :%s", err.Error())
		return se
	}

	err = json.Unmarshal(body, &se)
	if err != nil {
		Debugf("Error response could not be parsed due to: %s", err.Error())
		se.Message = fmt.Sprintf("Failed to parse json from response body due to: %s. With response body %s.", err.Error(), string(body[:]))
		return se
	}
	return se
}

// PostProcessServiceError process the service error after an error is raised and complete it with extra information
func PostProcessServiceError(err error, service string, method string, apiReferenceLink string) error {
	var serviceFailure servicefailure
	if _, ok := err.(servicefailure); !ok {
		return err
	}
	serviceFailure = err.(servicefailure)
	serviceFailure.OperationName = method
	serviceFailure.TargetService = service
	serviceFailure.ErrorTroubleshootingLink = fmt.Sprintf("https://docs.oracle.com/iaas/Content/API/References/apierrors.htm#apierrors_%v__%v_%s", serviceFailure.StatusCode, serviceFailure.StatusCode, strings.ToLower(serviceFailure.Code))
	serviceFailure.OperationReferenceLink = apiReferenceLink
	return serviceFailure
}

func (se servicefailure) Error() string {
	return fmt.Sprintf(`Error returned by %s Service. Http Status Code: %d. Error Code: %s. Opc request id: %s. Message: %s
Operation Name: %s
Timestamp: %s
Client Version: %s
Request Endpoint: %s
Troubleshooting Tips: See %s for more information about resolving this error.%s
To get more info on the failing request, you can set OCI_GO_SDK_DEBUG env var to info or higher level to log the request/response details.
If you are unable to resolve this %s issue, please contact Oracle support and provide them this full error message.`,
		se.TargetService, se.StatusCode, se.Code, se.OpcRequestID, se.Message, se.OperationName, se.Timestamp, se.ClientVersion, se.RequestTarget, se.ErrorTroubleshootingLink, se.getOperationReferenceMessage(), se.TargetService)
}

func (se servicefailure) getOperationReferenceMessage() string {
	if se.OperationReferenceLink == "" {
		return ""
	}
	return fmt.Sprintf("\nAlso see %s for details on this operation's requirements.", se.OperationReferenceLink)
}

func (se servicefailure) GetHTTPStatusCode() int {
	return se.StatusCode

}

func (se servicefailure) GetMessage() string {
	return se.Message
}

func (se servicefailure) GetOriginalMessage() string {
	return se.OriginalMessage
}

func (se servicefailure) GetOriginalMessageTemplate() string {
	return se.OriginalMessageTemplate
}

func (se servicefailure) GetMessageArgument() map[string]string {
	return se.MessageArgument
}

func (se servicefailure) GetCode() string {
	return se.Code
}

func (se servicefailure) GetOpcRequestID() string {
	return se.OpcRequestID
}

func (se servicefailure) GetTargetService() string {
	return se.TargetService
}

func (se servicefailure) GetOperationName() string {
	return se.OperationName
}

func (se servicefailure) GetTimestamp() SDKTime {
	return se.Timestamp
}

func (se servicefailure) GetRequestTarget() string {
	return se.RequestTarget
}

func (se servicefailure) GetClientVersion() string {
	return se.ClientVersion
}

func (se servicefailure) GetOperationReferenceLink() string {
	return se.OperationReferenceLink
}

func (se servicefailure) GetErrorTroubleshootingLink() string {
	return se.ErrorTroubleshootingLink
}

// IsServiceError returns false if the error is not service side, otherwise true
// additionally it returns an interface representing the ServiceError
func IsServiceError(err error) (failure ServiceError, ok bool) {
	failure, ok = err.(ServiceError)
	return
}

// IsServiceErrorRichInfo returns false if the error is not service side or is not containing rich info, otherwise true
// additionally it returns an interface representing the ServiceErrorRichInfo
func IsServiceErrorRichInfo(err error) (failure ServiceErrorRichInfo, ok bool) {
	failure, ok = err.(ServiceErrorRichInfo)
	return
}

// IsServiceErrorLocalizationMessage returns false if the error is not service side, otherwise true
// additionally it returns an interface representing the ServiceErrorOriginalMessage
func IsServiceErrorLocalizationMessage(err error) (failure ServiceErrorLocalizationMessage, ok bool) {
	failure, ok = err.(ServiceErrorLocalizationMessage)
	return
}

type deadlineExceededByBackoffError struct{}

func (deadlineExceededByBackoffError) Error() string {
	return "now() + computed backoff duration exceeds request deadline"
}

// DeadlineExceededByBackoff is the error returned by Call() when GetNextDuration() returns a time.Duration that would
// force the user to wait past the request deadline before re-issuing a request. This enables us to exit early, since
// we cannot succeed based on the configured retry policy.
var DeadlineExceededByBackoff error = deadlineExceededByBackoffError{}

// NonSeekableRequestRetryFailure is the error returned when the request is with binary request body, and is configured
// retry, but the request body is not retryable
type NonSeekableRequestRetryFailure struct {
	err error
}

func (ne NonSeekableRequestRetryFailure) Error() string {
	if ne.err == nil {
		return "Unable to perform Retry on this request body type, which did not implement seek() interface"
	}
	return fmt.Sprintf("%s. Unable to perform Retry on this request body type, which did not implement seek() interface", ne.err.Error())
}

// IsNetworkError validates if an error is a net.Error and check if it's temporary or timeout
func IsNetworkError(err error) bool {
	if err == nil {
		return false
	}

	if errors.Is(err, syscall.ECONNRESET) {
		return true
	}

	if r, ok := err.(net.Error); ok && (r.Timeout() || strings.Contains(err.Error(), "net/http: HTTP/1.x transport connection broken")) {
		return true
	}

	return false
}

// IsCircuitBreakerError validates if an error's text is Open state ErrOpenState or HalfOpen state ErrTooManyRequests
func IsCircuitBreakerError(err error) bool {
	if err == nil {
		return false
	}

	if err.Error() == gobreaker.ErrOpenState.Error() || err.Error() == gobreaker.ErrTooManyRequests.Error() {
		return true
	}
	return false
}

func getCircuitBreakerError(request *http.Request, err error, cbr *OciCircuitBreaker) error {
	cbErr := fmt.Errorf("%s, so this request was not sent to the %s service.\n\n The circuit breaker was opened because the %s service failed too many times recently. "+
		"Because the circuit breaker has been opened, requests within a %.2f second window of when the circuit breaker opened will not be sent to the %s service.\n\n"+
		"URL which circuit breaker prevented request to - %s \n Circuit Breaker Info \n Name - %s \n State - %s \n\n Errors from %s service which opened the circuit breaker:\n\n%s",
		err, cbr.Cbst.serviceName, cbr.Cbst.serviceName, cbr.Cbst.openStateWindow.Seconds(), cbr.Cbst.serviceName, request.URL.Host+request.URL.Path, cbr.Cbst.name, cbr.Cb.State().String(), cbr.Cbst.serviceName, cbr.GetHistory())
	return cbErr
}

// StatErrCode is a type which wraps error's statusCode and errorCode from service end
type StatErrCode struct {
	statusCode int
	errorCode  string
}
