package middleware

import (
	"database/sql"
	"net/http"
	"strings"
	"sync"

	"github.com/gin-gonic/gin"
	"github.com/nexuscrm/backend/pkg/auth"
	"github.com/nexuscrm/shared/pkg/constants"
)

// ScopeRegistry caches custom scope definitions for the ScopeEnforcer.
type ScopeRegistry struct {
	mu     sync.RWMutex
	scopes map[string]*CustomScope // name -> scope definition
}

// CustomScope holds a custom OAuth scope definition loaded from _System_OAuthCustomScope.
type CustomScope struct {
	Name               string
	CustomPermissionId string
	IsActive           bool
}

// NewScopeRegistry creates an empty ScopeRegistry.
func NewScopeRegistry() *ScopeRegistry {
	return &ScopeRegistry{
		scopes: make(map[string]*CustomScope),
	}
}

// LoadFromDB refreshes the custom scope cache from _System_OAuthCustomScope table.
func (r *ScopeRegistry) LoadFromDB(db *sql.DB) error {
	rows, err := db.Query("SELECT Name, CustomPermissionId, IsActive FROM `_System_OAuthCustomScope` WHERE IsActive = 1")
	if err != nil {
		return err
	}
	defer rows.Close()

	scopes := make(map[string]*CustomScope)
	for rows.Next() {
		var s CustomScope
		if err := rows.Scan(&s.Name, &s.CustomPermissionId, &s.IsActive); err != nil {
			continue
		}
		scopes[s.Name] = &s
	}

	r.mu.Lock()
	r.scopes = scopes
	r.mu.Unlock()
	return nil
}

// IsCustomScope checks if the given scope name is a registered custom scope.
func (r *ScopeRegistry) IsCustomScope(name string) bool {
	r.mu.RLock()
	defer r.mu.RUnlock()
	s, ok := r.scopes[name]
	return ok && s.IsActive
}

// ScopeEnforcer checks if the current session's OAuth scope allows access
// to the requested route. Placed AFTER requireAuth and BEFORE permLoader.
//
// Sessions without scope (direct login, Google SSO, legacy tokens) get full
// access — backward compatible. Only tokens with explicit scope are restricted.
//
// An optional ScopeRegistry enables custom scope lookups after built-in checks.
func ScopeEnforcer(registry ...*ScopeRegistry) gin.HandlerFunc {
	var reg *ScopeRegistry
	if len(registry) > 0 {
		reg = registry[0]
	}
	return func(c *gin.Context) {
		// Extract user from context (set by requireAuth)
		userVal, exists := c.Get(constants.ContextKeyUser)
		if !exists {
			c.Next()
			return
		}
		user, ok := userVal.(auth.UserSession)
		if !ok {
			c.Next()
			return
		}

		// No scope = direct login or legacy token → full access
		if user.Scope == "" {
			c.Next()
			return
		}

		scopes := strings.Fields(user.Scope)

		if !scopeCoversRoute(scopes, c, reg) {
			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
				"errorCode": "INSUFFICIENT_SCOPE",
				"message":   "This session is not valid for use with the requested API",
			})
			return
		}

		// Set ConnectedAppId in context for audit logging
		if user.ConnectedAppId != "" {
			c.Set("connected_app_id", user.ConnectedAppId)
		}

		c.Next()
	}
}

// scopeCoversRoute checks if any scope in the set covers the current route.
// Uses c.FullPath() (gin's route pattern) not c.Request.URL.Path (actual URL)
// to prevent substring bypass attacks (e.g., SObject named "connect").
func scopeCoversRoute(scopes []string, c *gin.Context, reg *ScopeRegistry) bool {
	fullPath := c.FullPath()
	for _, s := range scopes {
		switch s {
		case "api", "full":
			return true
		case "api.read":
			if c.Request.Method == "GET" || c.Request.Method == "HEAD" {
				return true
			}
		case "chatter_api":
			if strings.Contains(fullPath, "/connect/") || strings.HasSuffix(fullPath, "/connect") {
				return true
			}
		case "openid":
			if strings.Contains(fullPath, "/userinfo") || strings.Contains(fullPath, "/id/:orgId") {
				return true
			}
		default:
			// Custom scopes are registered but do NOT grant blanket route access.
			// They are validated at token issuance (computeEffectiveScope) to ensure
			// the token carries only scopes the app declared. At the route level,
			// custom scopes require explicit route-to-scope mapping (Phase 4).
			// Until then, a custom scope alone does not satisfy any route check.
			continue
		}
	}
	return false
}
