package middleware

import (
	"baron-sso-backend/internal/domain"
	"baron-sso-backend/internal/service"
	"log/slog"

	"github.com/gofiber/fiber/v2"
)

// RBACConfig defines the configuration for RBAC middleware
type RBACConfig struct {
	AllowedRoles []string
	AuthHandler  AuthProfileProvider
	KetoService  service.KetoService
}

// AuthProfileProvider는 미들웨어에서 사용자 정보를 조회하기 위한 최소 인터페이스입니다.
type AuthProfileProvider interface {
	GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
}

func setAuditUserContext(c *fiber.Ctx, profile *domain.UserProfileResponse) {
	if profile == nil || profile.ID == "" {
		return
	}
	if existingUserID, _ := c.Locals("user_id").(string); existingUserID != "" {
		return
	}
	c.Locals("user_id", profile.ID)
}

// RequireKetoPermission enforces permissions using Ory Keto (ReBAC)
func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber.Handler {
	return func(c *fiber.Ctx) error {
		profile, err := config.AuthHandler.GetEnrichedProfile(c)
		if err != nil {
			return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_keto)")
		}

		// Store profile in locals for further use in handlers
		c.Locals("user_profile", profile)
		setAuditUserContext(c, profile)

		role := domain.NormalizeRole(profile.Role)

		// Super Admin bypass
		if role == domain.RoleSuperAdmin {
			return c.Next()
		}

		// Get object ID from path (e.g., tenant ID)
		// Fix: For Tenant namespace, prioritize tenantId param if available
		objectID := ""
		if namespace == "Tenant" {
			objectID = c.Params("tenantId")
		}

		if objectID == "" {
			objectID = c.Params("id")
		}

		if objectID == "" {
			slog.Error("RBAC Keto check failed: missing object id", "path", c.Path())
			return errorJSON(c, fiber.StatusBadRequest, "missing object id for permission check")
		}

		slog.Info("Performing Keto permission check", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation)

		// Set tenant_id for audit logging if namespace is Tenant
		if namespace == "Tenant" {
			c.Locals("tenant_id", objectID)
		}

		// Check with Keto - add User: prefix to subject
		allowed, err := config.KetoService.CheckPermission(c.Context(), "User:"+profile.ID, namespace, objectID, relation)
		if err != nil {
			slog.Error("Keto service error", "error", err, "userID", profile.ID, "objectID", objectID)
			return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
		}

		if !allowed {
			slog.Warn("Keto permission denied", "userID", profile.ID, "userRole", role, "namespace", namespace, "objectID", objectID, "relation", relation, "X-Test-Role", c.Get("X-Test-Role"))
			return errorJSON(c, fiber.StatusForbidden, "forbidden: keto permission denied for "+namespace+":"+objectID)
		}

		return c.Next()
	}
}

// RequireRole enforces that the user has one of the allowed roles
func RequireRole(config RBACConfig) fiber.Handler {
	return func(c *fiber.Ctx) error {
		// Bypass if already authenticated via API Key
		if c.Locals("apiKeyName") != nil {
			return c.Next()
		}

		profile, err := config.AuthHandler.GetEnrichedProfile(c)
		if err != nil {
			return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_role): "+err.Error())
		}

		// Store profile in locals for further use in handlers
		c.Locals("user_profile", profile)
		setAuditUserContext(c, profile)

		userRole := domain.NormalizeRole(profile.Role)

		// Super Admin always has access
		if userRole == domain.RoleSuperAdmin {
			return c.Next()
		}

		// Check if user's role is in allowed roles
		roleAllowed := false
		for _, role := range config.AllowedRoles {
			if userRole == domain.NormalizeRole(role) {
				roleAllowed = true
				break
			}
		}

		if !roleAllowed {
			slog.Warn("RBAC access denied",
				"userID", profile.ID,
				"userRole", userRole,
				"allowedRoles", config.AllowedRoles,
				"path", c.Path(),
				"X-Test-Role", c.Get("X-Test-Role"),
			)
			return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions")
		}

		return c.Next()
	}
}

// RequireTenantMatch enforces that a Tenant Admin can only access their own tenant's data
func RequireTenantMatch(config RBACConfig) fiber.Handler {
	return func(c *fiber.Ctx) error {
		// Bypass if already authenticated via API Key (System-wide for now)
		if c.Locals("apiKeyName") != nil {
			return c.Next()
		}

		profile, err := config.AuthHandler.GetEnrichedProfile(c)
		if err != nil {
			return errorJSON(c, fiber.StatusUnauthorized, "unauthorized (trace:rbac_match)")
		}

		// Store profile in locals for further use in handlers
		c.Locals("user_profile", profile)
		setAuditUserContext(c, profile)

		userRole := domain.NormalizeRole(profile.Role)

		// Super Admin bypass
		if userRole == domain.RoleSuperAdmin {
			return c.Next()
		}

		// Since only Super Admin is maintained for tenant management, others are rejected here
		return errorJSON(c, fiber.StatusForbidden, "forbidden")
	}
}
