package identityruntime

import (
	"context"
	"errors"
	"net/http"
	"net/url"
	"slices"
	"strings"
	"sync"
	"time"

	"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents"
	"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)

type RuntimeBuilderConfig struct {
	AppEnv            string
	JWKSCacheTTL      time.Duration
	HeartbeatInterval time.Duration
	StaleAfter        time.Duration
	ClockSkew         time.Duration
}

type preparedSecurityRuntime struct {
	manager         *securityevents.ConnectionManager
	cancel          context.CancelFunc
	receiverReady   bool
	blockedCategory string
}

type safeRuntimeError struct {
	category string
	cause    error
}

func (err safeRuntimeError) Error() string {
	return "identity runtime operation failed: " + err.category
}
func (err safeRuntimeError) Unwrap() error             { return err.cause }
func (err safeRuntimeError) SafeErrorCategory() string { return err.category }

type RuntimeBuilder struct {
	ctx      context.Context
	store    *store.Store
	secrets  PairingSecretStore
	config   RuntimeBuilderConfig
	metrics  *securityevents.Metrics
	mutex    sync.Mutex
	prepared map[string]preparedSecurityRuntime
}

type PairingSecretStore interface {
	Put(context.Context, string, []byte) error
	Get(context.Context, string) ([]byte, error)
	Delete(context.Context, string) error
}

func NewRuntimeBuilder(ctx context.Context, data *store.Store, secrets PairingSecretStore, config RuntimeBuilderConfig, metrics *securityevents.Metrics) *RuntimeBuilder {
	if metrics == nil {
		metrics = &securityevents.Metrics{}
	}
	if config.JWKSCacheTTL <= 0 {
		config.JWKSCacheTTL = 5 * time.Minute
	}
	return &RuntimeBuilder{ctx: ctx, store: data, secrets: secrets, config: config, metrics: metrics, prepared: map[string]preparedSecurityRuntime{}}
}

func (builder *RuntimeBuilder) PreparedSecurityEventReceiver() http.Handler {
	builder.mutex.Lock()
	defer builder.mutex.Unlock()
	if len(builder.prepared) != 1 {
		return nil
	}
	for _, prepared := range builder.prepared {
		if prepared.receiverReady {
			return prepared.manager
		}
	}
	return nil
}

func (builder *RuntimeBuilder) PreparedSecurityEventManager() *securityevents.ConnectionManager {
	builder.mutex.Lock()
	defer builder.mutex.Unlock()
	if len(builder.prepared) != 1 {
		return nil
	}
	for _, prepared := range builder.prepared {
		return prepared.manager
	}
	return nil
}

func (builder *RuntimeBuilder) Build(ctx context.Context, revision identity.Revision) (*Runtime, error) {
	if err := identity.ValidateRevisionURLs(revision, builder.config.AppEnv); err != nil {
		return nil, err
	}
	if builder.store == nil || builder.secrets == nil || revision.Issuer == "" ||
		revision.Audience == "" || revision.RolePrefix == "" ||
		revision.TenantMode != "multi_tenant" && revision.TenantID == "" ||
		revision.TenantMode == "multi_tenant" && (revision.ApplicationID == "" || revision.TenantContextEndpoint == "") {
		return nil, errors.New("identity runtime configuration is incomplete")
	}
	if revision.TenantMode != "multi_tenant" {
		if exists, err := builder.store.HasActiveTenantKey(ctx, revision.LocalTenantKey); err != nil {
			return nil, err
		} else if !exists {
			return nil, identity.ErrLocalTenantInvalid
		}
	}
	runtimeCtx, cancel := context.WithCancel(builder.ctx)
	runtime := &Runtime{Revision: revision, CookieSecure: secureCookieFor(revision.PublicBaseURL), close: cancel}

	var securityManager *securityevents.ConnectionManager
	if revision.SessionRevocation {
		prepared := builder.preparedSecurityRuntime(revision.ID)
		securityManager = prepared.manager
		if securityManager != nil {
			if err := securityManager.ValidateConfiguredConnectionBinding(ctx); err != nil {
				cancel()
				return nil, err
			}
			if !prepared.receiverReady {
				cancel()
				return nil, safeRuntimeError{category: "security_event_not_ready", cause: store.ErrSecurityEventConnectionConflict}
			}
		}
		if securityManager == nil {
			var err error
			securityManager, err = builder.newSecurityEventManager(runtimeCtx, revision, true)
			if err != nil {
				cancel()
				return nil, err
			}
		}
		runtime.SecurityEvents = securityManager
		runtime.securityEventDisconnector = securityManager
	}

	var evaluator func(context.Context, auth.OIDCSecurityEventIdentity) (auth.OIDCSecurityEventEvaluation, error)
	if securityManager != nil {
		evaluator = securityManager.Evaluate
	}
	credentialProvider := func(ctx context.Context) (string, []byte, error) {
		if revision.MachineClientID == "" || revision.MachineCredentialRef == "" {
			return "", nil, errors.New("managed machine credential is unavailable")
		}
		secret, err := builder.secrets.Get(ctx, revision.MachineCredentialRef)
		return revision.MachineClientID, secret, err
	}
	verifier, err := auth.NewOIDCVerifier(auth.OIDCConfig{
		AppEnv: builder.config.AppEnv,
		Issuer: revision.Issuer, Audience: revision.Audience, TenantID: revision.TenantID,
		TenantMode: revision.TenantMode, ApplicationID: revision.ApplicationID, ClientID: revision.BrowserClientID,
		RolePrefix: revision.RolePrefix, RequiredScopes: append([]string(nil), revision.Scopes...),
		JWKSCacheTTL: builder.config.JWKSCacheTTL, IntrospectionEnabled: revision.TokenIntrospection,
		IntrospectionCredentialProvider: credentialProvider, SecurityEventEvaluator: evaluator,
		IntrospectionObserver:      builder.metrics.ObserveIntrospection,
		JWKSRefreshFailureObserver: func() { builder.metrics.ObserveJWKSRefreshFailure("oidc") },
	})
	if err != nil {
		cancel()
		return nil, err
	}
	if err := verifier.ValidateConfiguration(ctx); err != nil {
		cancel()
		return nil, err
	}
	runtime.Verifier = verifier
	if revision.TenantMode == "multi_tenant" {
		tenantContext, err := identity.NewTenantContextClient(
			builder.config.AppEnv, revision.Issuer, revision.ApplicationID,
			revision.TenantContextEndpoint, revision.TenantContextAudience, revision.TenantContextScope,
			credentialProvider, nil,
		)
		if err != nil {
			cancel()
			return nil, err
		}
		runtime.TenantContext = tenantContext
		runtime.start = func() {
			go runTenantContextSynchronizer(
				runtimeCtx, builder.store, tenantContext, revision.Issuer, revision.ApplicationID,
			)
		}
	}

	if slices.Contains(revision.Capabilities, "oidc_login") {
		if revision.BrowserClientID == "" || revision.SessionEncryptionKeyRef == "" {
			cancel()
			return nil, errors.New("OIDC browser session configuration is incomplete")
		}
		key, err := builder.secrets.Get(ctx, revision.SessionEncryptionKeyRef)
		if err != nil {
			cancel()
			return nil, err
		}
		cipher, err := oidcsession.NewCipher(key)
		clear(key)
		if err != nil {
			cancel()
			return nil, err
		}
		client, err := auth.NewOIDCPublicClient(auth.OIDCPublicClientConfig{
			AppEnv: builder.config.AppEnv,
			Issuer: revision.Issuer, ClientID: revision.BrowserClientID,
			RedirectURI:           revision.PublicBaseURL + "/api/v1/auth/oidc/callback",
			PostLogoutRedirectURI: revision.WebBaseURL + "/", Scopes: append([]string{"openid", "profile"}, revision.Scopes...),
		})
		if err != nil {
			cancel()
			return nil, err
		}
		if err := client.ValidateConfiguration(ctx); err != nil {
			cancel()
			return nil, err
		}
		sessions, err := oidcsession.NewService(builder.store, cipher, verifier, client, oidcsession.Config{
			IdleTTL:       time.Duration(revision.SessionIdleSeconds) * time.Second,
			AbsoluteTTL:   time.Duration(revision.SessionAbsoluteSeconds) * time.Second,
			RefreshBefore: time.Duration(revision.SessionRefreshSeconds) * time.Second,
		})
		if err != nil {
			cancel()
			return nil, err
		}
		runtime.PublicClient, runtime.Sessions, runtime.SessionCipher = client, sessions, cipher
	}
	return runtime, nil
}

func (builder *RuntimeBuilder) PrepareSecurityEvents(ctx context.Context, revision identity.Revision, managementSecret []byte) error {
	if err := identity.ValidateRevisionURLs(revision, builder.config.AppEnv); err != nil {
		return safeRuntimeError{category: "configuration_invalid", cause: err}
	}
	if !revision.SessionRevocation || revision.SecurityEventIssuer == "" || revision.SecurityEventAudience == "" || revision.MachineClientID == "" {
		return errors.New("security event configuration is incomplete")
	}
	if prepared := builder.preparedSecurityRuntime(revision.ID); prepared.manager != nil && prepared.blockedCategory != "" {
		view, err := prepared.manager.Get(ctx)
		if err == nil {
			category := prepared.blockedCategory
			if view.LifecycleStatus == "retiring" || view.LifecycleStatus == "disconnect_pending" {
				category = "retirement_pending"
			}
			return safeRuntimeError{category: category, cause: store.ErrSecurityEventConnectionConflict}
		}
		if !errors.Is(err, store.ErrSecurityEventConnectionNotFound) {
			return safeSecurityEventRuntimeError(err)
		}
		builder.removePreparedSecurityRuntime(revision.ID, prepared)
	}
	runtimeCtx, cancel := context.WithCancel(builder.ctx)
	manager, err := builder.newSecurityEventManager(runtimeCtx, revision, false)
	if err != nil {
		cancel()
		return safeRuntimeError{category: "configuration_invalid", cause: err}
	}
	secretCopy := append([]byte(nil), managementSecret...)
	_, err = manager.Connect(ctx, revision.SecurityEventIssuer, revision.MachineClientID, secretCopy, "identity-pairing-ssf-"+revision.ID)
	clear(secretCopy)
	if err != nil {
		// A conflicting persisted connection is itself the resource an
		// administrator must safely retire. Keep this manager reachable by the
		// recovery API even when there is no Active identity Runtime yet.
		if errors.Is(err, store.ErrSecurityEventConnectionConflict) {
			builder.replacePreparedSecurityRuntime(revision.ID, preparedSecurityRuntime{
				manager: manager, cancel: cancel, blockedCategory: safeSecurityEventCategory(err, "connection_conflict"),
			})
			return safeSecurityEventRuntimeError(err)
		}
		cancel()
		return safeSecurityEventRuntimeError(err)
	}
	if err := manager.ValidateConfiguredConnectionBinding(ctx); err != nil {
		builder.replacePreparedSecurityRuntime(revision.ID, preparedSecurityRuntime{
			manager: manager, cancel: cancel, blockedCategory: safeSecurityEventCategory(err, "connection_binding_mismatch"),
		})
		return safeSecurityEventRuntimeError(err)
	}
	builder.replacePreparedSecurityRuntime(revision.ID, preparedSecurityRuntime{manager: manager, cancel: cancel, receiverReady: true})
	return nil
}

func (builder *RuntimeBuilder) removePreparedSecurityRuntime(revisionID string, prepared preparedSecurityRuntime) {
	builder.mutex.Lock()
	current := builder.prepared[revisionID]
	if current.manager == prepared.manager {
		delete(builder.prepared, revisionID)
	}
	builder.mutex.Unlock()
	if prepared.cancel != nil {
		prepared.cancel()
	}
}

func (builder *RuntimeBuilder) replacePreparedSecurityRuntime(revisionID string, replacement preparedSecurityRuntime) {
	builder.mutex.Lock()
	previous := builder.prepared[revisionID]
	builder.prepared[revisionID] = replacement
	builder.mutex.Unlock()
	if previous.cancel != nil {
		previous.cancel()
	}
}

func (builder *RuntimeBuilder) CleanupPreparedSecurityEvents(ctx context.Context, revision identity.Revision) error {
	builder.mutex.Lock()
	prepared, exists := builder.prepared[revision.ID]
	builder.mutex.Unlock()
	if !exists {
		runtimeCtx, cancel := context.WithCancel(builder.ctx)
		manager, err := builder.newSecurityEventManager(runtimeCtx, revision, false)
		if err != nil {
			cancel()
			return safeRuntimeError{category: "configuration_invalid", cause: err}
		}
		prepared = preparedSecurityRuntime{manager: manager, cancel: cancel}
	}
	err := prepared.manager.DiscardPreparedConnection(ctx, "identity-pairing-ssf-"+revision.ID)
	if err != nil {
		builder.mutex.Lock()
		if _, alreadyStored := builder.prepared[revision.ID]; !alreadyStored {
			builder.prepared[revision.ID] = prepared
		}
		builder.mutex.Unlock()
		return safeSecurityEventRuntimeError(err)
	}
	builder.mutex.Lock()
	current := builder.prepared[revision.ID]
	if current.manager == prepared.manager {
		delete(builder.prepared, revision.ID)
	}
	builder.mutex.Unlock()
	if prepared.cancel != nil {
		prepared.cancel()
	}
	return nil
}

func (builder *RuntimeBuilder) RetireConflictingSecurityEvents(ctx context.Context, revision identity.Revision) error {
	prepared := builder.preparedSecurityRuntime(revision.ID)
	if prepared.manager == nil {
		runtimeCtx, cancel := context.WithCancel(builder.ctx)
		manager, err := builder.newSecurityEventManager(runtimeCtx, revision, false)
		if err != nil {
			cancel()
			return safeRuntimeError{category: "configuration_invalid", cause: err}
		}
		prepared = preparedSecurityRuntime{manager: manager, cancel: cancel}
		builder.replacePreparedSecurityRuntime(revision.ID, prepared)
	}
	if err := prepared.manager.RetireConflictingConnection(ctx, "identity-pairing-ssf-"+revision.ID); err != nil {
		return safeSecurityEventRuntimeError(err)
	}
	return nil
}

// RecoverSecurityEventDisconnector reconstructs only the SSF management
// surface needed to disable a fail-closed Active Revision. It deliberately
// avoids OIDC discovery, JIT, BFF Session and tenant Runtime construction.
func (builder *RuntimeBuilder) RecoverSecurityEventDisconnector(_ context.Context, revision identity.Revision) (SecurityEventDisconnector, error) {
	prepared := builder.preparedSecurityRuntime(revision.ID)
	if prepared.manager != nil {
		if err := prepared.manager.ValidateConfiguredConnectionBinding(builder.ctx); err != nil {
			return nil, err
		}
		return prepared.manager, nil
	}
	manager, err := builder.newSecurityEventManager(builder.ctx, revision, true)
	if errors.Is(err, store.ErrSecurityEventConnectionNotFound) {
		return nil, nil
	}
	if err != nil {
		return nil, err
	}
	return manager, nil
}

// AdoptPreparedSecurityEvents transfers the prepared manager lifetime to an
// activated Runtime. Validation only peeks at prepared state, so Verification
// callbacks remain available between validation and activation.
func (builder *RuntimeBuilder) AdoptPreparedSecurityEvents(revisionID string) context.CancelFunc {
	builder.mutex.Lock()
	defer builder.mutex.Unlock()
	prepared := builder.prepared[revisionID]
	delete(builder.prepared, revisionID)
	return prepared.cancel
}

func safeSecurityEventRuntimeError(err error) error {
	var categorized interface{ SafeErrorCategory() string }
	if errors.As(err, &categorized) {
		return err
	}
	if errors.Is(err, store.ErrSecurityEventConnectionConflict) {
		return safeRuntimeError{category: "connection_conflict", cause: err}
	}
	return safeRuntimeError{category: "preparation_failed", cause: err}
}

func safeSecurityEventCategory(err error, fallback string) string {
	var categorized interface{ SafeErrorCategory() string }
	if errors.As(err, &categorized) && categorized.SafeErrorCategory() != "" {
		return categorized.SafeErrorCategory()
	}
	return fallback
}

func (builder *RuntimeBuilder) newSecurityEventManager(ctx context.Context, revision identity.Revision, strictRevisionBinding bool) (*securityevents.ConnectionManager, error) {
	return securityevents.NewConnectionManager(ctx, builder.store, builder.secrets, builder.securityEventManagerConfig(revision, strictRevisionBinding), builder.metrics)
}

func (builder *RuntimeBuilder) securityEventManagerConfig(revision identity.Revision, strictRevisionBinding bool) securityevents.ConnectionManagerConfig {
	return securityevents.ConnectionManagerConfig{
		AppEnv: builder.config.AppEnv, OIDCEnabled: true, OIDCIssuer: revision.Issuer, OIDCTenantID: revision.TenantID,
		OIDCApplicationID: revision.ApplicationID, OIDCTenantMode: revision.TenantMode,
		ManagementClientID: revision.MachineClientID, PublicBaseURL: revision.PublicBaseURL,
		ExpectedTransmitterIssuer: strings.TrimRight(strings.TrimSpace(revision.SecurityEventIssuer), "/"),
		ExpectedAudience:          revision.SecurityEventAudience,
		ExpectedOwnerKey:          "identity-pairing-ssf-" + revision.ID,
		StrictRevisionBinding:     strictRevisionBinding,
		HeartbeatInterval:         builder.config.HeartbeatInterval,
		StaleAfter:                builder.config.StaleAfter,
		ClockSkew:                 builder.config.ClockSkew,
	}
}

func (builder *RuntimeBuilder) preparedSecurityRuntime(revisionID string) preparedSecurityRuntime {
	builder.mutex.Lock()
	defer builder.mutex.Unlock()
	return builder.prepared[revisionID]
}

func secureCookieFor(baseURL string) bool {
	parsed, err := url.Parse(baseURL)
	return err == nil && parsed.Scheme == "https"
}
