package api

import (
	"fmt"
	"log"
	"net/http"
	"strings"
	"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"
	sharedmodels "gitea.hostxtra.co.uk/mrhid6/vantage/shared/models"
	"github.com/gin-gonic/gin"
	"go.mongodb.org/mongo-driver/v2/bson"
	"golang.org/x/crypto/bcrypt"
)

// listAccountUsers returns the account's people, newest last.
//
// Any signed-in member may read this. Knowing who your colleagues are is not
// privileged, and hiding it would make the members panel unusable for the
// people it is meant to inform.
func listAccountUsers(c *gin.Context) {
	s := auth.Current(c)
	ctx := c.Request.Context()
	cur, err := db.Admin("customer_users").Find(ctx, bson.M{"account_id": s.AccountID})
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	users := []models.CustomerUser{}
	if err := cur.All(ctx, &users); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, users)
}

// inviteAccountUser adds a person to the account.
//
// It never sets a password: see CreateInvitedUser. Only an owner may invite
// another owner, mirroring the control plane's own rule that an admin cannot
// mint someone with more power than themselves.
func inviteAccountUser(c *gin.Context) {
	var body struct {
		Email string `json:"email"`
		Role  string `json:"role"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "email is required"})
		return
	}
	email := strings.ToLower(strings.TrimSpace(body.Email))
	if email == "" {
		c.JSON(http.StatusBadRequest, gin.H{"error": "email is required"})
		return
	}
	if body.Role == "" {
		body.Role = models.AccountRoleMember
	}
	if !models.ValidAccountRole(body.Role) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be one of owner, admin, or member"})
		return
	}
	me := auth.CurrentUser(c)
	if body.Role == models.AccountRoleOwner && me.AccountRole != models.AccountRoleOwner {
		c.JSON(http.StatusForbidden, gin.H{"error": "only an owner can invite another owner"})
		return
	}

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

	// customer_users.email is globally unique, so an address already in use
	// anywhere cannot be invited here. Say so plainly: unlike signup there is
	// nothing to conceal, because the inviter already knows this address.
	if n, _ := db.Admin("customer_users").CountDocuments(ctx, bson.M{"email": email}); n > 0 {
		c.JSON(http.StatusConflict, gin.H{
			"error": "that address already has a Vantage HQ account"})
		return
	}

	var acct models.Account
	if err := db.Admin("accounts").FindOne(ctx,
		bson.M{"account_id": s.AccountID}).Decode(&acct); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not read your account"})
		return
	}

	if err := auth.CreateInvitedUser(ctx, s.AccountID, acct.Name, email, body.Role); err != nil {
		log.Printf("invite %s to %s: %v", email, s.AccountID, err)
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not send the invitation"})
		return
	}

	audit.Write(ctx, models.AuditEntry{
		Actor: s.Email, Action: "account_user.invited", AccountID: s.AccountID,
		Target: email, Detail: "role=" + body.Role, IP: c.ClientIP()})
	c.JSON(http.StatusCreated, gin.H{"invited": true})
}

// accountUser loads one person and confirms they are on the caller's account.
func accountUser(c *gin.Context, userID string) (*models.CustomerUser, bool) {
	s := auth.Current(c)
	var u models.CustomerUser
	if err := db.Admin("customer_users").FindOne(c.Request.Context(),
		bson.M{"user_id": userID, "account_id": s.AccountID}).Decode(&u); err != nil {
		c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
		return nil, false
	}
	return &u, true
}

// countOtherAccountOwners counts owners of an account other than one person.
func countOtherAccountOwners(c *gin.Context, exceptUserID string) (int64, error) {
	s := auth.Current(c)
	return db.Admin("customer_users").CountDocuments(c.Request.Context(), bson.M{
		"account_id":   s.AccountID,
		"account_role": models.AccountRoleOwner,
		"user_id":      bson.M{"$ne": exceptUserID},
	})
}

func updateAccountUserRole(c *gin.Context) {
	var body struct {
		Role string `json:"role"`
	}
	if err := c.ShouldBindJSON(&body); err != nil || !models.ValidAccountRole(body.Role) {
		c.JSON(http.StatusBadRequest, gin.H{"error": "role must be one of owner, admin, or member"})
		return
	}
	target, ok := accountUser(c, c.Param("id"))
	if !ok {
		return
	}
	me := auth.CurrentUser(c)
	if target.UserID == me.UserID {
		c.JSON(http.StatusForbidden, gin.H{"error": "you cannot change your own role"})
		return
	}
	if (body.Role == models.AccountRoleOwner || target.AccountRole == models.AccountRoleOwner) &&
		me.AccountRole != models.AccountRoleOwner {
		c.JSON(http.StatusForbidden, gin.H{"error": "only an owner can change owner roles"})
		return
	}
	if target.AccountRole == models.AccountRoleOwner && body.Role != models.AccountRoleOwner {
		others, err := countOtherAccountOwners(c, target.UserID)
		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 account's last owner; promote someone else first"})
			return
		}
	}

	ctx := c.Request.Context()
	if _, err := db.Admin("customer_users").UpdateOne(ctx,
		bson.M{"user_id": target.UserID},
		bson.M{"$set": bson.M{"account_role": body.Role}}); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

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

// deleteAccountUser removes a person and every instance they hold.
//
// Grants go first, and the whole request is refused if any of them would strand
// an instance with no owner. Removing the person but leaving their projected
// rows behind would leave working logins for someone the account has removed —
// the exact failure this endpoint exists to prevent.
func deleteAccountUser(c *gin.Context) {
	target, ok := accountUser(c, c.Param("id"))
	if !ok {
		return
	}
	me := auth.CurrentUser(c)
	if target.UserID == me.UserID {
		c.JSON(http.StatusForbidden, gin.H{"error": "you cannot remove your own account"})
		return
	}
	if target.AccountRole == models.AccountRoleOwner && me.AccountRole != models.AccountRoleOwner {
		c.JSON(http.StatusForbidden, gin.H{"error": "only an owner can remove another owner"})
		return
	}
	if target.AccountRole == models.AccountRoleOwner {
		others, err := countOtherAccountOwners(c, target.UserID)
		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 account's last owner; promote someone else first"})
			return
		}
	}

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

	cur, err := db.Admin("instance_members").Find(ctx,
		bson.M{"customer_user_id": target.UserID})
	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
	}

	// Check every instance BEFORE deleting anything, so a refusal leaves the
	// person exactly as they were rather than half-revoked.
	for _, m := range members {
		if m.Role != sharedmodels.RoleOwner {
			continue
		}
		others, err := cloudprov.CountOtherOwners(ctx, m.InstanceID, target.UserID)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		if others == 0 {
			c.JSON(http.StatusConflict, gin.H{
				"error": "they are the last owner of an instance; give someone else that instance's owner role first"})
			return
		}
	}

	for _, m := range members {
		if err := cloudprov.RevokeUser(ctx, m.InstanceID, target.UserID); err != nil {
			log.Printf("deleteAccountUser: revoke %s from %s: %v", target.Email, m.InstanceID, err)
			c.JSON(http.StatusInternalServerError, gin.H{
				"error": "could not remove their instance access; nothing was deleted"})
			return
		}
		if _, err := db.Admin("instance_members").DeleteOne(ctx,
			bson.M{"member_id": m.MemberID}); err != nil {
			log.Printf("deleteAccountUser: drop member row %s: %v", m.MemberID, err)
		}
	}

	if _, err := db.Admin("customer_users").DeleteOne(ctx,
		bson.M{"user_id": target.UserID}); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}

	audit.Write(ctx, models.AuditEntry{
		Actor: s.Email, Action: "account_user.removed", AccountID: s.AccountID,
		Target: target.Email, Detail: fmt.Sprintf("revoked %d instance(s)", len(members)),
		IP: c.ClientIP()})
	c.JSON(http.StatusOK, gin.H{"deleted": true})
}

// changeAccountPassword sets one password and pushes it everywhere.
//
// HQ's hash is the single source of truth for every hq-sourced row, and the
// control plane has no local password-change path for them, so there is no
// competing writer.
//
// Propagation is best-effort ON PURPOSE. Failing the password change because
// one of three instances was briefly unreachable would leave the customer with
// the password they were trying to get rid of; hqsync repairs a stale instance
// within fifteen minutes, which is recoverable.
func changeAccountPassword(c *gin.Context) {
	var body struct {
		CurrentPassword string `json:"current_password"`
		NewPassword     string `json:"new_password"`
	}
	if err := c.ShouldBindJSON(&body); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": "current and new password are required"})
		return
	}
	if len(body.NewPassword) < 12 {
		c.JSON(http.StatusBadRequest, gin.H{"error": "choose a password of at least 12 characters"})
		return
	}

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

	var me models.CustomerUser
	if err := db.Admin("customer_users").FindOne(ctx,
		bson.M{"user_id": s.UserID}).Decode(&me); err != nil {
		c.JSON(http.StatusUnauthorized, gin.H{"error": "sign in required"})
		return
	}
	if bcrypt.CompareHashAndPassword([]byte(me.PasswordHash), []byte(body.CurrentPassword)) != nil {
		c.JSON(http.StatusForbidden, gin.H{"error": "that is not your current password"})
		return
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(body.NewPassword), auth.BcryptCost)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not set the password"})
		return
	}
	if _, err := db.Admin("customer_users").UpdateOne(ctx,
		bson.M{"user_id": me.UserID},
		bson.M{"$set": bson.M{"password_hash": string(hash)}}); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{"error": "could not set the password"})
		return
	}

	pending := false
	if n, err := cloudprov.SetPasswordHash(ctx, me.UserID, string(hash)); err != nil {
		pending = true
		now := time.Now().UTC()
		log.Printf("password: propagation for %s failed, hqsync will repair: %v", me.Email, err)
		_, _ = db.Admin("customer_users").UpdateOne(ctx,
			bson.M{"user_id": me.UserID},
			bson.M{"$set": bson.M{"hq_sync_failed_at": now}})
	} else {
		log.Printf("password: %s propagated to %d instance user(s)", me.Email, n)
		_, _ = db.Admin("customer_users").UpdateOne(ctx,
			bson.M{"user_id": me.UserID},
			bson.M{"$unset": bson.M{"hq_sync_failed_at": ""}})
	}

	audit.Write(ctx, models.AuditEntry{
		Actor: me.Email, Action: "account_user.password_changed", AccountID: s.AccountID,
		Target: me.Email, IP: c.ClientIP()})
	c.JSON(http.StatusOK, gin.H{"updated": true, "propagation_pending": pending})
}
