// Package tgerr implements helpers for error handling.
package tgerr

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/go-faster/errors"

	"github.com/gotd/td/internal/ascii"
)

// Error represents RPC error returned as result to request.
type Error struct {
	Code     int    // 420
	Message  string // FLOOD_WAIT_3
	Type     string // FLOOD_WAIT
	Argument int    // 3
}

// New creates new *Error from code and message, extracting argument
// and type.
func New(code int, msg string) *Error {
	e := &Error{
		Code:    code,
		Message: msg,
	}
	e.extractArgument()
	return e
}

// IsType reports whether error has type t.
func (e *Error) IsType(t string) bool {
	if e == nil {
		return false
	}
	return e.Type == t
}

// IsCode reports whether error Code is equal to code.
func (e *Error) IsCode(code int) bool {
	if e == nil {
		return false
	}
	return e.Code == code
}

// IsOneOf returns true if error type is in tt.
func (e *Error) IsOneOf(tt ...string) bool {
	if e == nil {
		return false
	}
	for _, t := range tt {
		if e.IsType(t) {
			return true
		}
	}
	return false
}

// IsCodeOneOf returns true if error code is one of codes.
func (e *Error) IsCodeOneOf(codes ...int) bool {
	if e == nil {
		return false
	}
	for _, code := range codes {
		if e.IsCode(code) {
			return true
		}
	}
	return false
}

// extractArgument extracts Type and Argument from Message.
func (e *Error) extractArgument() {
	if e.Message == "" {
		return
	}

	// Defaulting Type to Message.
	e.Type = e.Message

	// Splitting by underscore.
	parts := strings.Split(e.Message, "_")
	if len(parts) < 2 {
		return
	}

	var nonDigit []string
Parts:
	for _, part := range parts {
		for _, r := range part {
			if ascii.IsDigit(r) {
				continue
			}

			// Found non-digit part, skipping.
			nonDigit = append(nonDigit, part)
			continue Parts
		}

		// Found digit-only part, using as argument.
		argument, err := strconv.Atoi(part)
		if err != nil {
			// Should be unreachable.
			return
		}
		e.Argument = argument
	}

	e.Type = strings.Join(nonDigit, "_")
}

func (e *Error) Error() string {
	if e.Type != e.Message {
		return fmt.Sprintf("rpc error code %d: %s (%d)", e.Code, e.Type, e.Argument)
	}
	return fmt.Sprintf("rpc error code %d: %s", e.Code, e.Message)
}

// AsType returns *Error from err if rpc error type is t.
func AsType(err error, t string) (rpcErr *Error, ok bool) {
	if errors.As(err, &rpcErr) && rpcErr.Type == t {
		return rpcErr, true
	}
	return nil, false
}

// As extracts *Error from err if possible.
func As(err error) (rpcErr *Error, ok bool) {
	if errors.As(err, &rpcErr) {
		return rpcErr, true
	}
	return nil, false
}

// Is returns true if err type is t.
func Is(err error, tt ...string) bool {
	if rpcErr, ok := As(err); ok {
		return rpcErr.IsOneOf(tt...)
	}
	return false
}

// IsCode returns true of error code is as provided.
func IsCode(err error, code ...int) bool {
	if rpcErr, ok := As(err); ok {
		return rpcErr.IsCodeOneOf(code...)
	}
	return false
}
