package logstorage

import (
	"encoding/json"
	"fmt"
	"net/http"
	"strconv"
	"strings"

	"github.com/VictoriaMetrics/VictoriaMetrics/lib/encoding"
	"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)

// TenantID is an id of a tenant for log streams.
//
// Each log stream is associated with a single TenantID.
type TenantID struct {
	// AccountID is the id of the account for the log stream.
	AccountID uint32 `json:"account_id"`

	// ProjectID is the id of the project for the log stream.
	ProjectID uint32 `json:"project_id"`
}

// Reset resets tid.
func (tid *TenantID) Reset() {
	tid.AccountID = 0
	tid.ProjectID = 0
}

// String returns human-readable representation of tid
func (tid TenantID) String() string {
	return fmt.Sprintf("{accountID=%d,projectID=%d}", tid.AccountID, tid.ProjectID)
}

// Equal returns true if tid equals to a.
func (tid *TenantID) Equal(a *TenantID) bool {
	return tid.AccountID == a.AccountID && tid.ProjectID == a.ProjectID
}

// less returns true if tid is less than a.
func (tid *TenantID) less(a *TenantID) bool {
	if tid.AccountID != a.AccountID {
		return tid.AccountID < a.AccountID
	}
	return tid.ProjectID < a.ProjectID
}

func (tid *TenantID) marshalString(dst []byte) []byte {
	n := uint64(tid.AccountID)<<32 | uint64(tid.ProjectID)
	dst = marshalUint64Hex(dst, n)
	return dst
}

// marshal appends the marshaled tid to dst and returns the result
func (tid *TenantID) marshal(dst []byte) []byte {
	dst = encoding.MarshalUint32(dst, tid.AccountID)
	dst = encoding.MarshalUint32(dst, tid.ProjectID)
	return dst
}

// unmarshal unmarshals tid from src and returns the remaining tail.
func (tid *TenantID) unmarshal(src []byte) ([]byte, error) {
	if len(src) < 8 {
		return src, fmt.Errorf("cannot unmarshal tenantID from %d bytes; need at least 8 bytes", len(src))
	}
	tid.AccountID = encoding.UnmarshalUint32(src[:4])
	tid.ProjectID = encoding.UnmarshalUint32(src[4:])
	return src[8:], nil
}

// GetTenantIDFromRequest returns tenantID from r.
func GetTenantIDFromRequest(r *http.Request) (TenantID, error) {
	var tenantID TenantID

	accountID, err := getUint32FromHeader(r, "AccountID")
	if err != nil {
		return tenantID, err
	}
	projectID, err := getUint32FromHeader(r, "ProjectID")
	if err != nil {
		return tenantID, err
	}

	tenantID.AccountID = accountID
	tenantID.ProjectID = projectID
	return tenantID, nil
}

// ParseTenantID returns tenantID from s.
//
// s is expected in the form of accountID:projectID. If s is empty, then zero tenantID is returned.
func ParseTenantID(s string) (TenantID, error) {
	var tenantID TenantID
	if s == "" {
		return tenantID, nil
	}

	before, after, ok := strings.Cut(s, ":")
	if !ok {
		account, err := getUint32FromString(s)
		if err != nil {
			return tenantID, fmt.Errorf("cannot parse accountID from %q: %w", s, err)
		}
		tenantID.AccountID = account

		return tenantID, nil
	}

	account, err := getUint32FromString(before)
	if err != nil {
		return tenantID, fmt.Errorf("cannot parse accountID part from %q: %w", s, err)
	}
	tenantID.AccountID = account

	project, err := getUint32FromString(after)
	if err != nil {
		return tenantID, fmt.Errorf("cannot parse projectID part from %q: %w", s, err)
	}
	tenantID.ProjectID = project

	return tenantID, nil
}

// MarshalTenantIDsToJSON returns JSON representation of the given tenantIDs
func MarshalTenantIDsToJSON(tenantIDs []TenantID) []byte {
	data, err := json.Marshal(tenantIDs)
	if err != nil {
		logger.Panicf("BUG: cannot marshal tenantIDs to JSON: %s", err)
	}
	return data
}

// UnmarshalTenantIDsFromJSON unmarshals tenantIDs from JSON array at src.
func UnmarshalTenantIDsFromJSON(src []byte) ([]TenantID, error) {
	var tenantIDs []TenantID
	if err := json.Unmarshal(src, &tenantIDs); err != nil {
		return nil, fmt.Errorf("cannot unmarshal tenantIDs from JSON array: %w", err)
	}
	return tenantIDs, nil
}

func getUint32FromHeader(r *http.Request, headerName string) (uint32, error) {
	s := r.Header.Get(headerName)
	if len(s) == 0 {
		return 0, nil
	}
	return getUint32FromString(s)
}

func getUint32FromString(s string) (uint32, error) {
	if len(s) == 0 {
		return 0, nil
	}
	n, err := strconv.ParseUint(s, 10, 32)
	if err != nil {
		return 0, fmt.Errorf("cannot parse %q as uint32: %w", s, err)
	}
	return uint32(n), nil
}
