package openfga

import (
	"fmt"
	"net/http"
	"strconv"
	"time"

	"github.com/openfga/go-sdk/internal/utils/retryutils"
)

// GenericOpenAPIError Provides access to the body, error and model on returned errors.
type GenericOpenAPIError struct {
	body  []byte
	error string
	model interface{}
}

// Error returns non-empty string if there was an error.
func (e GenericOpenAPIError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e GenericOpenAPIError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e GenericOpenAPIError) Model() interface{} {
	return e.model
}

// FgaApiAuthenticationError is raised when API has errors due to invalid authentication
type FgaApiAuthenticationError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	requestHost        string
	endpointCategory   string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       string
}

// Error returns non-empty string if there was an error.
func (e FgaApiAuthenticationError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiAuthenticationError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiAuthenticationError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiAuthenticationError) StoreId() string {
	return e.storeId
}

// RequestHost returns the base host of the call to the API
func (e FgaApiAuthenticationError) RequestHost() string {
	return e.requestHost
}

// EndpointCategory returns the original API category
func (e FgaApiAuthenticationError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiAuthenticationError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response status HTTP code
func (e FgaApiAuthenticationError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiAuthenticationError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiAuthenticationError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiAuthenticationError) ResponseCode() string {
	return e.responseCode
}

func NewFgaApiAuthenticationError(
	operationName string,
	_ interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiAuthenticationError {
	err := FgaApiAuthenticationError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s auth error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	return err
}

// FgaApiError will be returned if there are errors in the API

type FgaApiError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	endpointCategory   string
	requestBody        interface{}
	requestMethod      string
	requestHost        string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       string
}

// Error returns non-empty string if there was an error.
func (e FgaApiError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiError) StoreId() string {
	return e.storeId
}

// RequestBody returns the original request body
func (e FgaApiError) RequestBody() interface{} {
	return e.requestBody
}

// RequestMethod returns the method calling the API
func (e FgaApiError) RequestMethod() string {
	return e.requestMethod
}

// RequestHost returns the base host of the call to the API
func (e FgaApiError) RequestHost() string {
	return e.requestHost
}

// EndpointCategory returns the original API category
func (e FgaApiError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response HTTP status code
func (e FgaApiError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiError) ResponseCode() string {
	return e.responseCode
}

func NewFgaApiError(
	operationName string,
	requestBody interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiError {
	err := FgaApiError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestBody:        requestBody,
		requestMethod:      httpResponse.Request.Method,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	return err
}

// FgaApiValidationError will be returned if there are errors in the API's parameters

type FgaApiValidationError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	endpointCategory   string
	requestBody        interface{}
	requestMethod      string
	requestHost        string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       ErrorCode
}

// Error returns non-empty string if there was an error.
func (e FgaApiValidationError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiValidationError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiValidationError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiValidationError) StoreId() string {
	return e.storeId
}

// RequestBody returns the original request body
func (e FgaApiValidationError) RequestBody() interface{} {
	return e.requestBody
}

// RequestMethod returns the method calling the API
func (e FgaApiValidationError) RequestMethod() string {
	return e.requestMethod
}

// RequestHost returns the base host of the call to the API
func (e FgaApiValidationError) RequestHost() string {
	return e.requestHost
}

// EndpointCategory returns the original API category
func (e FgaApiValidationError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiValidationError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response HTTP status code
func (e FgaApiValidationError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiValidationError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiValidationError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiValidationError) ResponseCode() ErrorCode {
	return e.responseCode
}

func NewFgaApiValidationError(
	operationName string,
	requestBody interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiValidationError {
	err := FgaApiValidationError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestBody:        requestBody,
		requestMethod:      httpResponse.Request.Method,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s validation error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	return err
}

// FgaApiNotFoundError will be returned if the endpoint cannot be found

type FgaApiNotFoundError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	endpointCategory   string
	requestBody        interface{}
	requestMethod      string
	requestHost        string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       NotFoundErrorCode
}

// Error returns non-empty string if there was an error.
func (e FgaApiNotFoundError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiNotFoundError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiNotFoundError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiNotFoundError) StoreId() string {
	return e.storeId
}

// RequestBody returns the original request body
func (e FgaApiNotFoundError) RequestBody() interface{} {
	return e.requestBody
}

// RequestMethod returns the method calling the API
func (e FgaApiNotFoundError) RequestMethod() string {
	return e.requestMethod
}

// RequestHost returns the base host of the call to the API
func (e FgaApiNotFoundError) RequestHost() string {
	return e.requestHost
}

// EndpointCategory returns the original API category
func (e FgaApiNotFoundError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiNotFoundError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response HTTP status code
func (e FgaApiNotFoundError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiNotFoundError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiNotFoundError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiNotFoundError) ResponseCode() NotFoundErrorCode {
	return e.responseCode
}

func NewFgaApiNotFoundError(
	operationName string,
	requestBody interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiNotFoundError {
	err := FgaApiNotFoundError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestBody:        requestBody,
		requestMethod:      httpResponse.Request.Method,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s not found error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	return err
}

// FgaApiInternalError will be returned if there are internal errors in OpenFGA

type FgaApiInternalError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	endpointCategory   string
	requestBody        interface{}
	requestHost        string
	requestMethod      string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       InternalErrorCode

	retryAfterDurationInMs int
	retryAfterEpoc         string
}

// Error returns non-empty string if there was an error.
func (e FgaApiInternalError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiInternalError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiInternalError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiInternalError) StoreId() string {
	return e.storeId
}

// RequestBody returns the original request body
func (e FgaApiInternalError) RequestBody() interface{} {
	return e.requestBody
}

// RequestMethod returns the method calling the API
func (e FgaApiInternalError) RequestMethod() string {
	return e.requestMethod
}

// RequestHost returns the base host of the call to the API
func (e FgaApiInternalError) RequestHost() string {
	return e.requestHost
}

// RetryAfterDurationInMs returns the retry duration in milliseconds
func (e FgaApiInternalError) RetryAfterDurationInMs() int {
	return e.retryAfterDurationInMs
}

// RetryAfterEpoc returns the retry duration in RFC3339 format
func (e FgaApiInternalError) RetryAfterEpoc() string {
	return e.retryAfterEpoc
}

// EndpointCategory returns the original API category
func (e FgaApiInternalError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiInternalError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response HTTP status code
func (e FgaApiInternalError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiInternalError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiInternalError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiInternalError) ResponseCode() InternalErrorCode {
	return e.responseCode
}

// ShouldRetry returns whether this error is retryable
func (e FgaApiInternalError) ShouldRetry() bool {
	return e.responseStatusCode != http.StatusNotImplemented
}

// GetTimeToWait returns how much time is needed before we can retry
func (e FgaApiInternalError) GetTimeToWait(retryCount int, retryParams retryutils.RetryParams) time.Duration {
	if !e.ShouldRetry() {
		return time.Duration(0)
	}
	return retryutils.GetTimeToWait(retryCount, retryParams.MaxRetry, retryParams.MinWaitInMs, e.responseHeader, e.endpointCategory)
}

func NewFgaApiInternalError(
	operationName string,
	requestBody interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiInternalError {
	err := FgaApiInternalError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestBody:        requestBody,
		requestMethod:      httpResponse.Request.Method,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s internal error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	retryAfter := retryutils.ParseRetryAfterHeaderValue(httpResponse.Header, retryutils.RetryAfterHeaderName)
	if retryAfter > 0 {
		err.retryAfterDurationInMs = int(retryAfter.Milliseconds())
		err.retryAfterEpoc = time.Now().Add(retryAfter).Format(time.RFC3339)
	}

	return err
}

// FgaApiRateLimitExceededError will be returned if error happens because the API's rate limit has been exceeded (429 HTTP response)
type FgaApiRateLimitExceededError struct {
	body  []byte
	error string
	model interface{}

	storeId            string
	endpointCategory   string
	requestBody        interface{}
	requestMethod      string
	requestHost        string
	modelDecodeError   error
	responseStatusCode int
	responseHeader     http.Header
	requestId          string
	responseCode       string

	retryAfterDurationInMs int
	retryAfterEpoc         string
	rateLimit              int
	rateUnit               string
	rateLimitResetEpoch    string
}

// Error returns non-empty string if there was an error.
func (e FgaApiRateLimitExceededError) Error() string {
	return e.error
}

// Body returns the raw bytes of the response
func (e FgaApiRateLimitExceededError) Body() []byte {
	return e.body
}

// Model returns the unpacked model of the error
func (e FgaApiRateLimitExceededError) Model() interface{} {
	return e.model
}

// StoreId returns the store ID for the API that causes the error
func (e FgaApiRateLimitExceededError) StoreId() string {
	return e.storeId
}

// RequestBody returns the original request body
func (e FgaApiRateLimitExceededError) RequestBody() interface{} {
	return e.requestBody
}

// RequestMethod returns the method calling the API
func (e FgaApiRateLimitExceededError) RequestMethod() string {
	return e.requestMethod
}

// RequestHost returns the base host of the call to the API
func (e FgaApiRateLimitExceededError) RequestHost() string {
	return e.requestHost
}

// RetryAfterDurationInMs returns the retry duration in milliseconds
func (e FgaApiRateLimitExceededError) RetryAfterDurationInMs() int {
	return e.retryAfterDurationInMs
}

// RetryAfterEpoc returns the retry duration in RFC3339 format
func (e FgaApiRateLimitExceededError) RetryAfterEpoc() string {
	return e.retryAfterEpoc
}

// RateLimit returns the limit for the API
func (e FgaApiRateLimitExceededError) RateLimit() int {
	return e.rateLimit
}

// RateUnit returns the unit used for rate limit
func (e FgaApiRateLimitExceededError) RateUnit() string {
	return e.rateUnit
}

// RateLimitResetEpoch returns the unit used for rate limit
func (e FgaApiRateLimitExceededError) RateLimitResetEpoch() string {
	return e.rateLimitResetEpoch
}

// EndpointCategory returns the original API category
func (e FgaApiRateLimitExceededError) EndpointCategory() string {
	return e.endpointCategory
}

// ModelDecodeError returns any error when decoding the unpacked model of the error
func (e FgaApiRateLimitExceededError) ModelDecodeError() error {
	return e.modelDecodeError
}

// ResponseStatusCode returns the original API response HTTP status code
func (e FgaApiRateLimitExceededError) ResponseStatusCode() int {
	return e.responseStatusCode
}

// ResponseHeader returns the original API response header
func (e FgaApiRateLimitExceededError) ResponseHeader() http.Header {
	return e.responseHeader
}

// RequestId returns the FGA request ID associated with the response
func (e FgaApiRateLimitExceededError) RequestId() string {
	return e.requestId
}

// ResponseCode returns response code
func (e FgaApiRateLimitExceededError) ResponseCode() string {
	return e.responseCode
}

// ShouldRetry returns whether this error is retryable
func (e FgaApiRateLimitExceededError) ShouldRetry() bool {
	return true
}

// GetTimeToWait returns how much time is needed before we can retry
func (e FgaApiRateLimitExceededError) GetTimeToWait(retryCount int, retryParams retryutils.RetryParams) time.Duration {
	return retryutils.GetTimeToWait(retryCount, retryParams.MaxRetry, retryParams.MinWaitInMs, e.responseHeader, e.endpointCategory)
}

func NewFgaApiRateLimitExceededError(
	operationName string,
	requestBody interface{},
	httpResponse *http.Response,
	responseBody []byte,
	storeId string,
) FgaApiRateLimitExceededError {
	err := FgaApiRateLimitExceededError{
		body:               responseBody,
		storeId:            storeId,
		endpointCategory:   operationName,
		requestBody:        requestBody,
		requestMethod:      httpResponse.Request.Method,
		requestHost:        httpResponse.Request.URL.Host,
		responseStatusCode: httpResponse.StatusCode,
		responseHeader:     httpResponse.Header,
		error:              fmt.Sprintf("%[1]s rate limit error for %[2]s %[1]s with body %[3]s", httpResponse.Request.Method, operationName, string(responseBody)),
	}

	requestID := httpResponse.Header.Get("Fga-Request-Id")
	if requestID != "" {
		err.requestId = requestID
	} else {
		requestID = httpResponse.Header.Get("X-Request-Id")
		if requestID != "" {
			err.requestId = requestID
		}
	}

	retryAfter := retryutils.ParseRetryAfterHeaderValue(httpResponse.Header, retryutils.RetryAfterHeaderName)
	if retryAfter > 0 {
		err.retryAfterDurationInMs = int(retryAfter.Milliseconds())
		err.retryAfterEpoc = time.Now().Add(retryAfter).Format(time.RFC3339)
	}

	rateLimitHeader := httpResponse.Header.Get("X-RateLimit-Limit")
	if rateLimitHeader != "" {
		err.rateLimit, _ = strconv.Atoi(rateLimitHeader)
	}
	rateUnitHeader := httpResponse.Header.Get("X-RateLimit-Unit")
	if rateUnitHeader != "" {
		err.rateUnit = rateUnitHeader
	}
	rateLimitResetHeader := httpResponse.Header.Get(retryutils.RateLimitResetHeaderName)
	if rateLimitResetHeader != "" {
		err.rateLimitResetEpoch = rateLimitResetHeader
	} else {
		rateLimitResetHeader = httpResponse.Header.Get(retryutils.RateLimitResetAltHeaderName)
		if rateLimitResetHeader != "" {
			err.rateLimitResetEpoch = rateLimitResetHeader
		}
	}

	return err
}
