// Package pop3 implements POP3 account management and import.
// It provides a DB-backed store for POP3 account configurations,
// a raw TCP/TLS POP3 client, and an import worker.
package pop3

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
	"io"
	"time"

	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"
)

// Account represents a POP3 account configuration stored in the database.
type Account struct {
	ID              int64      `json:"id"`
	Owner           string     `json:"owner"`
	Name            string     `json:"name"`
	Host            string     `json:"host"`
	Port            int        `json:"port"`
	TLS             string     `json:"tls"`
	TLSSkipVerify   bool       `json:"tls_skip_verify"`
	Username        string     `json:"username"`
	Status          string     `json:"status"`
	ErrorMsg        string     `json:"error_msg"`
	LastImportAt    *time.Time `json:"last_import_at,omitempty"`
	LastImportCount int        `json:"last_import_count"`
	ProgressCurrent int        `json:"progress_current"`
	ProgressTotal   int        `json:"progress_total"`
	CreatedAt       time.Time  `json:"created_at"`
	TenantID        *int64     `json:"tenant_id,omitempty"`
}

// Store manages POP3 account persistence in PostgreSQL.
type Store struct {
	pool   *pgxpool.Pool
	encKey [32]byte
}

const createTableSQL = `
CREATE TABLE IF NOT EXISTS pop3_accounts (
	id               SERIAL PRIMARY KEY,
	owner            TEXT NOT NULL,
	name             TEXT NOT NULL,
	host             TEXT NOT NULL,
	port             INTEGER NOT NULL DEFAULT 110,
	tls              TEXT NOT NULL DEFAULT 'none',
	tls_skip_verify  BOOLEAN NOT NULL DEFAULT false,
	username         TEXT NOT NULL,
	password_enc     BYTEA NOT NULL,
	status           TEXT NOT NULL DEFAULT 'idle',
	error_msg        TEXT NOT NULL DEFAULT '',
	last_import_at   TIMESTAMPTZ,
	last_import_count INTEGER NOT NULL DEFAULT 0,
	progress_current INTEGER NOT NULL DEFAULT 0,
	progress_total   INTEGER NOT NULL DEFAULT 0,
	created_at       TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_pop3_accounts_owner ON pop3_accounts (owner);
ALTER TABLE pop3_accounts ADD COLUMN IF NOT EXISTS tenant_id INTEGER REFERENCES tenants(id);
`

// New creates a new Store, connects to PostgreSQL, and runs the schema migration.
func New(dsn, secret string) (*Store, error) {
	pool, err := pgxpool.New(context.Background(), dsn)
	if err != nil {
		return nil, fmt.Errorf("pop3 store: connect: %w", err)
	}

	if _, err := pool.Exec(context.Background(), createTableSQL); err != nil {
		pool.Close()
		return nil, fmt.Errorf("pop3 store: migrate: %w", err)
	}

	key := sha256.Sum256([]byte(secret))
	return &Store{pool: pool, encKey: key}, nil
}

// Close releases the database connection pool.
func (s *Store) Close() {
	s.pool.Close()
}

// Create inserts a new POP3 account with an encrypted password.
func (s *Store) Create(ctx context.Context, acc Account, password string) (*Account, error) {
	enc, err := encryptPassword(password, s.encKey)
	if err != nil {
		return nil, fmt.Errorf("pop3 store: encrypt password: %w", err)
	}

	row := s.pool.QueryRow(ctx, `
		INSERT INTO pop3_accounts (owner, name, host, port, tls, tls_skip_verify, username, password_enc, tenant_id)
		VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
		RETURNING id, created_at`,
		acc.Owner, acc.Name, acc.Host, acc.Port, acc.TLS, acc.TLSSkipVerify, acc.Username, enc, acc.TenantID,
	)

	if err := row.Scan(&acc.ID, &acc.CreatedAt); err != nil {
		return nil, fmt.Errorf("pop3 store: create: %w", err)
	}

	acc.Status = "idle"
	acc.ErrorMsg = ""
	return &acc, nil
}

// selectColumns is the canonical column list used in all SELECT statements.
const selectColumns = ` id, owner, name, host, port, tls, tls_skip_verify, username,
	status, error_msg, last_import_at, last_import_count,
	progress_current, progress_total, created_at, tenant_id `

// scanner abstracts pgx.Row and pgx.Rows — both expose Scan(...any) error.
type scanner interface {
	Scan(dest ...any) error
}

func scanRow(row scanner) (Account, error) {
	var a Account
	err := row.Scan(
		&a.ID, &a.Owner, &a.Name, &a.Host, &a.Port, &a.TLS, &a.TLSSkipVerify, &a.Username,
		&a.Status, &a.ErrorMsg, &a.LastImportAt,
		&a.LastImportCount, &a.ProgressCurrent, &a.ProgressTotal, &a.CreatedAt, &a.TenantID,
	)
	return a, err
}

// List returns POP3 accounts. Superadmins (tenantID == nil) see all accounts;
// other admins (tenantID != nil) see all accounts within their own tenant;
// regular users see only their own accounts.
func (s *Store) List(ctx context.Context, owner string, isAdmin bool, tenantID *int64) ([]Account, error) {
	var rows pgx.Rows
	var err error

	q := `SELECT` + selectColumns + `FROM pop3_accounts`

	switch {
	case isAdmin && tenantID == nil:
		rows, err = s.pool.Query(ctx, q+` ORDER BY id`)
	case isAdmin:
		rows, err = s.pool.Query(ctx, q+` WHERE tenant_id = $1 ORDER BY id`, *tenantID)
	default:
		rows, err = s.pool.Query(ctx, q+` WHERE owner = $1 ORDER BY id`, owner)
	}
	if err != nil {
		return nil, fmt.Errorf("pop3 store: list: %w", err)
	}
	defer rows.Close()

	var accounts []Account
	for rows.Next() {
		a, err := scanRow(rows)
		if err != nil {
			return nil, fmt.Errorf("pop3 store: scan: %w", err)
		}
		accounts = append(accounts, a)
	}
	return accounts, rows.Err()
}

// Get returns a single POP3 account by ID.
func (s *Store) Get(ctx context.Context, id int64) (*Account, error) {
	row := s.pool.QueryRow(ctx,
		`SELECT`+selectColumns+`FROM pop3_accounts WHERE id = $1`, id)
	a, err := scanRow(row)
	if err != nil {
		return nil, fmt.Errorf("pop3 store: get %d: %w", id, err)
	}
	return &a, nil
}

// GetPassword retrieves and decrypts the stored password for a POP3 account.
func (s *Store) GetPassword(ctx context.Context, id int64) (string, error) {
	var enc []byte
	err := s.pool.QueryRow(ctx, `SELECT password_enc FROM pop3_accounts WHERE id = $1`, id).Scan(&enc)
	if err != nil {
		return "", fmt.Errorf("pop3 store: get password: %w", err)
	}
	return decryptPassword(enc, s.encKey)
}

// Delete removes a POP3 account by ID.
func (s *Store) Delete(ctx context.Context, id int64) error {
	tag, err := s.pool.Exec(ctx, `DELETE FROM pop3_accounts WHERE id = $1`, id)
	if err != nil {
		return fmt.Errorf("pop3 store: delete: %w", err)
	}
	if tag.RowsAffected() == 0 {
		return fmt.Errorf("pop3 store: account %d not found", id)
	}
	return nil
}

// DeleteByOwner removes all POP3 accounts belonging to the given username.
func (s *Store) DeleteByOwner(ctx context.Context, username string) (int, error) {
	tag, err := s.pool.Exec(ctx, `DELETE FROM pop3_accounts WHERE owner = $1`, username)
	if err != nil {
		return 0, fmt.Errorf("pop3 store: delete by owner: %w", err)
	}
	return int(tag.RowsAffected()), nil
}

// UpdateStatus updates the import progress and status of an account.
func (s *Store) UpdateStatus(ctx context.Context, id int64, status, errMsg string, current, total int) error {
	_, err := s.pool.Exec(ctx, `
		UPDATE pop3_accounts
		SET status = $1, error_msg = $2, progress_current = $3, progress_total = $4
		WHERE id = $5`, status, errMsg, current, total, id)
	if err != nil {
		return fmt.Errorf("pop3 store: update status: %w", err)
	}
	return nil
}

// UpdateDone marks an import as completed, setting status back to idle.
func (s *Store) UpdateDone(ctx context.Context, id int64, count int) error {
	_, err := s.pool.Exec(ctx, `
		UPDATE pop3_accounts
		SET status = 'idle', error_msg = '', last_import_at = now(),
		    last_import_count = $1, progress_current = 0, progress_total = 0
		WHERE id = $2`, count, id)
	if err != nil {
		return fmt.Errorf("pop3 store: update done: %w", err)
	}
	return nil
}

// encryptPassword encrypts a plaintext password using AES-256-GCM.
func encryptPassword(plaintext string, key [32]byte) ([]byte, error) {
	block, err := aes.NewCipher(key[:])
	if err != nil {
		return nil, err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, err
	}
	return gcm.Seal(nonce, nonce, []byte(plaintext), nil), nil
}

// decryptPassword decrypts a password previously encrypted with encryptPassword.
func decryptPassword(ciphertext []byte, key [32]byte) (string, error) {
	block, err := aes.NewCipher(key[:])
	if err != nil {
		return "", err
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", err
	}
	nonceSize := gcm.NonceSize()
	if len(ciphertext) < nonceSize {
		return "", fmt.Errorf("ciphertext too short")
	}
	nonce, ct := ciphertext[:nonceSize], ciphertext[nonceSize:]
	plaintext, err := gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return "", fmt.Errorf("decrypt failed: %w", err)
	}
	return string(plaintext), nil
}
