//go:build go1.18
// +build go1.18

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package exported

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"

	"github.com/Azure/azure-sdk-for-go/sdk/internal/exported"
)

// NewResponseError creates a new *ResponseError from the provided HTTP response.
// Exported as runtime.NewResponseError().
func NewResponseError(resp *http.Response) error {
	respErr := &ResponseError{
		StatusCode:  resp.StatusCode,
		RawResponse: resp,
	}

	// prefer the error code in the response header
	if ec := resp.Header.Get("x-ms-error-code"); ec != "" {
		respErr.ErrorCode = ec
		return respErr
	}

	// if we didn't get x-ms-error-code, check in the response body
	body, err := exported.Payload(resp, nil)
	if err != nil {
		return err
	}

	if len(body) > 0 {
		if code := extractErrorCodeJSON(body); code != "" {
			respErr.ErrorCode = code
		} else if code := extractErrorCodeXML(body); code != "" {
			respErr.ErrorCode = code
		}
	}

	return respErr
}

func extractErrorCodeJSON(body []byte) string {
	var rawObj map[string]interface{}
	if err := json.Unmarshal(body, &rawObj); err != nil {
		// not a JSON object
		return ""
	}

	// check if this is a wrapped error, i.e. { "error": { ... } }
	// if so then unwrap it
	if wrapped, ok := rawObj["error"]; ok {
		unwrapped, ok := wrapped.(map[string]interface{})
		if !ok {
			return ""
		}
		rawObj = unwrapped
	} else if wrapped, ok := rawObj["odata.error"]; ok {
		// check if this a wrapped odata error, i.e. { "odata.error": { ... } }
		unwrapped, ok := wrapped.(map[string]any)
		if !ok {
			return ""
		}
		rawObj = unwrapped
	}

	// now check for the error code
	code, ok := rawObj["code"]
	if !ok {
		return ""
	}
	codeStr, ok := code.(string)
	if !ok {
		return ""
	}
	return codeStr
}

func extractErrorCodeXML(body []byte) string {
	// regular expression is much easier than dealing with the XML parser
	rx := regexp.MustCompile(`<(?:\w+:)?[c|C]ode>\s*(\w+)\s*<\/(?:\w+:)?[c|C]ode>`)
	res := rx.FindStringSubmatch(string(body))
	if len(res) != 2 {
		return ""
	}
	// first submatch is the entire thing, second one is the captured error code
	return res[1]
}

// ResponseError is returned when a request is made to a service and
// the service returns a non-success HTTP status code.
// Use errors.As() to access this type in the error chain.
// Exported as azcore.ResponseError.
type ResponseError struct {
	// ErrorCode is the error code returned by the resource provider if available.
	ErrorCode string

	// StatusCode is the HTTP status code as defined in https://pkg.go.dev/net/http#pkg-constants.
	StatusCode int

	// RawResponse is the underlying HTTP response.
	RawResponse *http.Response
}

// Error implements the error interface for type ResponseError.
// Note that the message contents are not contractual and can change over time.
func (e *ResponseError) Error() string {
	// write the request method and URL with response status code
	msg := &bytes.Buffer{}
	fmt.Fprintf(msg, "%s %s://%s%s\n", e.RawResponse.Request.Method, e.RawResponse.Request.URL.Scheme, e.RawResponse.Request.URL.Host, e.RawResponse.Request.URL.Path)
	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
	fmt.Fprintf(msg, "RESPONSE %d: %s\n", e.RawResponse.StatusCode, e.RawResponse.Status)
	if e.ErrorCode != "" {
		fmt.Fprintf(msg, "ERROR CODE: %s\n", e.ErrorCode)
	} else {
		fmt.Fprintln(msg, "ERROR CODE UNAVAILABLE")
	}
	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
	body, err := exported.Payload(e.RawResponse, nil)
	if err != nil {
		// this really shouldn't fail at this point as the response
		// body is already cached (it was read in NewResponseError)
		fmt.Fprintf(msg, "Error reading response body: %v", err)
	} else if len(body) > 0 {
		if err := json.Indent(msg, body, "", "  "); err != nil {
			// failed to pretty-print so just dump it verbatim
			fmt.Fprint(msg, string(body))
		}
		// the standard library doesn't have a pretty-printer for XML
		fmt.Fprintln(msg)
	} else {
		fmt.Fprintln(msg, "Response contained no body")
	}
	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")

	return msg.String()
}
