package handlers

import (
	"database/sql"
	"errors"
	"net/http"

	"github.com/gin-gonic/gin"

	"switchboard-core/auth"
	"switchboard-core/database"
	"switchboard-core/models"
	"switchboard-core/notifications"
	"switchboard-core/store"
)

// ── Request types ───────────────────────────

type createGroupRequest struct {
	Name        string   `json:"name" binding:"required,min=1,max=200"`
	Description string   `json:"description,omitempty"`
	Scope       string   `json:"scope" binding:"required,oneof=global team"`
	TeamID      *string  `json:"team_id,omitempty"`
	Permissions []string `json:"permissions,omitempty"`
}

type updateGroupRequest struct {
	Name        *string   `json:"name,omitempty"`
	Description *string   `json:"description,omitempty"`
	Permissions *[]string `json:"permissions,omitempty"`
}

type addGroupMemberRequest struct {
	UserID string `json:"user_id" binding:"required"`
}

type setResourceGrantRequest struct {
	GrantScope    string   `json:"grant_scope" binding:"required,oneof=team_only global groups"`
	GrantedGroups []string `json:"granted_groups,omitempty"` // required when grant_scope=groups
}

// ── Handler ─────────────────────────────────

type GroupHandler struct {
	stores store.Stores
}

func NewGroupHandler(s store.Stores) *GroupHandler {
	return &GroupHandler{stores: s}
}

// ── Admin: List All Groups ──────────────────

func (h *GroupHandler) ListGroups(c *gin.Context) {
	groups, err := h.stores.Groups.ListAll(c.Request.Context())
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	if groups == nil {
		groups = []models.Group{}
	}
	c.JSON(http.StatusOK, gin.H{"data": groups})
}

// ── Admin: Create Group ─────────────────────

func (h *GroupHandler) CreateGroup(c *gin.Context) {
	actorID := getUserID(c)

	var req createGroupRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Validate scope constraints
	if req.Scope == models.ScopeTeam && (req.TeamID == nil || *req.TeamID == "") {
		c.JSON(http.StatusBadRequest, gin.H{"error": "team_id required for team-scoped groups"})
		return
	}
	if req.Scope == models.ScopeGlobal && req.TeamID != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "team_id must be null for global groups"})
		return
	}

	g := &models.Group{
		Name:        req.Name,
		Description: req.Description,
		Scope:       req.Scope,
		TeamID:      req.TeamID,
		CreatedBy:   &actorID,
		Permissions: req.Permissions,
	}

	if err := h.stores.Groups.Create(c.Request.Context(), g); err != nil {
		if database.IsUniqueViolation(err) {
			c.JSON(http.StatusConflict, gin.H{"error": "group name already exists in this scope"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create group"})
		return
	}

	c.JSON(http.StatusCreated, g)
	AuditLog(h.stores.Audit, c, "group.create", "group", g.ID, map[string]interface{}{
		"name": g.Name, "scope": g.Scope,
	})
}

// ── Admin: Get Group ────────────────────────

func (h *GroupHandler) GetGroup(c *gin.Context) {
	id := c.Param("id")

	g, err := h.stores.Groups.GetByID(c.Request.Context(), id)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}

	c.JSON(http.StatusOK, g)
}

// ── Admin: Update Group ─────────────────────

func (h *GroupHandler) UpdateGroup(c *gin.Context) {
	id := c.Param("id")

	var req updateGroupRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	patch := models.GroupPatch{
		Name:        req.Name,
		Description: req.Description,
		Permissions: req.Permissions,
	}

	// Validate permissions if provided
	if patch.Permissions != nil {
		valid := auth.AllPermissions
		validSet := make(map[string]bool, len(valid))
		for _, p := range valid {
			validSet[p] = true
		}
		for _, p := range *patch.Permissions {
			if !validSet[p] {
				c.JSON(http.StatusBadRequest, gin.H{"error": "unknown permission: " + p})
				return
			}
		}
	}

	err := h.stores.Groups.Update(c.Request.Context(), id, patch)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
		return
	}
	if err != nil {
		if database.IsUniqueViolation(err) {
			c.JSON(http.StatusConflict, gin.H{"error": "group name already exists in this scope"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "update failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
	AuditLog(h.stores.Audit, c, "group.update", "group", id, nil)
}

// ── Admin: Delete Group ─────────────────────

func (h *GroupHandler) DeleteGroup(c *gin.Context) {
	id := c.Param("id")

	err := h.stores.Groups.Delete(c.Request.Context(), id)
	if errors.Is(err, sql.ErrNoRows) {
		c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
		return
	}
	if errors.Is(err, store.ErrSystemGroup) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "system groups cannot be deleted"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
	AuditLog(h.stores.Audit, c, "group.delete", "group", id, nil)
}

// ── Members: List ───────────────────────────

func (h *GroupHandler) ListMembers(c *gin.Context) {
	groupID := c.Param("id")

	members, err := h.stores.Groups.ListMembers(c.Request.Context(), groupID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	if members == nil {
		members = []models.GroupMember{}
	}
	c.JSON(http.StatusOK, gin.H{"data": members})
}

// ── Members: Add ────────────────────────────

func (h *GroupHandler) AddMember(c *gin.Context) {
	groupID := c.Param("id")
	actorID := getUserID(c)

	var req addGroupMemberRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Verify group exists
	group, err := h.stores.Groups.GetByID(c.Request.Context(), groupID)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "group not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to look up group"})
		return
	}

	if err := h.stores.Groups.AddMember(c.Request.Context(), groupID, req.UserID, actorID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to add member"})
		return
	}

	// Notify the added user (v0.20.0)
	if svc := notifications.Default(); svc != nil {
		notifications.NotifyGroupMemberAdded(svc, req.UserID, groupID, group.Name)
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
	AuditLog(h.stores.Audit, c, "group.member.add", "group", groupID, map[string]interface{}{
		"user_id": req.UserID,
	})
}

// ── Members: Remove ─────────────────────────

func (h *GroupHandler) RemoveMember(c *gin.Context) {
	groupID := c.Param("id")
	userID := c.Param("userId")

	// Fetch group name for notification before removal
	group, _ := h.stores.Groups.GetByID(c.Request.Context(), groupID)

	err := h.stores.Groups.RemoveMember(c.Request.Context(), groupID, userID)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "member not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to remove member"})
		return
	}

	// Notify the removed user (v0.20.0)
	if svc := notifications.Default(); svc != nil && group != nil {
		notifications.NotifyGroupMemberRemoved(svc, userID, groupID, group.Name)
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
	AuditLog(h.stores.Audit, c, "group.member.remove", "group", groupID, map[string]interface{}{
		"user_id": userID,
	})
}

// ── User: My Groups ─────────────────────────

func (h *GroupHandler) MyGroups(c *gin.Context) {
	userID := getUserID(c)

	groups, err := h.stores.Groups.ListForUser(c.Request.Context(), userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	if groups == nil {
		groups = []models.Group{}
	}
	c.JSON(http.StatusOK, gin.H{"data": groups})
}

// ── Team Admin: List Team Groups ────────────

func (h *GroupHandler) ListTeamGroups(c *gin.Context) {
	teamID := getTeamID(c)

	groups, err := h.stores.Groups.ListForTeam(c.Request.Context(), teamID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	if groups == nil {
		groups = []models.Group{}
	}
	c.JSON(http.StatusOK, gin.H{"data": groups})
}

// ── Resource Grants ─────────────────────────

// GetResourceGrant returns the grant for a resource.
// GET /api/v1/admin/grants/:type/:id
func (h *GroupHandler) GetResourceGrant(c *gin.Context) {
	resourceType := c.Param("type")
	resourceID := c.Param("id")

	grant, err := h.stores.ResourceGrants.Get(c.Request.Context(), resourceType, resourceID)
	if err == sql.ErrNoRows {
		// No grant = default team_only behavior
		c.JSON(http.StatusOK, gin.H{
			"resource_type":  resourceType,
			"resource_id":    resourceID,
			"grant_scope":    models.GrantScopeTeamOnly,
			"granted_groups": []string{},
		})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "query failed"})
		return
	}
	c.JSON(http.StatusOK, grant)
}

// SetResourceGrant creates or updates a grant for a resource.
// PUT /api/v1/admin/grants/:type/:id
func (h *GroupHandler) SetResourceGrant(c *gin.Context) {
	resourceType := c.Param("type")
	resourceID := c.Param("id")
	actorID := getUserID(c)

	var req setResourceGrantRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// Validate: groups scope requires at least one group
	if req.GrantScope == models.GrantScopeGroups && len(req.GrantedGroups) == 0 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "granted_groups required when grant_scope is 'groups'"})
		return
	}

	// team_only = delete any existing grant (reverts to default scope behavior)
	if req.GrantScope == models.GrantScopeTeamOnly {
		_ = h.stores.ResourceGrants.Delete(c.Request.Context(), resourceType, resourceID)
		c.JSON(http.StatusOK, gin.H{"ok": true})
		AuditLog(h.stores.Audit, c, "grant.revoke", resourceType, resourceID, nil)
		return
	}

	grant := &models.ResourceGrant{
		ResourceType:  resourceType,
		ResourceID:    resourceID,
		GrantScope:    req.GrantScope,
		GrantedGroups: req.GrantedGroups,
		CreatedBy:     actorID,
	}

	if err := h.stores.ResourceGrants.Set(c.Request.Context(), grant); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to set grant"})
		return
	}

	c.JSON(http.StatusOK, grant)
	AuditLog(h.stores.Audit, c, "grant.set", resourceType, resourceID, map[string]interface{}{
		"grant_scope": req.GrantScope,
	})
}

// DeleteResourceGrant removes a grant for a resource.
// DELETE /api/v1/admin/grants/:type/:id
func (h *GroupHandler) DeleteResourceGrant(c *gin.Context) {
	resourceType := c.Param("type")
	resourceID := c.Param("id")

	err := h.stores.ResourceGrants.Delete(c.Request.Context(), resourceType, resourceID)
	if err == sql.ErrNoRows {
		c.JSON(http.StatusNotFound, gin.H{"error": "grant not found"})
		return
	}
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "delete failed"})
		return
	}

	c.JSON(http.StatusOK, gin.H{"ok": true})
	AuditLog(h.stores.Audit, c, "grant.delete", resourceType, resourceID, nil)
}

// ListPermissions returns all valid permission strings.
// GET /api/v1/admin/permissions
func (h *GroupHandler) ListPermissions(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{"permissions": auth.AllPermissions})
}

// GetUserPermissions returns the effective permissions for a given user.
// GET /api/v1/admin/users/:id/permissions
func (h *GroupHandler) GetUserPermissions(c *gin.Context) {
	userID := c.Param("id")
	ctx := c.Request.Context()

	perms, err := auth.ResolvePermissions(ctx, h.stores, userID)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to resolve permissions"})
		return
	}

	// Convert map to sorted slice for stable output
	list := make([]string, 0, len(perms))
	for p := range perms {
		list = append(list, p)
	}

	// Contributing groups: explicit membership + Everyone
	groupIDs, _ := h.stores.Groups.GetUserGroupIDs(ctx, userID)
	if groupIDs == nil {
		groupIDs = []string{}
	}
	// Everyone group always contributes
	groupIDs = append(groupIDs, auth.EveryoneGroupID)

	c.JSON(http.StatusOK, gin.H{"permissions": list, "user_id": userID, "groups": groupIDs})
}
