package server

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

	_ "github.com/jackc/pgx/v5/stdlib" // PostgreSQL driver

	"heckel.io/ntfy/v2/util"
)

// PostgreSQL schema version (starts at latest, no migrations needed initially)
const pgCurrentSchemaVersion = 1

// Messages cache
const (
	pgCreateMessagesTableQuery = `
		BEGIN;
		CREATE TABLE IF NOT EXISTS messages (
			id SERIAL PRIMARY KEY,
			mid TEXT NOT NULL,
			time INT NOT NULL,
			expires INT NOT NULL,
			topic TEXT NOT NULL,
			message TEXT NOT NULL,
			title TEXT NOT NULL,
			priority INT NOT NULL,
			tags TEXT NOT NULL,
			click TEXT NOT NULL,
			icon TEXT NOT NULL,			
			actions TEXT NOT NULL,
			attachment_name TEXT NOT NULL,
			attachment_type TEXT NOT NULL,
			attachment_size INT NOT NULL,
			attachment_expires INT NOT NULL,
			attachment_url TEXT NOT NULL,
			attachment_deleted BOOLEAN NOT NULL,
			sender TEXT NOT NULL,
			"user" TEXT NOT NULL,
			content_type TEXT NOT NULL,
			encoding TEXT NOT NULL,
			published BOOLEAN NOT NULL
		);
		CREATE INDEX IF NOT EXISTS idx_mid ON messages (mid);
		CREATE INDEX IF NOT EXISTS idx_time ON messages (time);
		CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
		CREATE INDEX IF NOT EXISTS idx_expires ON messages (expires);
		CREATE INDEX IF NOT EXISTS idx_sender ON messages (sender);
		CREATE INDEX IF NOT EXISTS idx_user ON messages ("user");
		CREATE INDEX IF NOT EXISTS idx_attachment_expires ON messages (attachment_expires);
		CREATE TABLE IF NOT EXISTS stats (
			key TEXT PRIMARY KEY,
			value INT
		);
		INSERT INTO stats (key, value) VALUES ('messages', 0);
		CREATE TABLE IF NOT EXISTS schemaVersion (
			id INT PRIMARY KEY,
			version INT NOT NULL
		);
		INSERT INTO schemaVersion VALUES (1, 1);
		COMMIT;
	`

	pgSelectMessagesCountQuery = `SELECT COUNT(*) FROM messages`
)

var (
	pgMessageCacheQueries = &messageCacheQueries{
		insertMessage: `
			INSERT INTO messages (mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_deleted, sender, "user", content_type, encoding, published)
			VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22)
		`,
		deleteMessage:                `DELETE FROM messages WHERE mid = $1`,
		updateMessagesForTopicExpiry: `UPDATE messages SET expires = $1 WHERE topic = $2`,
		selectRowIDFromMessageID:     `SELECT id FROM messages WHERE mid = $1`,
		selectMessagesByID: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE mid = $1
	`,
		selectMessagesSinceTime: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE topic = $1 AND time >= $2 AND published = TRUE
		ORDER BY time, id
	`,
		selectMessagesSinceTimeIncludeScheduled: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE topic = $1 AND time >= $2
		ORDER BY time, id
	`,
		selectMessagesSinceID: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE topic = $1 AND id > $2 AND published = TRUE
		ORDER BY time, id
	`,
		selectMessagesSinceIDIncludeScheduled: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE topic = $1 AND (id > $2 OR published = FALSE)
		ORDER BY time, id
	`,
		selectMessagesLatest: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE topic = $1 AND published = TRUE
		ORDER BY time DESC, id DESC
		LIMIT 1
	`,
		selectMessagesDue: `
		SELECT mid, time, expires, topic, message, title, priority, tags, click, icon, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, sender, "user", content_type, encoding
		FROM messages
		WHERE time <= $1 AND published = FALSE
		ORDER BY time, id
	`,
		selectMessagesExpired:      `SELECT mid FROM messages WHERE expires <= $1 AND published = TRUE`,
		updateMessagePublished:     `UPDATE messages SET published = TRUE WHERE mid = $1`,
		selectMessageCountPerTopic: `SELECT topic, COUNT(*) FROM messages GROUP BY topic`,
		selectTopics:               `SELECT topic FROM messages GROUP BY topic`,

		updateAttachmentDeleted:       `UPDATE messages SET attachment_deleted = TRUE WHERE mid = $1`,
		selectAttachmentsExpired:      `SELECT mid FROM messages WHERE attachment_expires > 0 AND attachment_expires <= $1 AND attachment_deleted = FALSE`,
		selectAttachmentsSizeBySender: `SELECT COALESCE(SUM(attachment_size), 0) FROM messages WHERE "user" = '' AND sender = $1 AND attachment_expires >= $2`,
		selectAttachmentsSizeByUserID: `SELECT COALESCE(SUM(attachment_size), 0) FROM messages WHERE "user" = $1 AND attachment_expires >= $2`,

		selectStats: `SELECT value FROM stats WHERE key = 'messages'`,
		updateStats: `UPDATE stats SET value = $1 WHERE key = 'messages'`,
	}
)

type pgMessageCache struct {
	*commonMessageCache
}

var _ MessageCache = (*pgMessageCache)(nil)

// newPgCache creates a PostgreSQL-backed message cache
func newPgCache(connectionString, startupQueries string, batchSize int, batchTimeout time.Duration) (*pgMessageCache, error) {
	db, err := sql.Open("pgx", connectionString)
	if err != nil {
		return nil, err
	}
	if err := setupPgMessagesDB(db, startupQueries); err != nil {
		return nil, err
	}
	var queue *util.BatchingQueue[*message]
	if batchSize > 0 || batchTimeout > 0 {
		queue = util.NewBatchingQueue[*message](batchSize, batchTimeout)
	}
	cache := &pgMessageCache{
		commonMessageCache: &commonMessageCache{
			db:      db,
			queue:   queue,
			queries: pgMessageCacheQueries,
		},
	}
	go cache.processMessageBatches()
	return cache, nil
}

func setupPgMessagesDB(db *sql.DB, startupQueries string) error {
	// Run startup queries
	if startupQueries != "" {
		if _, err := db.Exec(startupQueries); err != nil {
			return err
		}
	}

	// If 'messages' table does not exist, this must be a new database
	rowsMC, err := db.Query(pgSelectMessagesCountQuery)
	if err != nil {
		return setupNewPgCacheDB(db)
	}
	rowsMC.Close()

	// Future: Add migrations here when schema changes
	return nil
}

func setupNewPgCacheDB(db *sql.DB) error {
	if _, err := db.Exec(pgCreateMessagesTableQuery); err != nil {
		return err
	}
	return nil
}

// isPostgres checks if the connection string indicates a PostgreSQL database
func isPostgres(connectionString string) bool {
	return strings.HasPrefix(connectionString, "postgres:")
}
