// Package auth provides authentication and authorization functionality
package auth

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"log"
	"net/http"
	"os"
	"strings"
	"time"
)

// User represents an authenticated user
type User struct {
	Permissions map[string]bool `json:"permissions"`
	Name        string          `json:"name"`
	Roles       []string        `json:"roles"`
	Admin       bool            `json:"admin"`
}

// ExtractAPIKeyFromRequest extracts an API key from the standard headers.
func ExtractAPIKeyFromRequest(r *http.Request) string {
	apiKey := r.Header.Get("X-API-Key")
	if apiKey != "" {
		return apiKey
	}

	authHeader := r.Header.Get("Authorization")
	if strings.HasPrefix(authHeader, "Bearer ") {
		return strings.TrimPrefix(authHeader, "Bearer ")
	}

	return ""
}

// APIKeyHash represents a SHA256 hash of an API key
type APIKeyHash string

// APIKeyEntry represents an API key configuration
type APIKeyEntry struct {
	Permissions map[string]bool `yaml:"permissions,omitempty"`
	Hash        APIKeyHash      `yaml:"hash"`
	Salt        string          `yaml:"salt,omitempty"`
	Algorithm   string          `yaml:"algorithm,omitempty"`
	Roles       []string        `yaml:"roles,omitempty"`
	Admin       bool            `yaml:"admin"`
}

// Username represents a user identifier
type Username string

// Config represents the authentication configuration
type Config struct {
	APIKeys map[Username]APIKeyEntry `yaml:"api_keys"`
	Enabled bool                     `yaml:"enabled"`
}

// Store interface for different authentication backends
type Store interface {
	ValidateAPIKey(ctx context.Context, key string) (*User, error)
	CreateAPIKey(
		ctx context.Context,
		userID string,
		keyHash string,
		admin bool,
		roles []string,
		permissions map[string]bool,
		expiresAt *time.Time,
	) error
	RevokeAPIKey(ctx context.Context, userID string) error
	ListUsers(ctx context.Context) ([]UserInfo, error)
}

// contextKey is the type for context keys
type contextKey string

const userContextKey = contextKey("user")

// UserInfo represents user information from authentication store
type UserInfo struct {
	Created time.Time  `json:"created"`
	Expires *time.Time `json:"expires,omitempty"`
	Revoked *time.Time `json:"revoked,omitempty"`
	UserID  string     `json:"user_id"`
	KeyHash string     `json:"key_hash"`
	Admin   bool       `json:"admin"`
}

// ValidateAPIKey validates an API key and returns user information
// Supports both legacy SHA256 and modern Argon2id hashing
func (c *Config) ValidateAPIKey(key string) (*User, error) {
	if !c.Enabled {
		// Auth disabled - return default admin user for development
		return &User{Name: "default", Admin: true}, nil
	}

	for username, entry := range c.APIKeys {
		// Build HashedKey from entry
		stored := &HashedKey{
			Hash:      string(entry.Hash),
			Salt:      entry.Salt,
			Algorithm: HashAlgorithm(entry.Algorithm),
		}

		// Verify using appropriate algorithm
		match, err := VerifyAPIKey(key, stored)
		if err != nil {
			// Log error but continue to next key (don't leak info)
			continue
		}

		if match {
			// Build user with role and permission inheritance
			user := &User{
				Name:        string(username),
				Admin:       entry.Admin,
				Roles:       entry.Roles,
				Permissions: make(map[string]bool),
			}

			// Copy explicit permissions
			for perm, value := range entry.Permissions {
				user.Permissions[perm] = value
			}

			// Add role-based permissions
			rolePerms := getRolePermissions(entry.Roles)
			for perm, value := range rolePerms {
				if _, exists := user.Permissions[perm]; !exists {
					user.Permissions[perm] = value
				}
			}

			// Admin gets all permissions
			if entry.Admin {
				user.Permissions["*"] = true
			}

			return user, nil
		}
	}

	return nil, fmt.Errorf("invalid API key")
}

// ValidateAPIKeyHash validates a pre-hashed API key and returns user information
func (c *Config) ValidateAPIKeyHash(hash []byte) (*User, error) {
	if !c.Enabled {
		// Auth disabled - return default admin user for development
		return &User{Name: "default", Admin: true}, nil
	}
	if len(hash) != 16 {
		return nil, fmt.Errorf("invalid api key hash length: %d", len(hash))
	}

	for username, entry := range c.APIKeys {
		stored := strings.TrimSpace(string(entry.Hash))
		if stored == "" {
			continue
		}
		storedBytes, err := hex.DecodeString(stored)
		if err != nil {
			continue
		}
		if len(storedBytes) != sha256.Size {
			continue
		}
		if string(storedBytes[:16]) == string(hash) {
			// Build user with role and permission inheritance
			user := &User{
				Name:        string(username),
				Admin:       entry.Admin,
				Roles:       entry.Roles,
				Permissions: make(map[string]bool),
			}

			// Copy explicit permissions
			for perm, value := range entry.Permissions {
				user.Permissions[perm] = value
			}

			// Add role-based permissions
			rolePerms := getRolePermissions(entry.Roles)
			for perm, value := range rolePerms {
				if _, exists := user.Permissions[perm]; !exists {
					user.Permissions[perm] = value
				}
			}

			// Admin gets all permissions
			if entry.Admin {
				user.Permissions["*"] = true
			}

			return user, nil
		}
	}

	return nil, fmt.Errorf("invalid api key")
}

// AuthMiddleware creates HTTP middleware for API key authentication
func (c *Config) AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !c.Enabled {
			if os.Getenv("FETCH_ML_ALLOW_INSECURE_AUTH") != "1" || os.Getenv("FETCH_ML_DEBUG") != "1" {
				http.Error(w, "Unauthorized: Authentication disabled", http.StatusUnauthorized)
				return
			}
			log.Println(
				"WARNING: Insecure authentication bypass enabled: FETCH_ML_ALLOW_INSECURE_AUTH=1 " +
					"and FETCH_ML_DEBUG=1; do NOT use this configuration in production.",
			)
			ctx := context.WithValue(r.Context(), userContextKey, &User{Name: "default", Admin: true})
			next.ServeHTTP(w, r.WithContext(ctx))
			return
		}

		// Only accept API key from header - no query parameters for security
		apiKey := r.Header.Get("X-API-Key")
		if apiKey == "" {
			http.Error(w, "Unauthorized: Missing API key in X-API-Key header", http.StatusUnauthorized)
			return
		}

		user, err := c.ValidateAPIKey(apiKey)
		if err != nil {
			http.Error(w, "Unauthorized: Invalid API key", http.StatusUnauthorized)
			return
		}

		// Add user to context
		ctx := context.WithValue(r.Context(), userContextKey, user)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

// GetUserFromContext retrieves user from request context
func GetUserFromContext(ctx context.Context) *User {
	if user, ok := ctx.Value(userContextKey).(*User); ok {
		return user
	}
	return nil
}

func WithUserContext(ctx context.Context, user *User) context.Context {
	return context.WithValue(ctx, userContextKey, user)
}

// RequireAdmin creates middleware that requires admin privileges
func RequireAdmin(next http.Handler) http.Handler {
	return RequirePermission("system:admin")(next)
}

// RequirePermission creates middleware that requires specific permission
func RequirePermission(permission string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			user := GetUserFromContext(r.Context())
			if user == nil {
				http.Error(w, "Unauthorized: No user context", http.StatusUnauthorized)
				return
			}

			if !user.HasPermission(permission) {
				http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
				return
			}

			next.ServeHTTP(w, r)
		})
	}
}

// HasPermission checks if user has a specific permission
func (u *User) HasPermission(permission string) bool {
	// Wildcard permission grants all access
	if u.Permissions["*"] {
		return true
	}

	// Direct permission check
	if u.Permissions[permission] {
		return true
	}

	// Hierarchical permission check (e.g., "jobs:create" matches "jobs")
	parts := strings.Split(permission, ":")
	for i := 1; i < len(parts); i++ {
		partial := strings.Join(parts[:i], ":")
		if u.Permissions[partial] {
			return true
		}
	}

	return false
}

// HasRole checks if user has a specific role
func (u *User) HasRole(role string) bool {
	for _, userRole := range u.Roles {
		if userRole == role {
			return true
		}
	}
	return false
}

// getRolePermissions returns permissions for given roles
func getRolePermissions(roles []string) map[string]bool {
	permissions := make(map[string]bool)

	// Use YAML permission manager if available
	if pm := GetGlobalPermissionManager(); pm != nil && pm.loaded {
		for _, role := range roles {
			rolePerms := pm.GetRolePermissions(role)
			for perm, value := range rolePerms {
				permissions[perm] = value
			}
		}
		return permissions
	}

	// Fallback to inline permissions
	rolePermissions := map[string]map[string]bool{
		"admin": {"*": true},
		"data_scientist": {
			"jobs:create": true, "jobs:read": true, "jobs:update": true,
			"data:read": true, "models:read": true,
		},
		"data_engineer": {
			"data:create": true, "data:read": true, "data:update": true, "data:delete": true,
		},
		"viewer": {
			"jobs:read": true, "data:read": true, "models:read": true, "metrics:read": true,
		},
		"operator": {
			"jobs:read": true, "jobs:update": true, "metrics:read": true, "system:read": true,
		},
	}

	for _, role := range roles {
		if rolePerms, exists := rolePermissions[role]; exists {
			for perm, value := range rolePerms {
				permissions[perm] = value
			}
		}
	}

	return permissions
}

// GenerateAPIKey creates a new API key with Argon2id hashing.
// Returns plaintext key (show once) and the entry for storage.
func GenerateNewAPIKey(admin bool, roles []string, permissions map[string]bool) (plaintext string, entry APIKeyEntry, err error) {
	plaintext, hashed, err := GenerateAPIKey()
	if err != nil {
		return "", APIKeyEntry{}, err
	}

	entry = APIKeyEntry{
		Hash:        APIKeyHash(hashed.Hash),
		Salt:        hashed.Salt,
		Algorithm:   string(hashed.Algorithm),
		Admin:       admin,
		Roles:       roles,
		Permissions: permissions,
	}

	return plaintext, entry, nil
}

// isHex checks if a string contains only hex characters
func isHex(s string) bool {
	for _, c := range s {
		if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
			return false
		}
	}
	return len(s) > 0
}
