// Package credentiallease issues short-lived provider credentials only after
// checking the complete immutable dispatch binding against durable state.
package credentiallease

import (
	"context"
	"crypto/ed25519"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"net/http"
	"strings"
	"time"

	"github.com/google/uuid"

	"iop/apps/control-plane/internal/credentialseal"
	"iop/apps/control-plane/internal/credentialstore"
	"iop/packages/go/config"
	lease "iop/packages/go/credentiallease"
	iop "iop/proto/gen/iop"
)

var ErrBindingRejected = errors.New("credential lease binding rejected")

type opener interface {
	Open(context.Context, credentialstore.SecretEnvelope, credentialseal.Context) ([]byte, error)
}

type Service struct {
	store            *credentialstore.Store
	opener           opener
	protocolProfiles map[string]config.ProtocolProfileConf
	issuerKeyID      string
	issuerKey        ed25519.PrivateKey
	ttl              time.Duration
	clock            func() time.Time
	random           io.Reader
	live             chan struct{}
}

func New(store *credentialstore.Store, opener opener, issuerKeyID string, issuerKey ed25519.PrivateKey, ttl time.Duration, maxLive int, clock func() time.Time, randomSource io.Reader) (*Service, error) {
	if store == nil || opener == nil || strings.TrimSpace(issuerKeyID) == "" || len(issuerKey) != ed25519.PrivateKeySize || ttl < lease.MinTTL || ttl > lease.MaxTTL || maxLive < lease.MinSetSize || maxLive > lease.MaxSetSize {
		return nil, ErrBindingRejected
	}
	if clock == nil {
		clock = time.Now
	}
	if randomSource == nil {
		randomSource = rand.Reader
	}
	return &Service{
		store:            store,
		opener:           opener,
		protocolProfiles: config.BuiltInProtocolProfileCatalog(),
		issuerKeyID:      issuerKeyID,
		issuerKey:        append(ed25519.PrivateKey(nil), issuerKey...),
		ttl:              ttl,
		clock:            clock,
		random:           randomSource,
		live:             make(chan struct{}, maxLive),
	}, nil
}

// Acquire re-reads generation, route, and slot before opening secret-at-rest.
// Invalid or stale input therefore cannot trigger a keyring open.
func (s *Service) Acquire(ctx context.Context, request *iop.AcquireLeaseRequest) (*iop.AcquireLeaseResponse, error) {
	if s == nil || request == nil || request.GetBinding() == nil || len(request.GetRecipientPublicKey()) != 32 {
		return rejected(), nil
	}
	b := request.GetBinding()
	if strings.TrimSpace(request.GetEdgeId()) == "" || !validBinding(b) {
		return rejected(), nil
	}
	select {
	case s.live <- struct{}{}:
		defer func() { <-s.live }()
	default:
		return rejected(), nil
	}
	generation, err := s.store.ProjectionGeneration(ctx)
	if err != nil || generation != b.GetProjectionGeneration() {
		return rejected(), nil
	}
	route, err := s.store.GetRoute(ctx, b.GetPrincipalRef(), b.GetRouteId())
	if err != nil || route.Status != credentialstore.StatusActive || route.SlotID != b.GetCredentialSlotRef() || route.ProfileID != b.GetProfileId() || route.UpstreamModel != b.GetUpstreamTarget() || uint64(route.Revision) != b.GetRouteRevision() {
		return rejected(), nil
	}
	slot, err := s.store.GetSlot(ctx, b.GetPrincipalRef(), b.GetCredentialSlotRef())
	if err != nil || slot.Status != credentialstore.StatusActive || uint64(slot.Revision) != b.GetCredentialRevision() {
		return rejected(), nil
	}
	profile, err := config.ResolveProtocolProfile(route.ProfileID, slot.Vendor, s.protocolProfiles)
	if err != nil || strings.TrimSpace(profile.Auth.Header) == "" {
		return rejected(), nil
	}
	plaintext, err := s.opener.Open(ctx, slot.Envelope, credentialseal.Context{PrincipalID: slot.PrincipalID, SlotID: slot.ID, Kind: slot.CredentialKind})
	if err != nil {
		return rejected(), nil
	}
	defer zero(plaintext)
	leaseID, err := newLeaseID(s.random)
	if err != nil {
		return rejected(), nil
	}
	now := s.clock().UTC()
	// The built-in profile catalog may declare the auth header in canonical or
	// lowercase form (e.g. "x-api-key"). The signed lease scope only accepts the
	// canonical spelling, so normalize the trusted resolved header here before
	// issuing. validateScope stays strict for any directly supplied scope.
	headerName := http.CanonicalHeaderKey(profile.Auth.Header)
	envelope, err := lease.Issue(lease.Scope{
		LeaseID: leaseID, PrincipalRef: b.GetPrincipalRef(), CredentialSlotRef: b.GetCredentialSlotRef(),
		RouteID: b.GetRouteId(), ProfileID: b.GetProfileId(), UpstreamTarget: b.GetUpstreamTarget(),
		NodeID: b.GetNodeId(), RecipientKeyID: b.GetRecipientKeyId(), HeaderName: headerName,
		Scheme: profile.Auth.Scheme, CredentialRevision: b.GetCredentialRevision(), RouteRevision: b.GetRouteRevision(),
		ProjectionGeneration: b.GetProjectionGeneration(), IssuedAtUnixNano: now.UnixNano(), ExpiresAtUnixNano: now.Add(s.ttl).UnixNano(),
	}, plaintext, request.GetRecipientPublicKey(), s.issuerKeyID, s.issuerKey, s.random)
	if err != nil {
		return rejected(), nil
	}
	return &iop.AcquireLeaseResponse{Lease: envelope.ToProto()}, nil
}

func newLeaseID(randomSource io.Reader) (string, error) {
	var raw [16]byte
	if _, err := io.ReadFull(randomSource, raw[:]); err != nil {
		return "", err
	}
	raw[6] = (raw[6] & 0x0f) | 0x40
	raw[8] = (raw[8] & 0x3f) | 0x80
	id, err := uuid.FromBytes(raw[:])
	if err != nil {
		return "", err
	}
	return id.String(), nil
}

func validBinding(b *iop.CredentialLeaseBinding) bool {
	for _, value := range []string{b.GetPrincipalRef(), b.GetCredentialSlotRef(), b.GetRouteId(), b.GetProfileId(), b.GetUpstreamTarget(), b.GetNodeId(), b.GetRecipientKeyId()} {
		if strings.TrimSpace(value) == "" {
			return false
		}
	}
	return true
}

func rejected() *iop.AcquireLeaseResponse {
	return &iop.AcquireLeaseResponse{Error: ErrBindingRejected.Error()}
}

func LoadIssuerPrivateKey(path string) (ed25519.PrivateKey, error) {
	raw, err := lease.LoadPrivateKeyFile(path, ed25519.PrivateKeySize)
	if err != nil {
		return nil, fmt.Errorf("load credential lease issuer: %w", err)
	}
	return ed25519.PrivateKey(raw), nil
}

func zero(value []byte) {
	for i := range value {
		value[i] = 0
	}
}
