package httpapi

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

const (
	errorCodeGatewayUserNotProvisioned = "GATEWAY_USER_NOT_PROVISIONED"
	errorCodeGatewayUserDisabled       = "GATEWAY_USER_DISABLED"
	errorCodeGatewayTenantUnavailable  = "GATEWAY_TENANT_UNAVAILABLE"
	errorCodeGatewayProvisioningFailed = "GATEWAY_USER_PROVISIONING_FAILED"
)

type oidcUserResolver interface {
	ResolveOrProvisionOIDCUser(context.Context, store.ResolveOrProvisionOIDCUserInput) (store.ResolveOrProvisionOIDCUserResult, error)
}

type oidcTenantBindingReader interface {
	OIDCTenantBindingContext(context.Context, string, string, string) (store.OIDCTenantBindingContext, error)
}

func (s *Server) requireUser(permission auth.Permission, next http.Handler) http.Handler {
	return s.auth.Require(permission, s.resolveGatewayUser(next))
}

func (s *Server) resolveGatewayUser(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user, ok := auth.UserFromContext(r.Context())
		if !ok || user == nil || !strings.EqualFold(strings.TrimSpace(user.Source), "oidc") {
			next.ServeHTTP(w, r)
			return
		}
		result, err := s.resolveOIDCUserProjection(r.Context(), r, user)
		if err != nil {
			s.writeOIDCUserResolutionError(w, r, err)
			return
		}
		if result.User == nil || strings.TrimSpace(result.User.GatewayUserID) == "" {
			s.writeOIDCUserResolutionError(w, r, errors.New("OIDC user resolver returned no local user"))
			return
		}
		if result.Created {
			s.logger.InfoContext(r.Context(), "OIDC gateway user provisioned",
				"gatewayUserId", result.User.GatewayUserID,
				"auditId", result.AuditID,
			)
		}
		next.ServeHTTP(w, r.WithContext(auth.WithUser(r.Context(), result.User)))
	})
}

func (s *Server) resolveOIDCUserProjection(ctx context.Context, r *http.Request, user *auth.User) (store.ResolveOrProvisionOIDCUserResult, error) {
	runtime := s.currentIdentityRuntime()
	if runtime == nil {
		return store.ResolveOrProvisionOIDCUserResult{}, errors.New("active identity runtime is unavailable")
	}
	return s.resolveOIDCUserProjectionForRuntime(ctx, r, user, runtime)
}

func (s *Server) resolveOIDCUserProjectionForRuntime(ctx context.Context, r *http.Request, user *auth.User, runtime *identityRequestRuntime) (store.ResolveOrProvisionOIDCUserResult, error) {
	if s.oidcUserResolver == nil {
		return store.ResolveOrProvisionOIDCUserResult{}, errors.New("OIDC user resolver is unavailable")
	}
	revision := runtime.Revision
	switch user.ContextType {
	case "platform":
		if revision.TenantMode != "multi_tenant" ||
			strings.TrimSpace(user.TenantID) != "" {
			return store.ResolveOrProvisionOIDCUserResult{},
				errors.New("platform OIDC context is incompatible with the active identity revision")
		}
	case "tenant":
		if strings.TrimSpace(user.TenantID) == "" {
			return store.ResolveOrProvisionOIDCUserResult{},
				errors.New("tenant OIDC context is missing its tenant")
		}
	default:
		return store.ResolveOrProvisionOIDCUserResult{},
			errors.New("OIDC context type is missing or invalid")
	}
	tenantName := ""
	tenantSlug := ""
	tenantMetadataStatus := ""
	tenantMetadataVersion := ""
	tenantMetadataETag := ""
	var tenantMetadataUpdatedAt time.Time
	if user.ContextType == "tenant" &&
		revision.TenantMode == "multi_tenant" {
		if runtime.TenantContext == nil {
			return store.ResolveOrProvisionOIDCUserResult{}, errors.New("tenant context runtime is unavailable")
		}
		var cached store.OIDCTenantBindingContext
		bindingDisabled := false
		if reader, ok := s.oidcUserResolver.(oidcTenantBindingReader); ok {
			cached, _ = reader.OIDCTenantBindingContext(ctx, revision.Issuer, revision.ApplicationID, user.TenantID)
			bindingDisabled = cached.ID != "" && cached.AccessStatus != "active"
			if !bindingDisabled && cached.MetadataStatus == "synced" && cached.NextSyncAt.After(time.Now()) {
				tenantName, tenantSlug = cached.DisplayName, cached.Slug
				tenantMetadataStatus, tenantMetadataVersion = "synced", cached.Version
				tenantMetadataETag, tenantMetadataUpdatedAt = cached.ETag, cached.MetadataUpdatedAt
			}
		}
		if tenantMetadataStatus == "synced" {
			return s.resolveOIDCUserProjectionWithTenantContext(ctx, r, user, revision,
				tenantName, tenantSlug, tenantMetadataStatus, tenantMetadataVersion, tenantMetadataETag, tenantMetadataUpdatedAt)
		}
		etag := cached.ETag
		if bindingDisabled {
			// A revoked binding can only be reactivated after a fresh positive
			// Tenant Context response. Do not let a stale 304 or cached profile
			// reopen access after reassignment.
			etag = ""
		}
		tenant, unchanged, err := runtime.TenantContext.Get(ctx, user.TenantID, etag)
		switch {
		case err == nil && unchanged && !bindingDisabled && cached.MetadataStatus == "synced":
			tenantName, tenantSlug = cached.DisplayName, cached.Slug
			tenantMetadataStatus, tenantMetadataVersion = "synced", cached.Version
			tenantMetadataETag, tenantMetadataUpdatedAt = cached.ETag, cached.MetadataUpdatedAt
		case err == nil && tenant.Active():
			tenantName = tenant.DisplayName
			tenantSlug = tenant.Slug
			tenantMetadataStatus = "synced"
			tenantMetadataVersion = tenant.Version
			tenantMetadataETag = tenant.ETag
			tenantMetadataUpdatedAt = tenant.UpdatedAt
		case err == nil, errors.Is(err, identity.ErrTenantContextNotFound):
			return store.ResolveOrProvisionOIDCUserResult{}, store.ErrOIDCTenantUnavailable
		case errors.Is(err, identity.ErrTenantContextUnavailable):
			if bindingDisabled {
				return store.ResolveOrProvisionOIDCUserResult{}, store.ErrOIDCTenantUnavailable
			}
			if cached.MetadataStatus == "synced" {
				tenantName, tenantSlug = cached.DisplayName, cached.Slug
				tenantMetadataStatus, tenantMetadataVersion = "synced", cached.Version
				tenantMetadataETag, tenantMetadataUpdatedAt = cached.ETag, cached.MetadataUpdatedAt
			} else {
				tenantName = oidcTenantPlaceholderName(user.TenantID)
				tenantMetadataStatus = "metadata_pending"
			}
		default:
			return store.ResolveOrProvisionOIDCUserResult{}, err
		}
	}
	return s.resolveOIDCUserProjectionWithTenantContext(ctx, r, user, revision,
		tenantName, tenantSlug, tenantMetadataStatus, tenantMetadataVersion, tenantMetadataETag, tenantMetadataUpdatedAt)
}

func (s *Server) resolveOIDCUserProjectionWithTenantContext(
	ctx context.Context,
	r *http.Request,
	user *auth.User,
	revision identity.Revision,
	tenantName, tenantSlug, tenantMetadataStatus, tenantMetadataVersion, tenantMetadataETag string,
	tenantMetadataUpdatedAt time.Time,
) (store.ResolveOrProvisionOIDCUserResult, error) {
	return s.oidcUserResolver.ResolveOrProvisionOIDCUser(ctx, store.ResolveOrProvisionOIDCUserInput{
		Issuer:                  revision.Issuer,
		ApplicationID:           revision.ApplicationID,
		Subject:                 user.ID,
		Username:                user.Username,
		DisplayName:             user.DisplayName,
		Email:                   user.Email,
		Phone:                   user.Phone,
		AvatarURL:               user.AvatarURL,
		Roles:                   user.Roles,
		ContextType:             user.ContextType,
		TenantID:                user.TenantID,
		TenantMode:              revision.TenantMode,
		TenantName:              tenantName,
		TenantSlug:              tenantSlug,
		TenantMetadataStatus:    tenantMetadataStatus,
		TenantMetadataVersion:   tenantMetadataVersion,
		TenantMetadataETag:      tenantMetadataETag,
		TenantMetadataUpdatedAt: tenantMetadataUpdatedAt,
		OIDCClientID:            user.OIDCClientID,
		GatewayTenantKey:        revision.LocalTenantKey,
		ProvisioningEnabled:     revision.JITEnabled,
		RequestIP:               limitAuditText(requestIP(r), 128),
		UserAgent:               limitAuditText(r.UserAgent(), 512),
	})
}

func oidcTenantPlaceholderName(tenantID string) string {
	compact := strings.ReplaceAll(strings.TrimSpace(tenantID), "-", "")
	if len(compact) > 8 {
		compact = compact[:8]
	}
	return "认证中心租户 " + compact
}

func (s *Server) writeOIDCUserResolutionError(w http.ResponseWriter, r *http.Request, err error) {
	switch {
	case errors.Is(err, store.ErrOIDCUserNotProvisioned):
		writeError(w, http.StatusForbidden, "该账号尚未开通 EasyAI Gateway", errorCodeGatewayUserNotProvisioned)
	case errors.Is(err, store.ErrOIDCUserDisabled):
		writeError(w, http.StatusForbidden, "该 Gateway 账号已停用，请联系管理员", errorCodeGatewayUserDisabled)
	case errors.Is(err, store.ErrOIDCTenantUnavailable):
		writeError(w, http.StatusServiceUnavailable, "Gateway 租户尚未就绪，请联系管理员", errorCodeGatewayTenantUnavailable)
	default:
		s.logger.ErrorContext(r.Context(), "resolve OIDC gateway user failed", "error", err, "path", r.URL.Path)
		writeError(w, http.StatusServiceUnavailable, "Gateway 账号初始化失败，请稍后重试", errorCodeGatewayProvisioningFailed)
	}
}

func limitAuditText(value string, limit int) string {
	value = strings.TrimSpace(value)
	runes := []rune(value)
	if limit > 0 && len(runes) > limit {
		return string(runes[:limit])
	}
	return value
}
