// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package mautrix

import (
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
)

// Common error codes from https://matrix.org/docs/spec/client_server/latest#api-standards
//
// Can be used with errors.Is() to check the response code without casting the error:
//   err := client.Sync()
//   if errors.Is(err, MUnknownToken) {
//     // logout
//   }
var (
	// Forbidden access, e.g. joining a room without permission, failed login.
	MForbidden = RespError{ErrCode: "M_FORBIDDEN"}
	// The access token specified was not recognised.
	MUnknownToken = RespError{ErrCode: "M_UNKNOWN_TOKEN"}
	// No access token was specified for the request.
	MMissingToken = RespError{ErrCode: "M_MISSING_TOKEN"}
	// Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys.
	MBadJSON = RespError{ErrCode: "M_BAD_JSON"}
	// Request did not contain valid JSON.
	MNotJSON = RespError{ErrCode: "M_NOT_JSON"}
	// No resource was found for this request.
	MNotFound = RespError{ErrCode: "M_NOT_FOUND"}
	// Too many requests have been sent in a short period of time. Wait a while then try again.
	MLimitExceeded = RespError{ErrCode: "M_LIMIT_EXCEEDED"}
	// The user ID associated with the request has been deactivated.
	// Typically for endpoints that prove authentication, such as /login.
	MUserDeactivated = RespError{ErrCode: "M_USER_DEACTIVATED"}
	// Encountered when trying to register a user ID which has been taken.
	MUserInUse = RespError{ErrCode: "M_USER_IN_USE"}
	// Encountered when trying to register a user ID which is not valid.
	MInvalidUsername = RespError{ErrCode: "M_INVALID_USERNAME"}
	// Sent when the room alias given to the createRoom API is already in use.
	MRoomInUse = RespError{ErrCode: "M_ROOM_IN_USE"}
	// The state change requested cannot be performed, such as attempting to unban a user who is not banned.
	MBadState = RespError{ErrCode: "M_BAD_STATE"}
	// 	The request or entity was too large.
	MTooLarge = RespError{ErrCode: "M_TOO_LARGE"}
	// The resource being requested is reserved by an application service, or the application service making the request has not created the resource.
	MExclusive = RespError{ErrCode: "M_EXCLUSIVE"}
	// The client's request to create a room used a room version that the server does not support.
	MUnsupportedRoomVersion = RespError{ErrCode: "M_UNSUPPORTED_ROOM_VERSION"}
	// The client attempted to join a room that has a version the server does not support.
	// Inspect the room_version property of the error response for the room's version.
	MIncompatibleRoomVersion = RespError{ErrCode: "M_INCOMPATIBLE_ROOM_VERSION"}
)

// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
	Request      *http.Request
	Response     *http.Response
	ResponseBody string

	WrappedError error
	RespError    *RespError
	Message      string
}

func (e HTTPError) Is(err error) bool {
	return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err))
}

func (e HTTPError) IsStatus(code int) bool {
	return e.Response != nil && e.Response.StatusCode == code
}

func (e HTTPError) Error() string {
	if e.WrappedError != nil {
		return fmt.Sprintf("%s: %v", e.Message, e.WrappedError)
	} else if e.RespError != nil {
		return fmt.Sprintf("failed to %s %s: %s (HTTP %d): %s", e.Request.Method, e.Request.URL.Path,
			e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err)
	} else {
		msg := fmt.Sprintf("failed to %s %s: %s", e.Request.Method, e.Request.URL.Path, e.Response.Status)
		if len(e.ResponseBody) > 0 {
			msg = fmt.Sprintf("%s\n%s", msg, e.ResponseBody)
		}
		return msg
	}
}

func (e HTTPError) Unwrap() error {
	if e.WrappedError != nil {
		return e.WrappedError
	} else if e.RespError != nil {
		return *e.RespError
	}
	return nil
}

// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See https://spec.matrix.org/v1.2/client-server-api/#api-standards
type RespError struct {
	ErrCode   string
	Err       string
	ExtraData map[string]interface{}
}

func (e *RespError) UnmarshalJSON(data []byte) error {
	err := json.Unmarshal(data, &e.ExtraData)
	if err != nil {
		return err
	}
	e.ErrCode, _ = e.ExtraData["errcode"].(string)
	e.Err, _ = e.ExtraData["error"].(string)
	return nil
}

func (e *RespError) MarshalJSON() ([]byte, error) {
	if e.ExtraData == nil {
		e.ExtraData = make(map[string]interface{})
	}
	e.ExtraData["errcode"] = e.ErrCode
	e.ExtraData["error"] = e.Err
	return json.Marshal(&e.ExtraData)
}

// Error returns the errcode and error message.
func (e RespError) Error() string {
	return e.ErrCode + ": " + e.Err
}

func (e RespError) Is(err error) bool {
	e2, ok := err.(RespError)
	if !ok {
		return false
	}
	if e.ErrCode == "M_UNKNOWN" && e2.ErrCode == "M_UNKNOWN" {
		return e.Err == e2.Err
	}
	return e2.ErrCode == e.ErrCode
}
