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

package azidentity

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"strings"

	"github.com/Azure/azure-sdk-for-go/sdk/azcore/policy"
	"github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"
	"github.com/Azure/azure-sdk-for-go/sdk/internal/errorinfo"
	msal "github.com/AzureAD/microsoft-authentication-library-for-go/apps/errors"
)

// tsgAnchors maps credential type names to sections of the troubleshooting
// guide at https://aka.ms/azsdk/go/identity/troubleshoot
var tsgAnchors = map[string]string{
	credNameAzureCLI:          "azure-cli",
	credNameAzureDeveloperCLI: "azd",
	credNameAzurePipelines:    "apc",
	credNameAzurePowerShell:   "azure-pwsh",
	credNameCert:              "client-cert",
	credNameManagedIdentity:   "managed-id",
	credNameSecret:            "client-secret",
	credNameWorkloadIdentity:  "workload",
}

// getResponseFromError retrieves the response carried by
// an AuthenticationFailedError or MSAL CallErr, if any
func getResponseFromError(err error) *http.Response {
	var a *AuthenticationFailedError
	var c msal.CallErr
	var res *http.Response
	if errors.As(err, &c) {
		res = c.Resp
	} else if errors.As(err, &a) {
		res = a.RawResponse
	}
	return res
}

// AuthenticationFailedError indicates an authentication request has failed.
type AuthenticationFailedError struct {
	// RawResponse is the HTTP response motivating the error, if available.
	RawResponse *http.Response

	credType, message string
	omitResponse      bool
}

func newAuthenticationFailedError(credType, message string, resp *http.Response) error {
	return &AuthenticationFailedError{credType: credType, message: message, RawResponse: resp}
}

// newAuthenticationFailedErrorFromMSAL creates an AuthenticationFailedError from an MSAL error.
// If the error is an MSAL CallErr, the new error includes an HTTP response and not the MSAL error
// message, because that message is redundant given the response. If the original error isn't a
// CallErr, the returned error incorporates its message.
func newAuthenticationFailedErrorFromMSAL(credType string, err error) error {
	msg := ""
	res := getResponseFromError(err)
	if res == nil {
		msg = err.Error()
	}
	return newAuthenticationFailedError(credType, msg, res)
}

// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *AuthenticationFailedError) Error() string {
	link := ""
	if anchor, ok := tsgAnchors[e.credType]; ok {
		link = "To troubleshoot, visit https://aka.ms/azsdk/go/identity/troubleshoot#" + anchor
	}
	if e.RawResponse == nil || e.omitResponse {
		if link != "" {
			prefix := " "
			if !strings.HasSuffix(e.message, ".") {
				prefix = ". "
			}
			link = prefix + link
		}
		return e.credType + ": " + e.message + link
	}
	msg := &bytes.Buffer{}
	fmt.Fprintf(msg, "%s authentication failed. %s\n", e.credType, e.message)
	if e.RawResponse.Request != nil {
		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)
	} else {
		// this happens when the response is created from a custom HTTP transporter,
		// which doesn't guarantee to bind the original request to the response
		fmt.Fprintln(msg, "Request information not available")
	}
	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
	fmt.Fprintf(msg, "RESPONSE %d: %s\n", e.RawResponse.StatusCode, e.RawResponse.Status)
	fmt.Fprintln(msg, "--------------------------------------------------------------------------------")
	body, err := runtime.Payload(e.RawResponse)
	switch {
	case err != nil:
		fmt.Fprintf(msg, "Error reading response body: %v", err)
	case 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))
		}
	default:
		fmt.Fprint(msg, "Response contained no body")
	}
	if link != "" {
		link = "\n" + link
	}
	fmt.Fprint(msg, "\n--------------------------------------------------------------------------------"+link)
	return msg.String()
}

// NonRetriable indicates the request which provoked this error shouldn't be retried.
func (*AuthenticationFailedError) NonRetriable() {
	// marker method
}

var _ errorinfo.NonRetriable = (*AuthenticationFailedError)(nil)

// AuthenticationRequiredError indicates a credential's Authenticate method must be called to acquire a token
// because the credential requires user interaction and is configured not to request it automatically.
type AuthenticationRequiredError struct {
	credentialUnavailableError

	// TokenRequestOptions for the required token. Pass this to the credential's Authenticate method.
	TokenRequestOptions policy.TokenRequestOptions
}

func newAuthenticationRequiredError(credType string, tro policy.TokenRequestOptions) error {
	return &AuthenticationRequiredError{
		credentialUnavailableError: credentialUnavailableError{
			credType + " can't acquire a token without user interaction. Call Authenticate to authenticate a user interactively",
		},
		TokenRequestOptions: tro,
	}
}

var (
	_ credentialUnavailable  = (*AuthenticationRequiredError)(nil)
	_ errorinfo.NonRetriable = (*AuthenticationRequiredError)(nil)
)

type credentialUnavailable interface {
	error
	credentialUnavailable()
}

type credentialUnavailableError struct {
	message string
}

// newCredentialUnavailableError is an internal helper that ensures consistent error message formatting
func newCredentialUnavailableError(credType, message string) error {
	msg := fmt.Sprintf("%s: %s", credType, message)
	return &credentialUnavailableError{msg}
}

// NewCredentialUnavailableError constructs an error indicating a credential can't attempt authentication
// because it lacks required data or state. When [ChainedTokenCredential] receives this error it will try
// its next credential, if any.
func NewCredentialUnavailableError(message string) error {
	return &credentialUnavailableError{message}
}

// Error implements the error interface. Note that the message contents are not contractual and can change over time.
func (e *credentialUnavailableError) Error() string {
	return e.message
}

// NonRetriable is a marker method indicating this error should not be retried. It has no implementation.
func (*credentialUnavailableError) NonRetriable() {}

func (*credentialUnavailableError) credentialUnavailable() {}

var (
	_ credentialUnavailable  = (*credentialUnavailableError)(nil)
	_ errorinfo.NonRetriable = (*credentialUnavailableError)(nil)
)
