package securityevents

import (
	"context"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/tls"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math/big"
	"net/http"
	"net/url"
	"strings"
	"sync"
	"time"

	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

const (
	SessionRevokedEventType = "https://schemas.openid.net/secevent/caep/event-type/session-revoked"
	VerificationEventType   = "https://schemas.openid.net/secevent/ssf/event-type/verification"
	StreamUpdatedEventType  = "https://schemas.openid.net/secevent/ssf/event-type/stream-updated"
)

type Event struct {
	Issuer           string
	Audience         string
	JTI              string
	TransactionID    string
	IssuedAt         time.Time
	EventType        string
	EventTimestamp   time.Time
	SubjectIssuer    string
	ApplicationID    string
	SubjectType      string
	Subject          string
	TenantID         string
	InitiatingEntity string
	StreamID         string
	State            string
	StreamStatus     string
	Reason           string
}

type VerifierConfig struct {
	TransmitterIssuer string
	Audience          string
	SubjectIssuer     string
	TenantID          string
	ApplicationID     string
	TenantMode        string
	StreamID          string
	ClockSkew         time.Duration
	HTTPClient        *http.Client
}

type Verifier struct {
	config    VerifierConfig
	client    *http.Client
	mu        sync.Mutex
	keys      map[string]*ecdsa.PublicKey
	expiresAt time.Time
	metrics   *Metrics
	clock     func() time.Time
}

func (v *Verifier) SetMetrics(metrics *Metrics) { v.metrics = metrics }

type transmitterMetadata struct {
	Issuer  string `json:"issuer"`
	JWKSURI string `json:"jwks_uri"`
}

type jwkSet struct {
	Keys []struct {
		KID string `json:"kid"`
		KTY string `json:"kty"`
		Use string `json:"use"`
		Alg string `json:"alg"`
		Crv string `json:"crv"`
		X   string `json:"x"`
		Y   string `json:"y"`
	} `json:"keys"`
}

type protocolError struct {
	Code string
}

func (e *protocolError) Error() string { return e.Code }

func NewVerifier(config VerifierConfig) (*Verifier, error) {
	config.TransmitterIssuer = strings.TrimRight(strings.TrimSpace(config.TransmitterIssuer), "/")
	config.SubjectIssuer = strings.TrimRight(strings.TrimSpace(config.SubjectIssuer), "/")
	multiTenant := config.TenantMode == "multi_tenant"
	validBinding := multiTenant && uuid.Validate(config.ApplicationID) == nil ||
		!multiTenant && uuid.Validate(config.TenantID) == nil
	if !absoluteHTTPURL(config.TransmitterIssuer) || !absoluteHTTPURL(config.SubjectIssuer) || config.Audience == "" || !validBinding {
		return nil, errors.New("SSF transmitter, audience, subject issuer, and identity binding are required")
	}
	if _, err := uuid.Parse(config.StreamID); err != nil {
		return nil, errors.New("SSF stream id must be a UUID")
	}
	if config.ClockSkew < 0 || config.ClockSkew > 5*time.Minute {
		return nil, errors.New("SSF clock skew is invalid")
	}
	client := config.HTTPClient
	if client == nil {
		transport := http.DefaultTransport.(*http.Transport).Clone()
		transport.TLSClientConfig = &tls.Config{MinVersion: tls.VersionTLS12}
		transport.DisableCompression = true
		client = &http.Client{Timeout: 10 * time.Second,
			Transport:     transport,
			CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
	}
	return &Verifier{config: config, client: client, keys: make(map[string]*ecdsa.PublicKey), clock: time.Now}, nil
}

func (v *Verifier) Verify(ctx context.Context, raw string) (Event, error) {
	parser := jwt.NewParser(jwt.WithValidMethods([]string{"ES256"}))
	unverified, _, err := parser.ParseUnverified(raw, jwt.MapClaims{})
	if err != nil || unverified == nil || unverified.Header["typ"] != "secevent+jwt" || unverified.Header["alg"] != "ES256" {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	unverifiedClaims, ok := unverified.Claims.(jwt.MapClaims)
	if !ok {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	if stringClaim(unverifiedClaims, "iss") != v.config.TransmitterIssuer {
		return Event{}, &protocolError{Code: "invalid_issuer"}
	}
	if !claimHasAudience(unverifiedClaims["aud"], v.config.Audience) {
		return Event{}, &protocolError{Code: "invalid_audience"}
	}
	kid, _ := unverified.Header["kid"].(string)
	if kid == "" {
		return Event{}, &protocolError{Code: "invalid_key"}
	}
	key, err := v.key(ctx, kid)
	if err != nil {
		return Event{}, &protocolError{Code: "invalid_key"}
	}
	token, err := jwt.Parse(raw, func(token *jwt.Token) (any, error) { return key, nil },
		jwt.WithValidMethods([]string{"ES256"}), jwt.WithIssuer(v.config.TransmitterIssuer),
		jwt.WithAudience(v.config.Audience), jwt.WithIssuedAt(), jwt.WithLeeway(v.config.ClockSkew))
	if err != nil || !token.Valid {
		if errors.Is(err, jwt.ErrTokenSignatureInvalid) {
			return Event{}, &protocolError{Code: "invalid_key"}
		}
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	claims, ok := token.Claims.(jwt.MapClaims)
	if !ok {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	if _, present := claims["sub"]; present {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	if _, present := claims["exp"]; present {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	jti := stringClaim(claims, "jti")
	if _, err := uuid.Parse(jti); err != nil {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	issuedAt, err := claims.GetIssuedAt()
	if err != nil || issuedAt == nil {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	events, ok := claims["events"].(map[string]any)
	if !ok || len(events) != 1 {
		return Event{}, &protocolError{Code: "invalid_request"}
	}
	event := Event{Issuer: v.config.TransmitterIssuer, Audience: v.config.Audience, JTI: jti, TransactionID: stringClaim(claims, "txn"), IssuedAt: issuedAt.Time}
	if payload, present := events[SessionRevokedEventType]; present {
		value, ok := payload.(map[string]any)
		if !ok || event.TransactionID == "" {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		if _, err := uuid.Parse(event.TransactionID); err != nil {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		event.EventType, event.EventTimestamp, event.InitiatingEntity = SessionRevokedEventType, unixClaim(value["event_timestamp"]), stringValue(value["initiating_entity"])
		event.ApplicationID, event.SubjectType = stringValue(value["application_id"]), stringValue(value["subject_type"])
		if event.EventTimestamp.IsZero() || event.EventTimestamp.After(v.now().Add(v.config.ClockSkew)) ||
			!validInitiator(event.InitiatingEntity) || !v.readSessionSubject(claims["sub_id"], &event) {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		return event, nil
	}
	if payload, present := events[VerificationEventType]; present {
		value, ok := payload.(map[string]any)
		if !ok || !v.readStreamSubject(claims["sub_id"], &event) {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		event.EventType = VerificationEventType
		if rawState, present := value["state"]; present {
			var valid bool
			event.State, valid = rawState.(string)
			if !valid || len(event.State) > 1024 {
				return Event{}, &protocolError{Code: "invalid_request"}
			}
		}
		if len(event.State) > 1024 {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		return event, nil
	}
	if payload, present := events[StreamUpdatedEventType]; present {
		value, ok := payload.(map[string]any)
		if !ok || !v.readStreamSubject(claims["sub_id"], &event) {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		event.EventType = StreamUpdatedEventType
		event.StreamStatus, event.Reason = stringValue(value["status"]), stringValue(value["reason"])
		if event.StreamStatus != "enabled" && event.StreamStatus != "paused" && event.StreamStatus != "disabled" {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		if len(event.Reason) > 1024 {
			return Event{}, &protocolError{Code: "invalid_request"}
		}
		return event, nil
	}
	return Event{}, &protocolError{Code: "invalid_request"}
}

func claimHasAudience(raw any, expected string) bool {
	switch value := raw.(type) {
	case string:
		return value == expected
	case []string:
		for _, audience := range value {
			if audience == expected {
				return true
			}
		}
	case []any:
		for _, item := range value {
			if audience, ok := item.(string); ok && audience == expected {
				return true
			}
		}
	}
	return false
}

func (v *Verifier) now() time.Time {
	if v.clock != nil {
		return v.clock().UTC()
	}
	return time.Now().UTC()
}

func (v *Verifier) readSessionSubject(raw any, event *Event) bool {
	subject, ok := raw.(map[string]any)
	if !ok || stringValue(subject["format"]) != "complex" {
		return false
	}
	user, userOK := subject["user"].(map[string]any)
	tenant, tenantOK := subject["tenant"].(map[string]any)
	if !userOK || !tenantOK || stringValue(user["format"]) != "iss_sub" || stringValue(tenant["format"]) != "opaque" {
		return false
	}
	event.SubjectIssuer, event.Subject, event.TenantID = strings.TrimRight(stringValue(user["iss"]), "/"), stringValue(user["sub"]), stringValue(tenant["id"])
	if event.SubjectIssuer != v.config.SubjectIssuer || uuid.Validate(event.TenantID) != nil {
		return false
	}
	if v.config.TenantMode == "multi_tenant" {
		if event.ApplicationID != v.config.ApplicationID ||
			event.SubjectType != "principal" && event.SubjectType != "tenant" ||
			event.SubjectType == "tenant" && event.Subject != event.TenantID {
			return false
		}
	} else if event.ApplicationID != "" || event.SubjectType != "" || event.TenantID != v.config.TenantID {
		return false
	}
	return uuid.Validate(event.Subject) == nil
}

func (v *Verifier) readStreamSubject(raw any, event *Event) bool {
	subject, ok := raw.(map[string]any)
	if !ok || stringValue(subject["format"]) != "opaque" || stringValue(subject["id"]) != v.config.StreamID {
		return false
	}
	event.StreamID = v.config.StreamID
	return true
}

func (v *Verifier) key(ctx context.Context, kid string) (*ecdsa.PublicKey, error) {
	v.mu.Lock()
	if key := v.keys[kid]; key != nil && time.Now().Before(v.expiresAt) {
		v.mu.Unlock()
		return key, nil
	}
	v.mu.Unlock()
	if err := v.refresh(ctx); err != nil {
		if v.metrics != nil {
			v.metrics.ObserveJWKSRefreshFailure("ssf")
		}
		return nil, err
	}
	v.mu.Lock()
	defer v.mu.Unlock()
	key := v.keys[kid]
	if key == nil {
		return nil, errors.New("SSF signing key is unavailable")
	}
	return key, nil
}

func (v *Verifier) refresh(ctx context.Context) error {
	v.mu.Lock()
	defer v.mu.Unlock()
	configurationURL, err := ssfConfigurationURL(v.config.TransmitterIssuer)
	if err != nil {
		return err
	}
	var metadata transmitterMetadata
	if err := v.fetchJSON(ctx, configurationURL, &metadata); err != nil {
		return err
	}
	if strings.TrimRight(metadata.Issuer, "/") != v.config.TransmitterIssuer || !sameOrigin(metadata.JWKSURI, v.config.TransmitterIssuer) {
		return errors.New("SSF metadata is not bound to configured issuer")
	}
	var set jwkSet
	if err := v.fetchJSON(ctx, metadata.JWKSURI, &set); err != nil {
		return err
	}
	keys := make(map[string]*ecdsa.PublicKey)
	for _, item := range set.Keys {
		if item.KID == "" || item.KTY != "EC" || item.Alg != "ES256" || item.Crv != "P-256" || item.Use != "sig" {
			continue
		}
		x, xErr := decodeCoordinate(item.X)
		y, yErr := decodeCoordinate(item.Y)
		if xErr == nil && yErr == nil && elliptic.P256().IsOnCurve(x, y) {
			keys[item.KID] = &ecdsa.PublicKey{Curve: elliptic.P256(), X: x, Y: y}
		}
	}
	if len(keys) == 0 {
		return errors.New("SSF JWKS contains no ES256 keys")
	}
	v.keys, v.expiresAt = keys, time.Now().Add(5*time.Minute)
	return nil
}

func (v *Verifier) fetchJSON(ctx context.Context, endpoint string, target any) error {
	request, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
	if err != nil {
		return err
	}
	request.Header.Set("Accept", "application/json")
	response, err := v.client.Do(request)
	if err != nil {
		return err
	}
	defer response.Body.Close()
	if response.StatusCode != http.StatusOK {
		return fmt.Errorf("SSF endpoint returned HTTP %d", response.StatusCode)
	}
	payload, err := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1))
	if err != nil || len(payload) > 1<<20 {
		return errors.New("SSF endpoint response is too large")
	}
	if err := json.Unmarshal(payload, target); err != nil {
		return errors.New("SSF endpoint returned invalid JSON")
	}
	return nil
}

func ssfConfigurationURL(issuer string) (string, error) {
	parsed, err := url.Parse(issuer)
	if err != nil || parsed.Scheme == "" || parsed.Host == "" {
		return "", errors.New("SSF issuer is invalid")
	}
	path := strings.TrimSuffix(parsed.EscapedPath(), "/")
	parsed.Path, parsed.RawPath = "/.well-known/ssf-configuration"+path, ""
	parsed.RawQuery, parsed.Fragment = "", ""
	return parsed.String(), nil
}

func sameOrigin(candidate, issuer string) bool {
	candidateURL, candidateErr := url.Parse(candidate)
	issuerURL, issuerErr := url.Parse(issuer)
	return candidateErr == nil && issuerErr == nil && candidateURL.Scheme == issuerURL.Scheme && strings.EqualFold(candidateURL.Host, issuerURL.Host) && candidateURL.User == nil && candidateURL.Fragment == ""
}

func absoluteHTTPURL(raw string) bool {
	parsed, err := url.Parse(raw)
	return err == nil && parsed.IsAbs() && parsed.Hostname() != "" && (parsed.Scheme == "https" || parsed.Scheme == "http") && parsed.User == nil && parsed.Fragment == ""
}

func decodeCoordinate(raw string) (*big.Int, error) {
	value, err := base64.RawURLEncoding.DecodeString(raw)
	if err != nil || len(value) != 32 {
		return nil, errors.New("invalid EC coordinate")
	}
	return new(big.Int).SetBytes(value), nil
}

func stringClaim(claims jwt.MapClaims, name string) string { return stringValue(claims[name]) }

func stringValue(value any) string {
	result, _ := value.(string)
	return result
}

func unixClaim(value any) time.Time {
	switch typed := value.(type) {
	case float64:
		return time.Unix(int64(typed), 0).UTC()
	case json.Number:
		integer, err := typed.Int64()
		if err == nil {
			return time.Unix(integer, 0).UTC()
		}
	}
	return time.Time{}
}

func validInitiator(value string) bool {
	return value == "admin" || value == "user" || value == "policy" || value == "system"
}
