package db

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

// IPBlacklistEntry is a temporary (or manual) block on SMTP/IMAP traffic from one IP.
// Deliberately separate from WhitelistedIP (esrv_whitelisted_ips), which authorizes
// unauthenticated relay for a domain — a completely different concern.
type IPBlacklistEntry struct {
	ID            int64
	IPAddress     string
	Reason        string
	OffenseCount  int
	Manual        bool
	BlacklistedAt time.Time
	ExpiresAt     time.Time
}

// IPAbuseWhitelistEntry exempts one IP from abuse detection entirely.
type IPAbuseWhitelistEntry struct {
	ID        int64
	IPAddress string
	Note      string
	CreatedAt time.Time
}

// smtpImapAuthTypes are the esrv_auth_logs auth_type values that count toward abuse
// detection: SMTP/IMAP traffic, not admin/webmail dashboard logins (those already have
// their own lockout in internal/webui/ratelimit.go).
const smtpImapAuthTypesSQL = `auth_type IN ('sender', 'mailbox', 'sender_validation', 'mailbox_validation', 'ip', 'imap_login')`

// CountFailedAuthAttemptsByIP mirrors CountRecentFailedAttempts (queries.go:140) exactly,
// including its documented since.UTC().Format("2006-01-02 15:04:05") requirement, but
// scoped by ip_address across all SMTP/IMAP auth types instead of by identifier+one type.
func (d *DB) CountFailedAuthAttemptsByIP(ip string, since time.Time) (int, error) {
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_auth_logs
		WHERE ip_address = ? AND success = 0 AND created_at >= ? AND `+smtpImapAuthTypesSQL,
		ip, since.UTC().Format("2006-01-02 15:04:05")).Scan(&n)
	return n, err
}

// IsIPBlacklisted reports whether ip has a currently-active (unexpired) blacklist entry.
func (d *DB) IsIPBlacklisted(ip string) (bool, error) {
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_ip_blacklist WHERE ip_address = ? AND expires_at > ?`,
		ip, time.Now().UTC().Format("2006-01-02 15:04:05")).Scan(&n)
	return n > 0, err
}

// IsIPAbuseWhitelisted reports whether ip is exempt from abuse detection.
func (d *DB) IsIPAbuseWhitelisted(ip string) (bool, error) {
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_ip_abuse_whitelist WHERE ip_address = ?`, ip).Scan(&n)
	return n > 0, err
}

// BlacklistIP upserts ip's blacklist entry, escalating the block duration on repeat
// offenses: duration = min(baseHours * 2^(offenseCount-1), maxHours), where offenseCount
// is incremented on every call regardless of whether the previous entry had expired.
func (d *DB) BlacklistIP(ip, reason string, baseHours, maxHours int) error {
	var existingCount int
	err := d.QueryRow(`SELECT offense_count FROM esrv_ip_blacklist WHERE ip_address = ?`, ip).Scan(&existingCount)
	if err != nil && !errors.Is(err, sql.ErrNoRows) {
		return err
	}
	newCount := existingCount + 1

	hours := baseHours
	for i := 1; i < newCount; i++ {
		hours *= 2
		if hours >= maxHours {
			hours = maxHours
			break
		}
	}
	if hours > maxHours {
		hours = maxHours
	}
	expiresAt := time.Now().UTC().Add(time.Duration(hours) * time.Hour).Format("2006-01-02 15:04:05")

	_, err = d.Exec(`INSERT INTO esrv_ip_blacklist (ip_address, reason, offense_count, manual, blacklisted_at, expires_at)
		VALUES (?, ?, ?, 0, CURRENT_TIMESTAMP, ?)
		ON CONFLICT(ip_address) DO UPDATE SET
			reason = excluded.reason,
			offense_count = excluded.offense_count,
			manual = 0,
			blacklisted_at = CURRENT_TIMESTAMP,
			expires_at = excluded.expires_at`,
		ip, reason, newCount, expiresAt)
	return err
}

// AddManualBlacklistEntry is an admin-initiated block: fixed duration, no escalation math.
func (d *DB) AddManualBlacklistEntry(ip, reason string, hours int) error {
	expiresAt := time.Now().UTC().Add(time.Duration(hours) * time.Hour).Format("2006-01-02 15:04:05")
	_, err := d.Exec(`INSERT INTO esrv_ip_blacklist (ip_address, reason, offense_count, manual, blacklisted_at, expires_at)
		VALUES (?, ?, 1, 1, CURRENT_TIMESTAMP, ?)
		ON CONFLICT(ip_address) DO UPDATE SET
			reason = excluded.reason,
			manual = 1,
			blacklisted_at = CURRENT_TIMESTAMP,
			expires_at = excluded.expires_at`,
		ip, reason, expiresAt)
	return err
}

func (d *DB) ListBlacklist() ([]IPBlacklistEntry, error) {
	rows, err := d.Query(`SELECT id, ip_address, reason, offense_count, manual, blacklisted_at, expires_at
		FROM esrv_ip_blacklist ORDER BY blacklisted_at DESC`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []IPBlacklistEntry
	for rows.Next() {
		var e IPBlacklistEntry
		var blacklistedAt, expiresAt string
		if err := rows.Scan(&e.ID, &e.IPAddress, &e.Reason, &e.OffenseCount, &e.Manual, &blacklistedAt, &expiresAt); err != nil {
			return nil, err
		}
		e.BlacklistedAt, _ = parseTime(blacklistedAt)
		e.ExpiresAt, _ = parseTime(expiresAt)
		out = append(out, e)
	}
	return out, rows.Err()
}

func (d *DB) RemoveBlacklistEntry(id int64) error {
	_, err := d.Exec(`DELETE FROM esrv_ip_blacklist WHERE id = ?`, id)
	return err
}

func (d *DB) ListAbuseWhitelist() ([]IPAbuseWhitelistEntry, error) {
	rows, err := d.Query(`SELECT id, ip_address, note, created_at FROM esrv_ip_abuse_whitelist ORDER BY ip_address`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []IPAbuseWhitelistEntry
	for rows.Next() {
		var e IPAbuseWhitelistEntry
		var createdAt string
		if err := rows.Scan(&e.ID, &e.IPAddress, &e.Note, &createdAt); err != nil {
			return nil, err
		}
		e.CreatedAt, _ = parseTime(createdAt)
		out = append(out, e)
	}
	return out, rows.Err()
}

func (d *DB) AddAbuseWhitelist(ip, note string) error {
	_, err := d.Exec(`INSERT INTO esrv_ip_abuse_whitelist (ip_address, note) VALUES (?, ?)
		ON CONFLICT(ip_address) DO UPDATE SET note = excluded.note`, ip, note)
	return err
}

func (d *DB) RemoveAbuseWhitelist(id int64) error {
	_, err := d.Exec(`DELETE FROM esrv_ip_abuse_whitelist WHERE id = ?`, id)
	return err
}

// CountBlacklistEventsSince counts blacklist entries (auto or manual) created since the
// given cutoff, for the dashboard's attack-count tiles.
func (d *DB) CountBlacklistEventsSince(since time.Time) (int, error) {
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_ip_blacklist WHERE blacklisted_at >= ?`,
		since.UTC().Format("2006-01-02 15:04:05")).Scan(&n)
	return n, err
}

// CountFailedAuthSince counts failed SMTP/IMAP auth attempts (any IP) since the given
// cutoff, for the dashboard's attack-count tiles.
func (d *DB) CountFailedAuthSince(since time.Time) (int, error) {
	var n int
	err := d.QueryRow(`SELECT COUNT(*) FROM esrv_auth_logs
		WHERE success = 0 AND created_at >= ? AND `+smtpImapAuthTypesSQL,
		since.UTC().Format("2006-01-02 15:04:05")).Scan(&n)
	return n, err
}
