// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
// resty source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// SPDX-License-Identifier: MIT

package resty

import (
	"bytes"
	"crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"encoding/hex"
	"encoding/json"
	"encoding/xml"
	"errors"
	"fmt"
	"io"
	"log"
	"maps"
	"net/http"
	"net/url"
	"os"
	"reflect"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync/atomic"
	"time"
)

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Logger interface
//_______________________________________________________________________

// Logger abstracts Resty's internal logging, giving callers control over where
// and how log output is written. Implement this interface and register it via
// [Client.SetLogger] to supply a custom logger.
type Logger interface {
	Errorf(format string, v ...any)
	Warnf(format string, v ...any)
	Debugf(format string, v ...any)
}

func createLogger() *logger {
	l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
	return l
}

var _ Logger = (*logger)(nil)

type logger struct {
	l *log.Logger
}

func (l *logger) Errorf(format string, v ...any) {
	l.output("ERROR RESTY "+format, v...)
}

func (l *logger) Warnf(format string, v ...any) {
	l.output("WARN RESTY "+format, v...)
}

func (l *logger) Debugf(format string, v ...any) {
	l.output("DEBUG RESTY "+format, v...)
}

func (l *logger) output(format string, v ...any) {
	if len(v) == 0 {
		l.l.Print(format)
		return
	}
	l.l.Printf(format, v...)
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// In Memory JSON & XML Marshal and Unmarshal using Go package
//_____________________________________________________________

var (
	// InMemoryJSONMarshal function performs the JSON marshalling completely in memory.
	//
	//	c := resty.New()
	//	defer c.Close()
	//
	//	c.AddContentTypeEncoder("application/json", resty.InMemoryJSONMarshal)
	InMemoryJSONMarshal = func(w io.Writer, v any) error {
		jsonData, err := json.Marshal(v)
		if err != nil {
			return err
		}
		_, err = w.Write(jsonData)
		return err
	}

	// InMemoryJSONUnmarshal function performs the JSON unmarshalling completely in memory.
	//
	//	c := resty.New()
	//	defer c.Close()
	//
	//	c.AddContentTypeDecoder("application/json", resty.InMemoryJSONUnmarshal)
	InMemoryJSONUnmarshal = func(r io.Reader, v any) error {
		byteData, err := io.ReadAll(r)
		if err != nil {
			return err
		}
		return json.Unmarshal(byteData, v)
	}

	// InMemoryXMLMarshal function performs the XML marshalling completely in memory.
	//
	//	c := resty.New()
	//	defer c.Close()
	//
	//	c.AddContentTypeEncoder("application/xml", resty.InMemoryXMLMarshal)
	InMemoryXMLMarshal = func(w io.Writer, v any) error {
		xmlData, err := xml.Marshal(v)
		if err != nil {
			return err
		}
		_, err = w.Write(xmlData)
		return err
	}

	// InMemoryXMLUnmarshal function performs the XML unmarshalling completely in memory.
	//
	//	c := resty.New()
	//	defer c.Close()
	//
	//	c.AddContentTypeDecoder("application/xml", resty.InMemoryXMLUnmarshal)
	InMemoryXMLUnmarshal = func(r io.Reader, v any) error {
		byteData, err := io.ReadAll(r)
		if err != nil {
			return err
		}
		return xml.Unmarshal(byteData, v)
	}
)

// credentials holds a username and password pair used for HTTP Basic Auth.
type credentials struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

// Clone method returns a copy of the credentials.
func (c *credentials) Clone() *credentials {
	cc := new(credentials)
	*cc = *c
	return cc
}

// String method returns a masked representation of the username and password.
func (c credentials) String() string {
	return "Username: **********, Password: **********"
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Helper methods
//_______________________________________________________________________

// isStringEmpty method tells whether given string is empty or not
func isStringEmpty(str string) bool {
	return len(strings.TrimSpace(str)) == 0
}

// detectContentType method is used to figure out `Request.Body` content type for request header
func detectContentType(body any) string {
	contentType := plainTextType
	kind := inferKind(body)
	switch kind {
	case reflect.Struct, reflect.Map:
		contentType = jsonContentType
	case reflect.String:
		contentType = plainTextType
	default:
		if b, ok := body.([]byte); ok {
			contentType = http.DetectContentType(b)
		} else if kind == reflect.Slice { // check slice here to differentiate between any slice vs byte slice
			contentType = jsonContentType
		}
	}

	return contentType
}

func isJSONContentType(ct string) bool {
	return strings.Contains(ct, jsonKey)
}

func isXMLContentType(ct string) bool {
	return strings.Contains(ct, xmlKey)
}

func inferContentTypeMapKey(v string) string {
	if isJSONContentType(v) {
		return jsonKey
	} else if isXMLContentType(v) {
		return xmlKey
	}
	return ""
}

func firstNonEmpty(v ...string) string {
	for _, s := range v {
		if !isStringEmpty(s) {
			return s
		}
	}
	return ""
}

var (
	mkdirAll   = os.MkdirAll
	createFile = os.Create
	ioCopy     = io.Copy
)

func createDirectory(dir string) (err error) {
	if _, err = os.Stat(dir); err != nil {
		if os.IsNotExist(err) {
			if err = mkdirAll(dir, 0755); err != nil {
				return
			}
		}
	}
	return
}

func getPointer(v any) any {
	if v == nil {
		return nil
	}
	vv := reflect.ValueOf(v)
	if vv.Kind() == reflect.Ptr {
		return v
	}
	return reflect.New(vv.Type()).Interface()
}

func inferType(v any) reflect.Type {
	return reflect.Indirect(reflect.ValueOf(v)).Type()
}

func inferKind(v any) reflect.Kind {
	return inferType(v).Kind()
}

func newInterface(v any) any {
	if v == nil {
		return nil
	}
	return reflect.New(inferType(v)).Interface()
}

func functionName(i any) string {
	return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

func acquireBuffer() *bytes.Buffer {
	buf := bufPool.Get().(*bytes.Buffer)
	buf.Reset()
	return buf
}

func releaseBuffer(buf *bytes.Buffer) {
	if buf != nil {
		buf.Reset()
		bufPool.Put(buf)
	}
}

func backToBufPool(buf *bytes.Buffer) {
	if buf != nil {
		bufPool.Put(buf)
	}
}

func closeq(v any) {
	if c, ok := v.(io.Closer); ok {
		silently(c.Close())
	}
}

func silently(_ ...any) {}

var sanitizeHeaderToken = []string{
	"authorization",
	"auth",
	"token",
	"api-key",
	"secret",
}

func isSanitizeHeader(k string) bool {
	kk := strings.ToLower(k)
	for _, v := range sanitizeHeaderToken {
		if strings.Contains(kk, v) {
			return true
		}
	}
	return false
}

func sanitizeHeaders(hdr http.Header) http.Header {
	for k := range hdr {
		if isSanitizeHeader(k) {
			hdr[k] = []string{"*****REDACTED*****"}
		}
	}
	return hdr
}

func composeHeaders(hdr http.Header) string {
	str := make([]string, 0, len(hdr))
	headerKeys := slices.Sorted(maps.Keys(hdr))
	for _, k := range headerKeys {
		str = append(str, "\t"+strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdr[k], ", "))))
	}
	return strings.Join(str, "\n")
}

func wrapErrors(n error, inner error) error {
	if n == nil && inner == nil {
		return nil
	}
	if inner == nil {
		return n
	}
	if n == nil {
		return inner
	}
	return &restyError{
		err:   n,
		inner: inner,
	}
}

type restyError struct {
	err   error
	inner error
}

func (e *restyError) Error() string {
	return e.err.Error()
}

func (e *restyError) Unwrap() []error {
	return []error{e.err, e.inner}
}

// copied from net/http/clone.go
func cloneURLValues(v url.Values) url.Values {
	if v == nil {
		return nil
	}
	// http.Header and url.Values have the same representation, so temporarily
	// treat it like http.Header, which does have a clone:
	return url.Values(http.Header(v).Clone())
}

func cloneCookie(c *http.Cookie) *http.Cookie {
	return &http.Cookie{
		Name:       c.Name,
		Value:      c.Value,
		Path:       c.Path,
		Domain:     c.Domain,
		Expires:    c.Expires,
		RawExpires: c.RawExpires,
		MaxAge:     c.MaxAge,
		Secure:     c.Secure,
		HttpOnly:   c.HttpOnly,
		SameSite:   c.SameSite,
		Raw:        c.Raw,
		Unparsed:   c.Unparsed,
	}
}

type invalidRequestError struct {
	Err error
}

func (ire *invalidRequestError) Error() string {
	return ire.Err.Error()
}

func drainBody(res *Response) {
	if res != nil && res.Body != nil {
		drainReadCloser(res.Body)
	}
}

func drainReadCloser(body io.ReadCloser) {
	if body != nil {
		defer closeq(body)
		_, _ = io.Copy(io.Discard, body)
	}
}

func toJSON(v any) string {
	buf := acquireBuffer()
	defer releaseBuffer(buf)
	_ = encodeJSON(buf, v)
	return buf.String()
}

// formatAnyToString converts various types of values to their string representation
// based on predefined formatting rules.
func formatAnyToString(value any) string {
	switch v := value.(type) {

	// Tier 1: most common URL types
	case string:
		return v
	case int:
		return strconv.Itoa(v)
	case bool:
		return strconv.FormatBool(v)
	case int64:
		return strconv.FormatInt(v, 10)
	case []string:
		return strings.Join(v, ",")

	// Tier 2: common stdlib types
	case time.Time:
		return v.Format(time.RFC3339)
	case []byte:
		return string(v)
	case float64:
		return strconv.FormatFloat(v, 'f', -1, 64)

	// Tier 3: less common integers (signed)
	case int32:
		return strconv.FormatInt(int64(v), 10)
	case int16:
		return strconv.FormatInt(int64(v), 10)
	case int8:
		return strconv.FormatInt(int64(v), 10)

	// Tier 4: less common integers (unsigned)
	case uint64:
		return strconv.FormatUint(v, 10)
	case uint32:
		return strconv.FormatUint(uint64(v), 10)
	case uint16:
		return strconv.FormatUint(uint64(v), 10)
	case uint8:
		return strconv.FormatUint(uint64(v), 10)
	case uint:
		return strconv.FormatUint(uint64(v), 10)

	// Tier 5: rare types and fallbacks
	case float32:
		return strconv.FormatFloat(float64(v), 'f', -1, 32)
	case fmt.Stringer:
		return v.String()
	default:
		return fmt.Sprint(v)
	}
}

//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// GUID generation
// Code inspired from mgo/bson ObjectId
// Code obtained from https://github.com/go-aah/aah/blob/edge/essentials/guid.go
//___________________________________

var (
	// guidCounter is atomically incremented on each call to newGUID
	// to provide the counter component of the generated ID.
	guidCounter = readRandomUint32()

	// machineID is a 3-byte identifier derived from the hostname hash,
	// generated once at startup and reused by newGUID.
	machineID = readMachineID()

	// processID is the current process ID.
	processID = os.Getpid()
)

// newGUID returns a new globally unique identifier (GUID) encoded as a hex string.
//
// The 12-byte ID consists of:
//   - 4 bytes: Unix timestamp (big-endian seconds)
//   - 3 bytes: machine identifier (first 3 bytes of SHA-256 of the hostname)
//   - 2 bytes: process ID (big-endian)
//   - 3 bytes: atomically incrementing counter (big-endian), seeded randomly
//
// The algorithm is based on the [MongoDB ObjectId] specification.
//
// [MongoDB ObjectId]: https://www.mongodb.com/docs/manual/reference/method/ObjectId/
func newGUID() string {
	var b [12]byte
	// Timestamp, 4 bytes, big endian
	binary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))

	// Machine, first 3 bytes of sha256.Sum256([]byte(hostname))
	b[4], b[5], b[6] = machineID[0], machineID[1], machineID[2]

	// Pid, 2 bytes, specs don't specify endianness, but we use big endian.
	b[7], b[8] = byte(processID>>8), byte(processID)

	// Increment, 3 bytes, big endian
	i := atomic.AddUint32(&guidCounter, 1)
	b[9], b[10], b[11] = byte(i>>16), byte(i>>8), byte(i)

	return hex.EncodeToString(b[:])
}

var ioReadFull = io.ReadFull

// readRandomUint32 returns a cryptographically random uint32 used to seed guidCounter.
func readRandomUint32() uint32 {
	var b [4]byte
	if _, err := ioReadFull(rand.Reader, b[:]); err == nil {
		return (uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
	}

	// To initialize package unexported variable 'guidCounter'.
	// This panic would happen at program startup, so no worries at runtime panic.
	panic(errors.New("resty - guid: unable to generate random object id"))
}

var osHostname = os.Hostname

// readMachineID generates and returns a 3-byte machine identifier.
// It derives the ID from the SHA-256 hash of the hostname, falling back to
// random bytes. Panics at startup if neither source is available.
func readMachineID() []byte {
	const idSize = 3
	id := make([]byte, idSize)

	if hostname, err := osHostname(); err == nil {
		hash := sha256.Sum256([]byte(hostname))
		copy(id, hash[:idSize])
		return id
	}

	if _, err := ioReadFull(rand.Reader, id); err == nil {
		return id
	}

	// To initialize package unexported variable 'machineID'.
	// This panic would happen at program startup, so no worries at runtime panic.
	panic(errors.New("resty - guid: unable to get hostname and random bytes"))
}
