package db

import (
	"database/sql"
	"errors"
	"strings"
	"time"

	"golang.org/x/crypto/bcrypt"
)

func equalFold(a, b string) bool { return strings.EqualFold(a, b) }

func domainPart(address string) string {
	i := strings.LastIndex(address, "@")
	if i < 0 {
		return ""
	}
	return strings.ToLower(address[i+1:])
}

// bcryptCost is pinned to 12 to match Python's bcrypt.gensalt() default, since Go's
// bcrypt.DefaultCost is 10 and would otherwise silently produce weaker hashes.
const bcryptCost = 12

// HashPassword mirrors models.hash_password.
func HashPassword(password string) (string, error) {
	b, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
	if err != nil {
		return "", err
	}
	return string(b), nil
}

// CheckPassword mirrors models.check_password.
func CheckPassword(password, hash string) bool {
	return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}

// GetSenderByEmail mirrors models.get_sender_by_email: case-insensitive match against
// the lower-cased stored email, active senders only.
func (d *DB) GetSenderByEmail(email string) (*Sender, error) {
	row := d.QueryRow(`SELECT id, email, password_hash, domain_id, can_send_as_domain, is_active, created_at, store_message_content
		FROM esrv_senders WHERE lower(email) = lower(?) AND is_active = 1`, email)
	var s Sender
	var createdAt string
	if err := row.Scan(&s.ID, &s.Email, &s.PasswordHash, &s.DomainID, &s.CanSendAsDomain, &s.IsActive, &createdAt, &s.StoreMessageContent); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	s.CreatedAt, _ = parseTime(createdAt)
	return &s, nil
}

const domainColumns = `id, domain_name, is_active, created_at, verification_token, is_verified, verified_at, mfa_exempt`

// scanDomain scans a row selected with domainColumns, in that order.
func scanDomain(row *sql.Row) (*Domain, error) {
	var dom Domain
	var createdAt string
	var verifiedAt sql.NullString
	if err := row.Scan(&dom.ID, &dom.DomainName, &dom.IsActive, &createdAt, &dom.VerificationToken, &dom.IsVerified, &verifiedAt, &dom.MFAExempt); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	dom.CreatedAt, _ = parseTime(createdAt)
	if verifiedAt.Valid {
		t, _ := parseTime(verifiedAt.String)
		dom.VerifiedAt = &t
	}
	return &dom, nil
}

// GetDomainByName mirrors models.get_domain_by_name.
func (d *DB) GetDomainByName(name string) (*Domain, error) {
	row := d.QueryRow(`SELECT `+domainColumns+` FROM esrv_domains
		WHERE lower(domain_name) = lower(?) AND is_active = 1`, name)
	return scanDomain(row)
}

// GetWhitelistedIP mirrors models.get_whitelisted_ip. domainName == "" means no domain
// filter, matching the Python default parameter.
func (d *DB) GetWhitelistedIP(ipAddress, domainName string) (*WhitelistedIP, error) {
	var row *sql.Row
	if domainName == "" {
		row = d.QueryRow(`SELECT id, ip_address, domain_id, is_active, created_at, store_message_content
			FROM esrv_whitelisted_ips WHERE ip_address = ? AND is_active = 1`, ipAddress)
	} else {
		dom, err := d.GetDomainByName(domainName)
		if err != nil {
			return nil, err
		}
		if dom == nil {
			return nil, nil
		}
		row = d.QueryRow(`SELECT id, ip_address, domain_id, is_active, created_at, store_message_content
			FROM esrv_whitelisted_ips WHERE ip_address = ? AND is_active = 1 AND domain_id = ?`, ipAddress, dom.ID)
	}
	var w WhitelistedIP
	var createdAt string
	if err := row.Scan(&w.ID, &w.IPAddress, &w.DomainID, &w.IsActive, &createdAt, &w.StoreMessageContent); err != nil {
		if errors.Is(err, sql.ErrNoRows) {
			return nil, nil
		}
		return nil, err
	}
	w.CreatedAt, _ = parseTime(createdAt)
	return &w, nil
}

// CanSendForDomain mirrors WhitelistedIP.can_send_for_domain. Not called from the live
// auth path (models.py's own equivalent isn't either) — kept for interface parity.
func (w WhitelistedIP) CanSendForDomain(d *DB, domainName string) (bool, error) {
	if !w.IsActive {
		return false, nil
	}
	dom, err := d.GetDomainByName(domainName)
	if err != nil || dom == nil {
		return false, err
	}
	return w.DomainID == dom.ID, nil
}

// LogAuthAttempt mirrors models.log_auth_attempt.
func (d *DB) LogAuthAttempt(authType, identifier, ipAddress string, success bool, message string) error {
	_, err := d.Exec(`INSERT INTO esrv_auth_logs (auth_type, identifier, ip_address, success, message)
		VALUES (?, ?, ?, ?, ?)`, authType, identifier, ipAddress, success, message)
	return err
}

// CountRecentFailedAttempts counts failed esrv_auth_logs rows for one identifier
// (independent of which IP each attempt came from — a distributed credential-
// stuffing attempt against a single account should still trip this) within
// authType and since the given cutoff, powering the per-account lockout in
// internal/webui/login.go and webmail_login.go.
func (d *DB) CountRecentFailedAttempts(authType, identifier string, since time.Time) (int, error) {
	var n int
	// created_at is populated by SQLite's own CURRENT_TIMESTAMP: a plain
	// "YYYY-MM-DD HH:MM:SS" UTC string, space-separated, no fractional seconds, no
	// offset. modernc.org/sqlite instead binds a Go time.Time query parameter as
	// RFC3339Nano with a zone offset (e.g. "2026-08-14T06:57:50.497566315+01:00") —
	// a live check confirmed this by inserting a time.Time into a real column and
	// reading the stored text back. That format is structurally different from
	// CURRENT_TIMESTAMP's own (different separator, precision, and offset), so a
	// plain text >= comparison between the two doesn't reflect chronological order at
	// all (confirmed: it silently matched zero rows). Two Go-bound time.Time values
	// compared against each other DO work correctly, since the driver formats both
	// identically — this only breaks when one side is a raw SQL CURRENT_TIMESTAMP
	// default and the other is a Go-bound parameter, which happens on THIS column but
	// nowhere else in this codebase (checked every other DATETIME comparison).
	// Formatting since into CURRENT_TIMESTAMP's exact layout makes both sides match.
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_auth_logs
		WHERE auth_type = ? AND identifier = ? AND success = 0 AND created_at >= ?`,
		authType, identifier, since.UTC().Format("2006-01-02 15:04:05")).Scan(&n)
	return n, err
}

func parseTime(s string) (time.Time, error) {
	for _, layout := range []string{"2006-01-02 15:04:05.999999999-07:00", "2006-01-02 15:04:05", time.RFC3339} {
		if t, err := time.Parse(layout, s); err == nil {
			return t, nil
		}
	}
	return time.Time{}, errors.New("unparseable time: " + s)
}
