package webui

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

	"mailgoserver/internal/db"
)

func pathID(r *http.Request) int64 {
	return int64(atoi(r.PathValue("id")))
}

// validDomainName requires a real, multi-label FQDN (e.g. "example.com" or
// "mail.example.com") — a bare word like "zczsdc" has no dot and is rejected.
var validDomainName = regexp.MustCompile(`^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,63}$`)

func isValidDomainName(name string) bool {
	return len(name) <= 253 && validDomainName.MatchString(name)
}

// domainsList mirrors domains.py's domains_list(), scoped to the current admin's
// assigned domains unless they're a global admin.
func (a *App) domainsList(w http.ResponseWriter, r *http.Request) {
	allDomains, err := a.DB.ListDomains()
	if err != nil {
		setFlash(w, "error", "Error loading domains")
		http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
		return
	}
	scope := scopeFromContext(r)
	var domains []db.Domain
	var rows []M
	for _, d := range allDomains {
		if !scope.Allowed(d.ID) {
			continue
		}
		domains = append(domains, d)
		senderCount, _ := a.DB.CountSendersForDomain(d.ID)
		hasActiveDKIM, _ := a.DB.HasActiveDKIMForDomain(d.ID)
		hasAnyDKIM, _ := a.DB.HasAnyDKIMForDomain(d.ID)
		rows = append(rows, M{
			"domain": d, "sender_count": senderCount,
			"has_active_dkim": hasActiveDKIM, "has_any_dkim": hasAnyDKIM,
		})
	}
	a.render(w, r, "domains.html", M{"active": "domains", "domains": domains, "rows": rows})
}

func (a *App) addDomainForm(w http.ResponseWriter, r *http.Request) {
	a.render(w, r, "add_domain.html", M{"active": "domains"})
}

// addDomain mirrors domains.py's add_domain() POST branch.
func (a *App) addDomain(w http.ResponseWriter, r *http.Request) {
	name := strings.ToLower(strings.TrimSpace(r.FormValue("domain_name")))
	if !isValidDomainName(name) {
		setFlash(w, "error", "Please enter a real domain name, e.g. example.com or mail.example.com")
		http.Redirect(w, r, Prefix+"/domains/add", http.StatusFound)
		return
	}
	existing, _ := a.DB.GetDomainByNameExact(name)
	if existing != nil {
		setFlash(w, "error", "Domain already exists")
		http.Redirect(w, r, Prefix+"/domains/add", http.StatusFound)
		return
	}
	newID, err := a.DB.CreateDomain(name)
	if err != nil {
		setFlash(w, "error", "Error adding domain")
		http.Redirect(w, r, Prefix+"/domains/add", http.StatusFound)
		return
	}
	// A scoped admin who creates a domain automatically gets management access to it
	// — otherwise they'd create a domain and immediately lose the ability to see it.
	if scope := scopeFromContext(r); !scope.Global {
		if err := a.DB.GrantDomainAccess(userFromContext(r).ID, newID); err != nil {
			a.Logger.Error("grant domain access after create: %v", err)
		}
	}
	if _, err := a.DKIM.GenerateDKIMKeypair(name, "", false); err != nil {
		a.Logger.Error("DKIM generation for %s failed: %v", name, err)
	}
	setFlash(w, "success", "Domain added successfully")
	http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
}

// toggleDomainOff mirrors domains.py's delete_domain(): unconditional soft-disable.
func (a *App) toggleDomainOff(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	if !requireDomainAccess(w, r, id) {
		return
	}
	if err := a.DB.SetDomainActive(id, false); err != nil {
		setFlash(w, "error", "Error disabling domain")
	} else {
		setFlash(w, "success", "Domain disabled")
	}
	http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
}

func (a *App) editDomainForm(w http.ResponseWriter, r *http.Request) {
	dom, err := a.DB.GetDomainByID(pathID(r))
	if err != nil || dom == nil {
		http.NotFound(w, r)
		return
	}
	if !requireDomainAccess(w, r, dom.ID) {
		return
	}
	a.render(w, r, "edit_domain.html", M{"active": "domains", "domain": dom})
}

// editDomain mirrors domains.py's edit_domain() POST branch.
func (a *App) editDomain(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	dom, err := a.DB.GetDomainByID(id)
	if err != nil || dom == nil {
		http.NotFound(w, r)
		return
	}
	if !requireDomainAccess(w, r, dom.ID) {
		return
	}
	name := strings.ToLower(strings.TrimSpace(r.FormValue("domain_name")))
	requiresAuth := r.FormValue("requires_auth") == "on"
	if !isValidDomainName(name) {
		setFlash(w, "error", "Please enter a real domain name, e.g. example.com or mail.example.com")
		a.render(w, r, "edit_domain.html", M{"active": "domains", "domain": dom})
		return
	}
	if other, _ := a.DB.GetDomainByNameExact(name); other != nil && other.ID != id {
		setFlash(w, "error", "Domain already exists")
		a.render(w, r, "edit_domain.html", M{"active": "domains", "domain": dom})
		return
	}
	if err := a.DB.UpdateDomain(id, name, requiresAuth); err != nil {
		setFlash(w, "error", "Error updating domain")
		http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
		return
	}
	if err := a.DB.SetDomainMFAExempt(id, r.FormValue("mfa_exempt") == "on"); err != nil {
		setFlash(w, "error", "Error updating domain")
		http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
		return
	}
	setFlash(w, "success", "Domain updated successfully")
	http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
}

// toggleDomain mirrors domains.py's toggle_domain().
func (a *App) toggleDomain(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	dom, err := a.DB.GetDomainByID(id)
	if err != nil || dom == nil {
		http.NotFound(w, r)
		return
	}
	if !requireDomainAccess(w, r, dom.ID) {
		return
	}
	if err := a.DB.SetDomainActive(id, !dom.IsActive); err != nil {
		setFlash(w, "error", "Error updating domain status")
	} else if dom.IsActive {
		setFlash(w, "success", "Domain disabled")
	} else {
		setFlash(w, "success", "Domain enabled")
	}
	http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
}

// removeDomain mirrors domains.py's remove_domain(): hard delete + cascade.
func (a *App) removeDomain(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	dom, err := a.DB.GetDomainByID(id)
	if err != nil || dom == nil {
		http.NotFound(w, r)
		return
	}
	if !requireDomainAccess(w, r, dom.ID) {
		return
	}
	senders, ips, keys, headers, err := a.DB.RemoveDomainCascade(id)
	if err != nil {
		setFlash(w, "error", "Error removing domain")
	} else {
		setFlash(w, "success", domainRemovedMessage(dom.DomainName, senders, ips, keys, headers))
	}
	http.Redirect(w, r, Prefix+"/domains", http.StatusFound)
}

// verifyDomainCheck queries the domain's DNS TXT ownership record via 1.1.1.1 and
// 8.8.8.8 and marks it verified if found, mirroring the DKIM/SPF "Check DNS" pattern.
func (a *App) verifyDomainCheck(w http.ResponseWriter, r *http.Request) {
	id := pathID(r)
	dom, err := a.DB.GetDomainByID(id)
	if err != nil || dom == nil {
		http.NotFound(w, r)
		return
	}
	if !requireDomainAccess(w, r, dom.ID) {
		return
	}
	verified, records, err := checkDomainOwnership(dom.DomainName, dom.VerificationToken)
	if err != nil {
		writeJSON(w, http.StatusOK, M{"success": false, "message": "DNS lookup failed: " + err.Error()})
		return
	}
	if verified {
		if err := a.DB.SetDomainVerified(id, true); err != nil {
			writeJSON(w, http.StatusOK, M{"success": false, "message": "Verified via DNS but failed to save: " + err.Error()})
			return
		}
		writeJSON(w, http.StatusOK, M{"success": true, "message": "Domain ownership verified — it can now send mail.", "records": records})
		return
	}
	writeJSON(w, http.StatusOK, M{"success": false, "message": "TXT record not found or doesn't match yet. DNS changes can take a while to propagate.", "records": records})
}

// accessibleDomains returns the active domains the current admin can pick from in a
// dropdown (add/edit sender, IP, DKIM forms) — all of them for a global admin, only
// their assigned ones for a scoped admin.
func (a *App) accessibleDomains(r *http.Request) ([]db.Domain, error) {
	all, err := a.DB.ListActiveDomains()
	if err != nil {
		return nil, err
	}
	scope := scopeFromContext(r)
	if scope.Global {
		return all, nil
	}
	var out []db.Domain
	for _, d := range all {
		if scope.Allowed(d.ID) {
			out = append(out, d)
		}
	}
	return out, nil
}

// accessibleDomainNames is accessibleDomains's counterpart for tables that store the
// domain as text (mail_from, auth log identifiers) rather than a domain_id — email
// logs and auth logs, which predate per-domain admin scoping. isGlobal=true means
// "don't filter, they can see everything" and names will be nil.
func (a *App) accessibleDomainNames(r *http.Request) (names map[string]bool, isGlobal bool, err error) {
	scope := scopeFromContext(r)
	if scope.Global {
		return nil, true, nil
	}
	domains, err := a.DB.ListDomains()
	if err != nil {
		return nil, false, err
	}
	names = make(map[string]bool)
	for _, d := range domains {
		if scope.Allowed(d.ID) {
			names[strings.ToLower(d.DomainName)] = true
		}
	}
	return names, false, nil
}

func domainRemovedMessage(name string, senders, ips, keys, headers int) string {
	return "Domain " + name + " and associated records removed (senders: " + strconv.Itoa(senders) + ", IPs: " + strconv.Itoa(ips) + ", DKIM keys: " + strconv.Itoa(keys) + ", custom headers: " + strconv.Itoa(headers) + ")"
}
