package controllers

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"

	"github.com/gofiber/fiber/v3"
	"gorm.io/gorm"

	"server/internal/auth"
	"server/internal/models"
)

type RoleConfig struct {
	Roles       map[string][]string `json:"roles"`
	Permissions map[string][]string `json:"permissions"`
	Endpoints   map[string]string   `json:"endpoints"`
}

type RoleResolver struct {
	roleClosure  map[string]map[string]struct{}
	permMap      map[string]map[string]struct{}
	endpointPerm map[string]string
}

func LoadRoleConfig(path string) (*RoleResolver, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read role config: %w", err)
	}
	var cfg RoleConfig
	if err := json.Unmarshal(data, &cfg); err != nil {
		return nil, fmt.Errorf("parse role config: %w", err)
	}
	res := &RoleResolver{
		roleClosure:  make(map[string]map[string]struct{}),
		permMap:      make(map[string]map[string]struct{}),
		endpointPerm: make(map[string]string),
	}

	for role := range cfg.Roles {
		res.roleClosure[role] = make(map[string]struct{})
	}

	// Compute role closure (role implies itself).
	var dfs func(string, map[string]struct{})
	dfs = func(role string, seen map[string]struct{}) {
		if _, ok := seen[role]; ok {
			return
		}
		seen[role] = struct{}{}
		if implied, ok := cfg.Roles[role]; ok {
			for _, r := range implied {
				dfs(r, seen)
			}
		}
	}
	for role := range cfg.Roles {
		set := make(map[string]struct{})
		set[role] = struct{}{}
		dfs(role, set)
		res.roleClosure[role] = set
	}

	// Build permission map including inherited permissions.
	for role := range cfg.Roles {
		res.permMap[role] = make(map[string]struct{})
	}
	for role := range cfg.Roles {
		closure := res.roleClosure[role]
		for implied := range closure {
			for _, p := range cfg.Permissions[implied] {
				res.permMap[role][p] = struct{}{}
			}
		}
	}

	// Normalise endpoints to "METHOD /path".
	for key, perm := range cfg.Endpoints {
		parts := strings.SplitN(key, " ", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid endpoint key %q", key)
		}
		method := strings.TrimSpace(strings.ToUpper(parts[0]))
		path := strings.TrimSpace(parts[1])
		res.endpointPerm[method+" "+path] = perm
	}

	return res, nil
}

func (r *RoleResolver) HasRole(userRoles []string, required string) bool {
	for _, ur := range userRoles {
		if closure, ok := r.roleClosure[ur]; ok {
			if _, present := closure[required]; present {
				return true
			}
		}
	}
	return false
}

func (r *RoleResolver) HasPermission(userRoles []string, perm string) bool {
	for _, ur := range userRoles {
		if perms, ok := r.permMap[ur]; ok {
			if _, present := perms[perm]; present {
				return true
			}
		}
	}
	return false
}

func (r *RoleResolver) PermissionForEndpoint(method, path string) (string, bool) {
	key := strings.ToUpper(method) + " " + path
	perm, ok := r.endpointPerm[key]
	return perm, ok
}

func (r *RoleResolver) RoleDefined(role string) bool {
	_, ok := r.roleClosure[role]
	return ok
}

// RequireRole ensures the authenticated user has the specified role (with inheritance).
func RequireRole(resolver *RoleResolver, role string) fiber.Handler {
	return func(c fiber.Ctx) error {
		claims, ok := auth.ClaimsFromCtx(c)
		if !ok {
			return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
		}

		db, err := dbFromCtx(c)
		if err != nil {
			return err
		}

		var user models.User
		if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusUnauthorized, "user not found")
			}
			return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
		}

		if !resolver.HasRole(user.Roles, role) {
			return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
		}
		return c.Next()
	}
}

// RequirePermission ensures the authenticated user has the given permission.
func RequirePermission(resolver *RoleResolver, perm string) fiber.Handler {
	return func(c fiber.Ctx) error {
		claims, ok := auth.ClaimsFromCtx(c)
		if !ok {
			return fiber.NewError(fiber.StatusUnauthorized, "missing claims")
		}

		db, err := dbFromCtx(c)
		if err != nil {
			return err
		}

		var user models.User
		if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusUnauthorized, "user not found")
			}
			return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
		}

		if !resolver.HasPermission(user.Roles, perm) {
			return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
		}
		return c.Next()
	}
}

// RequireEndpointPermission enforces permission mapping defined in role config.
// If the endpoint is not configured, or mapped to "*", it allows the request.
func RequireEndpointPermission(resolver *RoleResolver, authService *auth.Service) fiber.Handler {
	return func(c fiber.Ctx) error {
		perm, ok := resolver.PermissionForEndpoint(c.Method(), c.Path())
		if !ok || perm == "*" {
			return c.Next()
		}

		tokenString := c.Get("Auth-Token")
		if tokenString == "" {
			return fiber.NewError(fiber.StatusUnauthorized, "missing token header")
		}

		claims, err := authService.ValidateAccessToken(tokenString)
		if err != nil {
			return fiber.NewError(fiber.StatusUnauthorized, err.Error())
		}
		c.Locals("authClaims", claims)

		db, err := dbFromCtx(c)
		if err != nil {
			return err
		}

		var user models.User
		if err := db.Select("roles").Where("email = ?", claims.Username).First(&user).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return fiber.NewError(fiber.StatusUnauthorized, "user not found")
			}
			return fiber.NewError(fiber.StatusInternalServerError, "failed to load user roles")
		}

		if !resolver.HasPermission(user.Roles, perm) {
			return fiber.NewError(fiber.StatusForbidden, "insufficient permissions")
		}

		return c.Next()
	}
}
