package webui

import (
	"errors"
	"net/http"
	"strconv"
	"strings"

	"mailgoserver/internal/db"
)

var errNotOwned = errors.New("domain not in the requesting admin's scope")

// manageableAdmins mirrors the delegation rule: a global admin manages everyone; a
// scoped admin manages any other scoped admin whose entire domain assignment is a
// subset of their own (not just admins they personally created) — see the approved
// design in the conversation this shipped from.
func (a *App) manageableAdmins(r *http.Request) ([]db.AdminUser, error) {
	user := userFromContext(r)
	if user.IsGlobalAdmin {
		return a.DB.ListAllAdminUsers()
	}
	scope := scopeFromContext(r)
	scoped, err := a.DB.ListScopedAdminUsers()
	if err != nil {
		return nil, err
	}
	var out []db.AdminUser
	for _, other := range scoped {
		if other.ID == user.ID {
			continue
		}
		theirDomains, err := a.DB.AccessibleDomainIDs(other.ID)
		if err != nil {
			return nil, err
		}
		if isDomainSubset(theirDomains, scope) {
			out = append(out, other)
		}
	}
	return out, nil
}

func isDomainSubset(ids []int64, scope accessScope) bool {
	for _, id := range ids {
		if !scope.Allowed(id) {
			return false
		}
	}
	return true
}

// canManageAdmin re-checks a specific target admin against the current admin's
// delegation rights — used by the mutating routes so they don't just trust whatever
// the list page happened to render.
func (a *App) canManageAdmin(r *http.Request, target *db.AdminUser) (bool, error) {
	user := userFromContext(r)
	if target.ID == user.ID {
		return false, nil
	}
	if user.IsGlobalAdmin {
		return true, nil
	}
	if target.IsGlobalAdmin {
		return false, nil
	}
	theirDomains, err := a.DB.AccessibleDomainIDs(target.ID)
	if err != nil {
		return false, err
	}
	return isDomainSubset(theirDomains, scopeFromContext(r)), nil
}

func (a *App) adminsList(w http.ResponseWriter, r *http.Request) {
	admins, err := a.manageableAdmins(r)
	if err != nil {
		setFlash(w, "error", "Error loading admins")
	}
	var rows []M
	for _, u := range admins {
		domainIDs, _ := a.DB.AccessibleDomainIDs(u.ID)
		var domainNames []string
		for _, id := range domainIDs {
			if dom, _ := a.DB.GetDomainByID(id); dom != nil {
				domainNames = append(domainNames, dom.DomainName)
			}
		}
		rows = append(rows, M{"user": u, "domain_names": domainNames})
	}
	a.render(w, r, "admins.html", M{"active": "admins", "rows": rows, "current_user_id": userFromContext(r).ID})
}

func (a *App) addAdminForm(w http.ResponseWriter, r *http.Request) {
	domains, _ := a.accessibleDomains(r)
	a.render(w, r, "add_admin.html", M{"active": "admins", "domains": domains, "can_grant_global": userFromContext(r).IsGlobalAdmin})
}

// addAdmin mirrors the delegation flow: creates a new scoped admin (or, for a global
// admin, optionally a new global admin), forced to change their password on first
// login exactly like the seeded default account.
func (a *App) addAdmin(w http.ResponseWriter, r *http.Request) {
	user := userFromContext(r)
	username := strings.TrimSpace(r.FormValue("username"))
	password := r.FormValue("password")
	makeGlobal := user.IsGlobalAdmin && r.FormValue("is_global_admin") == "on"

	if username == "" || !isStrongPassword(password) {
		setFlash(w, "error", "Username is required and password must be at least 10 characters with a letter, a number, and a symbol")
		http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
		return
	}
	if existing, _ := a.DB.GetAdminUserByUsername(username); existing != nil {
		setFlash(w, "error", "That username is already taken")
		http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
		return
	}
	hash, err := db.HashPassword(password)
	if err != nil {
		setFlash(w, "error", "Something went wrong")
		http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
		return
	}

	if makeGlobal {
		if _, err := a.DB.CreateAdminUser(username, hash, true); err != nil {
			setFlash(w, "error", "Error creating admin")
			http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
			return
		}
		setFlash(w, "success", "Global admin created")
		http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
		return
	}

	domainIDs, err := a.parseOwnedDomainIDs(r)
	if err != nil {
		setFlash(w, "error", "You can only assign domains you manage yourself")
		http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
		return
	}
	if _, err := a.DB.CreateScopedAdminUser(username, hash, user.ID, domainIDs); err != nil {
		setFlash(w, "error", "Error creating admin")
		http.Redirect(w, r, Prefix+"/admins/add", http.StatusFound)
		return
	}
	setFlash(w, "success", "Admin created and given access to the selected domains")
	http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
}

// parseOwnedDomainIDs reads the "domain_ids" checkbox list from the form and rejects
// the request outright if any of them fall outside the current admin's own scope —
// the actual enforcement point for "can only delegate domains you have yourself".
func (a *App) parseOwnedDomainIDs(r *http.Request) ([]int64, error) {
	scope := scopeFromContext(r)
	var ids []int64
	for _, v := range r.Form["domain_ids"] {
		id, err := strconv.ParseInt(v, 10, 64)
		if err != nil {
			continue
		}
		if !scope.Allowed(id) {
			return nil, errNotOwned
		}
		ids = append(ids, id)
	}
	return ids, nil
}

func (a *App) editAdminDomainsForm(w http.ResponseWriter, r *http.Request) {
	target, ok := a.adminWithManageAccess(w, r)
	if !ok {
		return
	}
	domains, _ := a.accessibleDomains(r)
	assigned, _ := a.DB.AccessibleDomainIDs(target.ID)
	assignedSet := make(map[int64]bool, len(assigned))
	for _, id := range assigned {
		assignedSet[id] = true
	}
	a.render(w, r, "edit_admin.html", M{"active": "admins", "target": target, "domains": domains, "assigned": assignedSet})
}

func (a *App) editAdminDomains(w http.ResponseWriter, r *http.Request) {
	target, ok := a.adminWithManageAccess(w, r)
	if !ok {
		return
	}
	if err := r.ParseForm(); err != nil {
		setFlash(w, "error", "Invalid form data")
		http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
		return
	}
	domainIDs, err := a.parseOwnedDomainIDs(r)
	if err != nil {
		setFlash(w, "error", "You can only assign domains you manage yourself")
		http.Redirect(w, r, Prefix+"/admins/"+strconv.FormatInt(target.ID, 10)+"/edit", http.StatusFound)
		return
	}
	if err := a.DB.SetAdminDomainAccess(target.ID, domainIDs); err != nil {
		setFlash(w, "error", "Error updating domain access")
	} else {
		setFlash(w, "success", "Domain access updated")
	}
	http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
}

// adminWithManageAccess fetches the target admin by path ID and re-validates the
// delegation rule server-side (never trust that the list page's filtering was the
// only gate).
func (a *App) adminWithManageAccess(w http.ResponseWriter, r *http.Request) (*db.AdminUser, bool) {
	target, err := a.DB.GetAdminUserByID(pathID(r))
	if err != nil || target == nil {
		http.NotFound(w, r)
		return nil, false
	}
	allowed, err := a.canManageAdmin(r, target)
	if err != nil || !allowed {
		http.NotFound(w, r)
		return nil, false
	}
	return target, true
}

// resetAdminMFA clears a target admin's TOTP and passkeys — e.g. after a lost device
// — so they can sign back in without a second factor (or under enforce_admin_mfa,
// re-enroll from /account on their next login) without needing database access.
func (a *App) resetAdminMFA(w http.ResponseWriter, r *http.Request) {
	target, ok := a.adminWithManageAccess(w, r)
	if !ok {
		return
	}
	if err := a.DB.ResetAdminMFA(target.ID); err != nil {
		setFlash(w, "error", "Error resetting MFA")
	} else {
		_ = a.DB.LogAuthAttempt("admin_mfa", target.Username, a.requestIP(r), true, "MFA reset by admin "+userFromContext(r).Username)
		setFlash(w, "success", "MFA reset for "+target.Username)
	}
	http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
}

func (a *App) removeAdmin(w http.ResponseWriter, r *http.Request) {
	target, ok := a.adminWithManageAccess(w, r)
	if !ok {
		return
	}
	if target.IsGlobalAdmin {
		if all, err := a.DB.ListAllAdminUsers(); err == nil {
			remaining := 0
			for _, u := range all {
				if u.IsGlobalAdmin {
					remaining++
				}
			}
			if remaining <= 1 {
				setFlash(w, "error", "Can't remove the last global admin")
				http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
				return
			}
		}
	}
	if err := a.DB.DeleteAdminUser(target.ID); err != nil {
		setFlash(w, "error", "Error removing admin")
	} else {
		setFlash(w, "success", "Admin removed")
	}
	http.Redirect(w, r, Prefix+"/admins", http.StatusFound)
}
