package license

import (
	"context"
	"errors"
	"fmt"
)

// MultiRemote races read-only health probes, then sends each mutating license
// request to one selected node. Query and heartbeat calls remain single-node
// operations so a shared lease sequence is not incremented more than once.
type MultiRemote struct {
	remotes []Remote
}

type latencyProber interface {
	Probe(context.Context) error
}

// QuorumRejectionError means a supermajority of authorization centers
// explicitly rejected one heartbeat. It is distinct from an outage, which
// remains eligible for the normal grace-period handling.
type QuorumRejectionError struct {
	Rejections int
	Required   int
	Total      int
	Last       *HTTPError
}

func (e *QuorumRejectionError) Error() string {
	if e.Last != nil && e.Last.Code != "" {
		return fmt.Sprintf("authorization heartbeat rejected by %d/%d nodes: %s", e.Rejections, e.Total, e.Last.Code)
	}
	return fmt.Sprintf("authorization heartbeat rejected by %d/%d nodes", e.Rejections, e.Total)
}

func (e *QuorumRejectionError) Unwrap() error {
	return e.Last
}

// NewMultiRemote creates a MultiRemote. At least one remote is required.
func NewMultiRemote(remotes ...Remote) *MultiRemote {
	filtered := make([]Remote, 0, len(remotes))
	for _, r := range remotes {
		if r != nil {
			filtered = append(filtered, r)
		}
	}
	return &MultiRemote{remotes: filtered}
}

func (m *MultiRemote) Detail(ctx context.Context, code string) (Detail, error) {
	var lastErr error
	for _, r := range m.ordered(ctx) {
		if ctx.Err() != nil {
			return Detail{}, ctx.Err()
		}
		d, err := r.Detail(ctx, code)
		if err == nil {
			return d, nil
		}
		lastErr = err
	}
	if lastErr == nil {
		lastErr = ErrLicenseUnavailable
	}
	return Detail{}, lastErr
}

func (m *MultiRemote) Register(ctx context.Context, req RegisterRequest) (Registration, error) {
	var lastErr error
	for _, r := range m.ordered(ctx) {
		if ctx.Err() != nil {
			return Registration{}, ctx.Err()
		}
		reg, err := r.Register(ctx, req)
		if err == nil {
			return reg, nil
		}
		lastErr = err
	}
	if lastErr == nil {
		lastErr = ErrLicenseUnavailable
	}
	return Registration{}, lastErr
}

func (m *MultiRemote) QueryRaw(ctx context.Context, licenseCode, instanceID, token, backendURL string) (Query, []byte, string, error) {
	var lastErr error
	for _, r := range m.ordered(ctx) {
		if ctx.Err() != nil {
			return Query{}, nil, "", ctx.Err()
		}
		q, raw, sig, err := r.QueryRaw(ctx, licenseCode, instanceID, token, backendURL)
		if err == nil {
			return q, raw, sig, nil
		}
		lastErr = err
	}
	if lastErr == nil {
		lastErr = ErrLicenseUnavailable
	}
	return Query{}, nil, "", lastErr
}

func (m *MultiRemote) Heartbeat(ctx context.Context, token string, req HeartbeatRequest) (*HeartbeatResult, error) {
	var lastErr error
	remotes := m.ordered(ctx)
	required := quorumRequired(len(remotes))
	rejections := 0
	var lastRejection *HTTPError
	for _, r := range remotes {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		result, err := r.Heartbeat(ctx, token, req)
		if err == nil {
			return result, nil
		}
		lastErr = err
		if rejection := hardRejection(err); rejection != nil {
			rejections++
			lastRejection = rejection
			if rejections >= required {
				return nil, &QuorumRejectionError{
					Rejections: rejections,
					Required:   required,
					Total:      len(remotes),
					Last:       lastRejection,
				}
			}
		}
	}
	if lastErr == nil {
		lastErr = ErrLicenseUnavailable
	}
	return nil, lastErr
}

func quorumRequired(total int) int {
	if total <= 1 {
		return total
	}
	return (2*total + 2) / 3
}

func hardRejection(err error) *HTTPError {
	var httpError *HTTPError
	if !errors.As(err, &httpError) || (httpError.StatusCode != 401 && httpError.StatusCode != 403) {
		return nil
	}
	return httpError
}

func (m *MultiRemote) GetPublicKey(ctx context.Context) ([]byte, error) {
	var lastErr error
	for _, r := range m.ordered(ctx) {
		if ctx.Err() != nil {
			return nil, ctx.Err()
		}
		key, err := r.GetPublicKey(ctx)
		if err == nil {
			return key, nil
		}
		lastErr = err
	}
	if lastErr == nil {
		lastErr = ErrLicenseUnavailable
	}
	return nil, lastErr
}

// Len returns the number of remotes in this MultiRemote.
func (m *MultiRemote) Len() int { return len(m.remotes) }

func (m *MultiRemote) ordered(ctx context.Context) []Remote {
	if len(m.remotes) < 2 {
		return m.remotes
	}

	type probeResult struct {
		remote Remote
		err    error
	}
	probeCtx, cancel := context.WithCancel(ctx)
	defer cancel()
	results := make(chan probeResult, len(m.remotes))
	count := 0
	for _, remote := range m.remotes {
		prober, ok := remote.(latencyProber)
		if !ok {
			continue
		}
		count++
		go func(remote Remote, prober latencyProber) {
			results <- probeResult{remote: remote, err: prober.Probe(probeCtx)}
		}(remote, prober)
	}
	if count == 0 {
		return m.remotes
	}

	for range count {
		select {
		case result := <-results:
			if result.err == nil {
				return m.withFirst(result.remote)
			}
		case <-ctx.Done():
			return m.remotes
		}
	}
	return m.remotes
}

func (m *MultiRemote) withFirst(first Remote) []Remote {
	ordered := make([]Remote, 0, len(m.remotes))
	ordered = append(ordered, first)
	for _, remote := range m.remotes {
		if remote != first {
			ordered = append(ordered, remote)
		}
	}
	return ordered
}
