package api

import (
	"errors"
	"log"
	"net/http"
	"time"

	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/audit"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/auth"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/cloudprov"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/db"
	"gitea.hostxtra.co.uk/mrhid6/vantage/admin/internal/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/license"
	sharedmodels "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
	"gitea.hostxtra.co.uk/mrhid6/vantage/shared/provision"
	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"go.mongodb.org/mongo-driver/v2/bson"
)

// selfHostedRefusal is the one message every membership endpoint gives for a
// self-hosted instance. Their users live in their own deployment, which we
// cannot see and must not write to.
const selfHostedRefusal = "this install manages its own users; add them in Settings → Instance inside your Vantage install"

func listInstanceMembers(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	ctx := c.Request.Context()
	cur, err := db.Admin("instance_members").Find(ctx,
		bson.M{"instance_id": inst.InstanceID})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	members := []models.InstanceMember{}
	if err := cur.All(ctx, &members); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, members)
}

// grantInstanceMember projects an account person into a cloud instance.
func grantInstanceMember(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	if inst.Deployment != license.DeploymentCloud {
		c.JSON(http.StatusBadRequest, gin.H{"error": selfHostedRefusal})
		return
	}

	var body struct {
		UserID string `json:"user_id"`
		Role   string `json:"role"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || body.UserID == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "user_id is required"})
		return
	}
	if body.Role == "" {
		body.Role = sharedmodels.RoleMember
	}
	if !sharedmodels.ValidRole(body.Role) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be one of owner, admin, or member"})
		return
	}

	target, ok := accountUser(c, body.UserID)
	if !ok {
		return
	}
	if target.VerifiedAt == nil || target.PasswordHash == "" {
		// The projection copies a hash. An unverified invitee has no hash, so
		// the row would exist and be unusable — and an address nobody has
		// proven they control would hold a login inside a real instance.
		c.JSON(http.StatusConflict, gin.H{
			"error": "they have not accepted their invitation yet"})
		return
	}

	ctx := c.Request.Context()
	s := auth.Current(c)

	u, err := cloudprov.GrantUser(ctx, inst.InstanceID, target.Email,
		target.PasswordHash, body.Role, target.UserID)
	if err != nil {
		if errors.Is(err, provision.ErrEmailTaken) {
			c.JSON(http.StatusConflict, gin.H{
				"error": "that address already has a user inside this instance"})
			return
		}
		log.Printf("grant %s to %s: %v", target.Email, inst.InstanceID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not grant access"})
		return
	}

	m := models.InstanceMember{
		MemberID:       uuid.NewString(),
		AccountID:      inst.AccountID,
		InstanceID:     inst.InstanceID,
		CustomerUserID: target.UserID,
		ControlUserID:  u.UserID,
		Role:           body.Role,
		Email:          target.Email,
		CreatedAt:      time.Now().UTC(),
	}
	if _, err := db.Admin("instance_members").InsertOne(ctx, m); err != nil {
		// Unwind the projection: a control-plane login nobody on this side
		// records is a login nobody can revoke through the portal.
		if rErr := cloudprov.RevokeUser(ctx, inst.InstanceID, target.UserID); rErr != nil {
			log.Printf("grant: FAILED to unwind projection of %s in %s: %v",
				target.Email, inst.InstanceID, rErr)
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not grant access"})
		return
	}

	audit.Write(ctx, models.AuditEntry{
		Actor: s.Email, Action: "instance_member.granted", AccountID: s.AccountID,
		Target: inst.InstanceID, Detail: target.Email + " role=" + body.Role, IP: c.ClientIP()})
	c.JSON(http.StatusCreated, m)
}

// memberRow loads one membership on an instance the caller owns.
func memberRow(c *gin.Context, instanceID, customerUserID string) (*models.InstanceMember, bool) {
	var m models.InstanceMember
	if err := db.Admin("instance_members").FindOne(c.Request.Context(), bson.M{
		"instance_id":      instanceID,
		"customer_user_id": customerUserID,
	}).Decode(&m); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return nil, false
	}
	return &m, true
}

func updateInstanceMemberRole(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	if inst.Deployment != license.DeploymentCloud {
		c.JSON(http.StatusBadRequest, gin.H{"error": selfHostedRefusal})
		return
	}
	var body struct {
		Role string `json:"role"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || !sharedmodels.ValidRole(body.Role) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be one of owner, admin, or member"})
		return
	}
	m, ok := memberRow(c, inst.InstanceID, c.Param("uid"))
	if !ok {
		return
	}

	ctx := c.Request.Context()
	if m.Role == sharedmodels.RoleOwner && body.Role != sharedmodels.RoleOwner {
		others, err := cloudprov.CountOtherOwners(ctx, inst.InstanceID, m.CustomerUserID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if others == 0 {
			c.JSON(http.StatusConflict, gin.H{
				"error": "this is the instance's last owner; make someone else an owner first"})
			return
		}
	}

	if err := cloudprov.SetMemberRole(ctx, inst.InstanceID, m.CustomerUserID, body.Role); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not change their role"})
		return
	}
	if _, err := db.Admin("instance_members").UpdateOne(ctx,
		bson.M{"member_id": m.MemberID},
		bson.M{"$set": bson.M{"role": body.Role}}); err != nil {
		log.Printf("member role: control plane updated but member row %s did not: %v", m.MemberID, err)
	}

	s := auth.Current(c)
	audit.Write(ctx, models.AuditEntry{
		Actor: s.Email, Action: "instance_member.role_changed", AccountID: s.AccountID,
		Target: inst.InstanceID, Detail: m.Email + " role=" + body.Role, IP: c.ClientIP()})
	c.JSON(http.StatusOK, gin.H{"ok": true})
}

func revokeInstanceMember(c *gin.Context) {
	inst, ok := ownedInstance(c, c.Param("id"))
	if !ok {
		return
	}
	if inst.Deployment != license.DeploymentCloud {
		c.JSON(http.StatusBadRequest, gin.H{"error": selfHostedRefusal})
		return
	}
	m, ok := memberRow(c, inst.InstanceID, c.Param("uid"))
	if !ok {
		return
	}

	ctx := c.Request.Context()
	if m.Role == sharedmodels.RoleOwner {
		others, err := cloudprov.CountOtherOwners(ctx, inst.InstanceID, m.CustomerUserID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if others == 0 {
			c.JSON(http.StatusConflict, gin.H{
				"error": "this is the instance's last owner; make someone else an owner first"})
			return
		}
	}

	if err := cloudprov.RevokeUser(ctx, inst.InstanceID, m.CustomerUserID); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not revoke access"})
		return
	}
	if _, err := db.Admin("instance_members").DeleteOne(ctx,
		bson.M{"member_id": m.MemberID}); err != nil {
		log.Printf("revoke: control-plane user deleted but member row %s remains: %v", m.MemberID, err)
	}

	s := auth.Current(c)
	audit.Write(ctx, models.AuditEntry{
		Actor: s.Email, Action: "instance_member.revoked", AccountID: s.AccountID,
		Target: inst.InstanceID, Detail: m.Email, IP: c.ClientIP()})
	c.JSON(http.StatusOK, gin.H{"revoked": true})
}
