package identityruntime

import (
	"context"
	"errors"
	"net/http"
	"strings"
	"sync"
	"sync/atomic"
	"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"
)

type Repository interface {
	IdentityConfigurationRevision(context.Context, string) (identity.Revision, error)
	ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error)
	MarkIdentityRevisionValidated(context.Context, string, int64, string, string) (identity.Revision, error)
	RevalidateActiveIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, error)
	MarkIdentityRevisionFailed(context.Context, string, int64, string, string, string) (identity.Revision, error)
	ActivateIdentityRevision(context.Context, string, int64, string, string) (identity.Revision, bool, error)
	DisableActiveIdentityRevision(context.Context, int64, string, string) (identity.Revision, error)
}

type Builder interface {
	Build(context.Context, identity.Revision) (*Runtime, error)
}

type SecurityEventDisconnector interface {
	Disconnect(context.Context) (securityevents.ConnectionView, error)
}

type Runtime struct {
	Revision                  identity.Revision
	Verifier                  *auth.OIDCVerifier
	PublicClient              *auth.OIDCPublicClient
	Sessions                  *oidcsession.Service
	SessionCipher             *oidcsession.Cipher
	SecurityEvents            *securityevents.ConnectionManager
	TenantContext             *identity.TenantContextClient
	CookieSecure              bool
	close                     func()
	start                     func()
	startOnce                 sync.Once
	securityEventDisconnector SecurityEventDisconnector
}

func (runtime *Runtime) Start() {
	if runtime != nil && runtime.start != nil {
		runtime.startOnce.Do(runtime.start)
	}
}

func (runtime *Runtime) Close() {
	if runtime != nil && runtime.close != nil {
		runtime.close()
	}
}

type Manager struct {
	repository             Repository
	builder                Builder
	operation              sync.Mutex
	current                atomic.Pointer[Runtime]
	reconciliationRequired atomic.Bool
	legacyJWTAllowed       atomic.Bool
	trustedWebBaseURL      atomic.Pointer[string]
}

const identityRuntimeReconciliationTimeout = 5 * time.Second

func NewManager(repository Repository, builder Builder) *Manager {
	manager := &Manager{repository: repository, builder: builder}
	manager.legacyJWTAllowed.Store(true)
	return manager
}

func (manager *Manager) Current() *Runtime {
	return manager.current.Load()
}

func (manager *Manager) ReconciliationRequired() bool {
	return manager.reconciliationRequired.Load()
}

func (manager *Manager) LegacyJWTEnabled() bool {
	return manager.legacyJWTAllowed.Load()
}

// TrustedWebBaseURL is the persisted Active Revision's exact browser origin.
// It intentionally survives a fail-closed Runtime build so the local
// break-glass manager can still repair or disable a broken Active Revision.
func (manager *Manager) TrustedWebBaseURL() string {
	value := manager.trustedWebBaseURL.Load()
	if value == nil {
		return ""
	}
	return *value
}

// SecurityEventReceiver resolves the request-time receiver. During first
// onboarding there is no Active Runtime yet, so the builder-owned prepared
// receiver must remain reachable for SSF Verification callbacks.
func (manager *Manager) SecurityEventReceiver() http.Handler {
	provider, ok := manager.builder.(interface{ PreparedSecurityEventReceiver() http.Handler })
	if ok {
		if prepared := provider.PreparedSecurityEventReceiver(); prepared != nil {
			return prepared
		}
	}
	if runtime := manager.Current(); runtime != nil && runtime.SecurityEvents != nil {
		return runtime.SecurityEvents
	}
	if securityEventManager := manager.SecurityEventManager(); securityEventManager != nil {
		return securityEventManager
	}
	return nil
}

// SecurityEventManager resolves the manager used by administrative recovery
// operations. A prepared manager may represent an older persisted connection
// that must be retired before the first identity Runtime can be activated.
func (manager *Manager) SecurityEventManager() *securityevents.ConnectionManager {
	if runtime := manager.Current(); runtime != nil && runtime.SecurityEvents != nil {
		return runtime.SecurityEvents
	}
	managerProvider, ok := manager.builder.(interface {
		PreparedSecurityEventManager() *securityevents.ConnectionManager
	})
	if ok {
		return managerProvider.PreparedSecurityEventManager()
	}
	return nil
}

func (manager *Manager) LoadActive(ctx context.Context) error {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	revision, err := manager.repository.ActiveIdentityConfigurationRevision(ctx)
	if errors.Is(err, identity.ErrRevisionNotFound) {
		manager.publishDisabledRuntime()
		return nil
	}
	if err != nil {
		manager.failClosedRuntime()
		return err
	}
	manager.rememberTrustedWebBaseURL(revision.WebBaseURL)
	runtime, err := manager.builder.Build(ctx, revision)
	if err != nil {
		manager.failClosedRuntime()
		return err
	}
	manager.publishRuntime(runtime, revision)
	return nil
}

// ReconcileActive restores a fail-closed Runtime after an uncertain mutation
// outcome. It is safe to call periodically: a matching Active Runtime is left
// untouched, while a changed or missing Active Revision is published atomically.
func (manager *Manager) ReconcileActive(ctx context.Context) error {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	revision, err := manager.repository.ActiveIdentityConfigurationRevision(ctx)
	if errors.Is(err, identity.ErrRevisionNotFound) {
		manager.publishDisabledRuntime()
		return nil
	}
	if err != nil {
		manager.reconciliationRequired.Store(true)
		return err
	}
	manager.rememberTrustedWebBaseURL(revision.WebBaseURL)
	if current := manager.current.Load(); current != nil && current.Revision.ID == revision.ID && current.Revision.Version == revision.Version {
		manager.reconciliationRequired.Store(false)
		return nil
	}
	runtime, err := manager.builder.Build(ctx, revision)
	if err != nil {
		manager.reconciliationRequired.Store(true)
		return err
	}
	manager.publishRuntime(runtime, revision)
	return nil
}

func (manager *Manager) Validate(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
	if err != nil {
		return identity.Revision{}, err
	}
	if revision.Version != expectedVersion || revision.State != identity.RevisionDraft && revision.State != identity.RevisionActive {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	candidate, buildErr := manager.builder.Build(ctx, revision)
	if buildErr != nil {
		if revision.State != identity.RevisionActive {
			_, _ = manager.repository.MarkIdentityRevisionFailed(ctx, id, expectedVersion, "validation_failed", traceID, auditID)
		}
		return identity.Revision{}, buildErr
	}
	if revision.State == identity.RevisionActive {
		revalidated, err := manager.repository.RevalidateActiveIdentityRevision(ctx, id, expectedVersion, traceID, auditID)
		if err != nil {
			candidate.Close()
			return identity.Revision{}, err
		}
		manager.publishRuntime(candidate, revalidated)
		return revalidated, nil
	}
	candidate.Close()
	return manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
}

func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
	if err != nil {
		return identity.Revision{}, err
	}
	if revision.Version != expectedVersion || revision.State != identity.RevisionValidated {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	candidate, err := manager.builder.Build(ctx, revision)
	if err != nil {
		return identity.Revision{}, err
	}
	activated, _, err := manager.repository.ActivateIdentityRevision(ctx, id, expectedVersion, traceID, auditID)
	if err != nil {
		return manager.reconcileActivationMutation(ctx, id, candidate, err)
	}
	manager.publishRuntime(candidate, activated)
	return activated, nil
}

func (manager *Manager) Rollback(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
	if err != nil {
		return identity.Revision{}, err
	}
	if revision.Version != expectedVersion || revision.State != identity.RevisionSuperseded {
		return identity.Revision{}, identity.ErrRevisionConflict
	}
	// Auth Center currently reuses and mutates OAuth/SSF resources. An older
	// local Revision therefore cannot prove that its remote redirect URIs,
	// scopes, audiences, or credentials still match. Require a fresh onboarding
	// exchange until a versioned remote-resource handoff exists.
	return identity.Revision{}, identity.ErrRollbackConfigurationHandoffRequired
}

func (manager *Manager) Disable(ctx context.Context, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	if err := manager.retireActiveSecurityEventsBeforeDisable(ctx, expectedVersion); err != nil {
		return identity.Revision{}, err
	}
	disabled, err := manager.repository.DisableActiveIdentityRevision(ctx, expectedVersion, traceID, auditID)
	if err != nil {
		return manager.reconcileDisableMutation(ctx, expectedVersion, err)
	}
	manager.publishDisabledRuntime()
	return disabled, nil
}

func (manager *Manager) retireActiveSecurityEventsBeforeDisable(ctx context.Context, expectedVersion int64) error {
	active, err := manager.repository.ActiveIdentityConfigurationRevision(ctx)
	if err != nil {
		return err
	}
	if active.Version != expectedVersion {
		return identity.ErrRevisionConflict
	}
	if !active.SessionRevocation {
		return nil
	}
	runtime := manager.current.Load()
	var disconnector SecurityEventDisconnector
	if runtime != nil && runtime.Revision.ID == active.ID {
		disconnector = runtime.securityEventDisconnector
		if disconnector == nil && runtime.SecurityEvents != nil {
			disconnector = runtime.SecurityEvents
		}
	}
	if disconnector == nil {
		recoverer, ok := manager.builder.(interface {
			RecoverSecurityEventDisconnector(context.Context, identity.Revision) (SecurityEventDisconnector, error)
		})
		if !ok {
			return identity.ErrSecurityEventRetirementPending
		}
		var recoverErr error
		disconnector, recoverErr = recoverer.RecoverSecurityEventDisconnector(ctx, active)
		if recoverErr != nil {
			return recoverErr
		}
		// A missing local connection means there is no bound local Stream or
		// credential left to retire. The already-required audit record makes
		// this fail-closed recovery decision traceable.
		if disconnector == nil {
			return nil
		}
	}
	connection, err := disconnector.Disconnect(ctx)
	if err != nil {
		return err
	}
	if connection.LifecycleStatus != "retiring" {
		return identity.ErrSecurityEventRetirementPending
	}
	return nil
}

func (manager *Manager) reconcileActivationMutation(ctx context.Context, targetID string, candidate *Runtime, mutationErr error) (identity.Revision, error) {
	reconcileCtx, cancel := identityRuntimeReconciliationContext(ctx)
	defer cancel()
	active, err := manager.repository.ActiveIdentityConfigurationRevision(reconcileCtx)
	if err == nil && active.ID == targetID && active.State == identity.RevisionActive {
		manager.publishRuntime(candidate, active)
		return active, nil
	}
	candidate.Close()
	if errors.Is(err, identity.ErrRevisionNotFound) {
		manager.publishDisabledRuntime()
		return identity.Revision{}, mutationErr
	}
	if err != nil {
		manager.failClosedRuntime()
		return identity.Revision{}, mutationErr
	}
	current := manager.current.Load()
	if current == nil || current.Revision.ID != active.ID {
		manager.failClosedRuntime()
	}
	return identity.Revision{}, mutationErr
}

func (manager *Manager) reconcileRollbackValidation(ctx context.Context, targetID string, expectedVersion int64, mutationErr error) (identity.Revision, error) {
	reconcileCtx, cancel := identityRuntimeReconciliationContext(ctx)
	defer cancel()
	revision, err := manager.repository.IdentityConfigurationRevision(reconcileCtx, targetID)
	if err == nil && revision.State == identity.RevisionValidated && revision.Version == expectedVersion+1 {
		return revision, nil
	}
	return identity.Revision{}, mutationErr
}

func (manager *Manager) reconcileDisableMutation(ctx context.Context, expectedVersion int64, mutationErr error) (identity.Revision, error) {
	previous := manager.current.Load()
	reconcileCtx, cancel := identityRuntimeReconciliationContext(ctx)
	defer cancel()
	active, err := manager.repository.ActiveIdentityConfigurationRevision(reconcileCtx)
	if err == nil {
		if previous == nil || active.ID != previous.Revision.ID {
			manager.failClosedRuntime()
		}
		return identity.Revision{}, mutationErr
	}
	if !errors.Is(err, identity.ErrRevisionNotFound) {
		manager.failClosedRuntime()
		return identity.Revision{}, mutationErr
	}
	manager.publishDisabledRuntime()
	if previous == nil || previous.Revision.ID == "" {
		return identity.Revision{}, mutationErr
	}
	disabled, lookupErr := manager.repository.IdentityConfigurationRevision(reconcileCtx, previous.Revision.ID)
	if lookupErr == nil && disabled.State == identity.RevisionSuperseded && disabled.Version == expectedVersion+1 {
		return disabled, nil
	}
	return identity.Revision{}, mutationErr
}

func identityRuntimeReconciliationContext(ctx context.Context) (context.Context, context.CancelFunc) {
	return context.WithTimeout(context.WithoutCancel(ctx), identityRuntimeReconciliationTimeout)
}

func (manager *Manager) publishRuntime(candidate *Runtime, revision identity.Revision) {
	candidate.Revision = revision
	manager.rememberTrustedWebBaseURL(revision.WebBaseURL)
	manager.legacyJWTAllowed.Store(revision.LegacyJWTEnabled)
	old := manager.current.Swap(candidate)
	candidate.Start()
	manager.adoptPreparedSecurityEvents(candidate, revision.ID)
	manager.reconciliationRequired.Store(false)
	retireRuntime(old)
}

func (manager *Manager) publishDisabledRuntime() {
	old := manager.current.Swap(nil)
	manager.trustedWebBaseURL.Store(nil)
	manager.legacyJWTAllowed.Store(true)
	manager.reconciliationRequired.Store(false)
	retireRuntime(old)
}

func (manager *Manager) failClosedRuntime() {
	manager.legacyJWTAllowed.Store(false)
	manager.reconciliationRequired.Store(true)
	old := manager.current.Swap(nil)
	retireRuntime(old)
}

func (manager *Manager) rememberTrustedWebBaseURL(value string) {
	normalized := strings.TrimSpace(value)
	if normalized == "" {
		manager.trustedWebBaseURL.Store(nil)
		return
	}
	manager.trustedWebBaseURL.Store(&normalized)
}

func (manager *Manager) PrepareSecurityEvents(ctx context.Context, revision identity.Revision, secret []byte) error {
	manager.operation.Lock()
	defer manager.operation.Unlock()
	current, err := manager.repository.IdentityConfigurationRevision(ctx, revision.ID)
	if err != nil {
		return err
	}
	if current.State == identity.RevisionActive {
		// Activation already crossed the prepared-manager adoption point. A
		// stale restart restore must not insert a second Receiver afterwards.
		return nil
	}
	if current.Version != revision.Version || current.State != identity.RevisionDraft && current.State != identity.RevisionValidated {
		return identity.ErrRevisionConflict
	}
	preparer, ok := manager.builder.(interface {
		PrepareSecurityEvents(context.Context, identity.Revision, []byte) error
	})
	if !ok {
		return errors.New("security event runtime preparation is unavailable")
	}
	return preparer.PrepareSecurityEvents(ctx, current, secret)
}

func (manager *Manager) CleanupPreparedSecurityEvents(ctx context.Context, revision identity.Revision) error {
	cleaner, ok := manager.builder.(interface {
		CleanupPreparedSecurityEvents(context.Context, identity.Revision) error
	})
	if !ok {
		return errors.New("security event runtime cleanup is unavailable")
	}
	return cleaner.CleanupPreparedSecurityEvents(ctx, revision)
}

func (manager *Manager) RetireConflictingSecurityEvents(ctx context.Context, revision identity.Revision) error {
	resolver, ok := manager.builder.(interface {
		RetireConflictingSecurityEvents(context.Context, identity.Revision) error
	})
	if !ok {
		return errors.New("security event conflict recovery is unavailable")
	}
	return resolver.RetireConflictingSecurityEvents(ctx, revision)
}

func (manager *Manager) adoptPreparedSecurityEvents(runtime *Runtime, revisionID string) {
	adopter, ok := manager.builder.(interface {
		AdoptPreparedSecurityEvents(string) context.CancelFunc
	})
	if !ok {
		return
	}
	cancel := adopter.AdoptPreparedSecurityEvents(revisionID)
	if cancel == nil {
		return
	}
	closeRuntime := runtime.close
	runtime.close = func() {
		if closeRuntime != nil {
			closeRuntime()
		}
		cancel()
	}
}

func retireRuntime(runtime *Runtime) {
	if runtime == nil || runtime.close == nil {
		return
	}
	time.AfterFunc(30*time.Second, runtime.Close)
}
