package db

import (
	"crypto/rand"
	"database/sql"
	"math/big"
	"time"
)

const appPasswordChars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789"

// GenerateAppPassword returns a random secret for IMAP/SMTP client login — the only
// credential those protocols ever see, since AUTH has no interactive MFA step (see
// esrv_mailbox_app_passwords in schema.go). Floors at 25 chars regardless of minLen.
func GenerateAppPassword(minLen int) string {
	if minLen < 25 {
		minLen = 25
	}
	b := make([]byte, minLen)
	max := big.NewInt(int64(len(appPasswordChars)))
	for i := range b {
		n, _ := rand.Int(rand.Reader, max)
		b[i] = appPasswordChars[n.Int64()]
	}
	return string(b)
}

func (d *DB) ListAppPasswordsForMailbox(mailboxID int64) ([]MailboxAppPassword, error) {
	rows, err := d.Query(`SELECT id, mailbox_id, label, password_hash, is_active, created_at, last_used_at, expires_at
		FROM esrv_mailbox_app_passwords WHERE mailbox_id = ? ORDER BY created_at`, mailboxID)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []MailboxAppPassword
	for rows.Next() {
		var p MailboxAppPassword
		var createdAt string
		var lastUsedAt, expiresAt sql.NullString
		if err := rows.Scan(&p.ID, &p.MailboxID, &p.Label, &p.PasswordHash, &p.IsActive, &createdAt, &lastUsedAt, &expiresAt); err != nil {
			return nil, err
		}
		p.CreatedAt, _ = parseTime(createdAt)
		if lastUsedAt.Valid {
			t, _ := parseTime(lastUsedAt.String)
			p.LastUsedAt = &t
		}
		if expiresAt.Valid {
			t, _ := parseTime(expiresAt.String)
			p.ExpiresAt = &t
		}
		out = append(out, p)
	}
	return out, rows.Err()
}

// CreateAppPassword inserts a new app password. expiresAt is nil for one that never
// expires (the default).
func (d *DB) CreateAppPassword(mailboxID int64, label, passwordHash string, expiresAt *time.Time) (int64, error) {
	res, err := d.Exec(`INSERT INTO esrv_mailbox_app_passwords (mailbox_id, label, password_hash, expires_at) VALUES (?, ?, ?, ?)`,
		mailboxID, label, passwordHash, expiresAt)
	if err != nil {
		return 0, err
	}
	return res.LastInsertId()
}

// VerifyMailboxAppPassword resolves the mailbox by its primary email (never an alias)
// and bcrypt-checks it against every active app password. A mailbox's app-password
// list is small, so a linear scan needs no index. Returns (nil, nil) on no match.
func (d *DB) VerifyMailboxAppPassword(email, password string) (*Mailbox, error) {
	mbox, err := d.GetMailboxByEmail(email)
	if err != nil || mbox == nil {
		return nil, err
	}
	rows, err := d.Query(`SELECT id, password_hash FROM esrv_mailbox_app_passwords
		WHERE mailbox_id = ? AND is_active = 1 AND (expires_at IS NULL OR expires_at > ?)`, mbox.ID, time.Now())
	if err != nil {
		return nil, err
	}
	var matchedID int64
	found := false
	for rows.Next() {
		var id int64
		var hash string
		if err := rows.Scan(&id, &hash); err != nil {
			rows.Close()
			return nil, err
		}
		if CheckPassword(password, hash) {
			matchedID = id
			found = true
			break
		}
	}
	rowsErr := rows.Err()
	// Must close before the UPDATE below: the connection pool is capped to one
	// connection (see schema.go's Open), so an Exec while these rows are still open
	// would deadlock waiting for a connection that rows itself is holding.
	rows.Close()
	if rowsErr != nil {
		return nil, rowsErr
	}
	if !found {
		return nil, nil
	}
	if _, err := d.Exec(`UPDATE esrv_mailbox_app_passwords SET last_used_at = ? WHERE id = ?`, time.Now(), matchedID); err != nil {
		return nil, err
	}
	return mbox, nil
}

func (d *DB) SetAppPasswordActive(id int64, active bool) error {
	_, err := d.Exec(`UPDATE esrv_mailbox_app_passwords SET is_active = ? WHERE id = ?`, active, id)
	return err
}

// RemoveAppPassword deletes an app password, scoped to mailboxID so a caller can't
// remove one belonging to a different mailbox by guessing/manipulating its id —
// mirrors DeleteWebAuthnCredential's (id, ownerID) pattern.
func (d *DB) RemoveAppPassword(id, mailboxID int64) error {
	_, err := d.Exec(`DELETE FROM esrv_mailbox_app_passwords WHERE id = ? AND mailbox_id = ?`, id, mailboxID)
	return err
}
