package main

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

	"hamlog/internal/applog"

	wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
)

// Multi-operator chat over the SHARED MySQL logbook. The database is the message
// bus: each OpsLog INSERTs into chat_messages and polls for new rows (~3 s), so
// operators on the same shared log (e.g. a special-event call) can talk. No
// extra server. Presence is a lightweight heartbeat into chat_presence. Chat is
// only available on a MySQL logbook (SQLite/solo has no one else to talk to).

const (
	chatPollInterval      = 3 * time.Second
	chatPresenceEvery     = 20 * time.Second
	chatRetentionDays     = 7
	chatHistoryDefault    = 80
	chatPresenceStaleSecs = 120 // a presence row older than this = offline
)

// ChatMessage is one chat line.
type ChatMessage struct {
	ID        int64  `json:"id"`
	Operator  string `json:"operator"`
	Station   string `json:"station"`
	Message   string `json:"message"`
	CreatedAt string `json:"created_at"` // ISO UTC
}

// ChatPresence is one operator currently online (recent heartbeat).
type ChatPresence struct {
	Operator string `json:"operator"`
	Station  string `json:"station"`
	AgoSecs  int    `json:"ago_secs"`
}

// chatActive reports whether chat can run (shared MySQL logbook).
func (a *App) chatActive() bool {
	return a.logDb != nil && a.dbBackend == "mysql"
}

// ChatAvailable lets the UI show/hide the chat icon (only on a shared log).
func (a *App) ChatAvailable() bool { return a.chatActive() }

func (a *App) ensureChatTables() error {
	if _, err := a.logDb.ExecContext(a.ctx,
		"CREATE TABLE IF NOT EXISTS chat_messages ("+
			"id BIGINT AUTO_INCREMENT PRIMARY KEY, "+
			"operator VARCHAR(32), station VARCHAR(32), "+
			"message TEXT, created_at DATETIME)"); err != nil {
		return err
	}
	_, err := a.logDb.ExecContext(a.ctx,
		"CREATE TABLE IF NOT EXISTS chat_presence ("+
			"operator VARCHAR(32) PRIMARY KEY, station VARCHAR(32), updated_at DATETIME)")
	return err
}

// SendChatMessage posts a message to the shared chat and returns the stored row
// (with its id) so the UI can show it immediately; the poll loop dedupes by id.
func (a *App) SendChatMessage(text string) (ChatMessage, error) {
	text = strings.TrimSpace(text)
	if text == "" {
		return ChatMessage{}, nil
	}
	if len(text) > 1000 {
		text = text[:1000]
	}
	if !a.chatActive() {
		return ChatMessage{}, fmt.Errorf("chat is only available on a shared MySQL logbook")
	}
	op, station := a.liveStatusOperator()
	if op == "" {
		return ChatMessage{}, fmt.Errorf("set your callsign/operator in Settings → Station first")
	}
	if err := a.ensureChatTables(); err != nil {
		return ChatMessage{}, err
	}
	res, err := a.logDb.ExecContext(a.ctx,
		"INSERT INTO chat_messages (operator, station, message, created_at) VALUES (?, ?, ?, UTC_TIMESTAMP())",
		op, station, text)
	if err != nil {
		return ChatMessage{}, err
	}
	id, _ := res.LastInsertId()
	return ChatMessage{ID: id, Operator: op, Station: station, Message: text,
		CreatedAt: time.Now().UTC().Format(time.RFC3339)}, nil
}

// GetChatHistory returns the most recent messages (oldest first) for the panel.
func (a *App) GetChatHistory(limit int) ([]ChatMessage, error) {
	if !a.chatActive() {
		return nil, nil
	}
	if limit <= 0 || limit > 500 {
		limit = chatHistoryDefault
	}
	if err := a.ensureChatTables(); err != nil {
		return nil, err
	}
	rows, err := a.logDb.QueryContext(a.ctx,
		"SELECT id, operator, station, message, created_at FROM chat_messages ORDER BY id DESC LIMIT ?", limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []ChatMessage
	for rows.Next() {
		var m ChatMessage
		if err := rows.Scan(&m.ID, &m.Operator, &m.Station, &m.Message, &m.CreatedAt); err != nil {
			return nil, err
		}
		out = append(out, m)
	}
	// Reverse to chronological order (we queried newest-first to honour LIMIT).
	for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
		out[i], out[j] = out[j], out[i]
	}
	return out, rows.Err()
}

// GetOnlineOperators lists operators with a recent presence heartbeat.
func (a *App) GetOnlineOperators() ([]ChatPresence, error) {
	if !a.chatActive() {
		return nil, nil
	}
	if err := a.ensureChatTables(); err != nil {
		return nil, err
	}
	rows, err := a.logDb.QueryContext(a.ctx,
		"SELECT operator, station, TIMESTAMPDIFF(SECOND, updated_at, UTC_TIMESTAMP()) AS ago "+
			"FROM chat_presence WHERE updated_at > UTC_TIMESTAMP() - INTERVAL ? SECOND ORDER BY operator",
		chatPresenceStaleSecs)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var out []ChatPresence
	for rows.Next() {
		var p ChatPresence
		if err := rows.Scan(&p.Operator, &p.Station, &p.AgoSecs); err != nil {
			return nil, err
		}
		out = append(out, p)
	}
	return out, rows.Err()
}

// chatLoop polls for new messages and heartbeats presence while on a shared
// MySQL logbook. Started once at startup; a cheap no-op otherwise.
func (a *App) chatLoop() {
	defer func() { _ = recover() }()
	var lastID int64 = -1 // -1 = not yet baselined
	var lastDB *sql.DB    // logbook the baseline belongs to
	lastPresence := time.Time{}
	lastPurge := time.Time{}
	t := time.NewTicker(chatPollInterval)
	defer t.Stop()
	for range t.C {
		if !a.chatActive() {
			lastID = -1 // re-baseline if the backend changes
			lastDB = nil
			continue
		}
		// Profile switch swaps the logbook under us: re-baseline against the new
		// DB so we don't query it with the previous log's id cursor.
		if a.logDb != lastDB {
			lastID = -1
			lastDB = a.logDb
		}
		if err := a.ensureChatTables(); err != nil {
			continue
		}
		now := time.Now()
		// Presence heartbeat.
		if now.Sub(lastPresence) >= chatPresenceEvery {
			if op, station := a.liveStatusOperator(); op != "" {
				_, _ = a.logDb.ExecContext(a.ctx,
					"INSERT INTO chat_presence (operator, station, updated_at) VALUES (?, ?, UTC_TIMESTAMP()) "+
						"ON DUPLICATE KEY UPDATE station=VALUES(station), updated_at=UTC_TIMESTAMP()",
					op, station)
			}
			lastPresence = now
		}
		// Baseline on first run so existing history isn't replayed as "new"
		// (the panel loads it via GetChatHistory).
		if lastID < 0 {
			row := a.logDb.QueryRowContext(a.ctx, "SELECT COALESCE(MAX(id),0) FROM chat_messages")
			_ = row.Scan(&lastID)
			continue
		}
		// Emit new messages.
		rows, err := a.logDb.QueryContext(a.ctx,
			"SELECT id, operator, station, message, created_at FROM chat_messages WHERE id > ? ORDER BY id", lastID)
		if err != nil {
			continue
		}
		for rows.Next() {
			var m ChatMessage
			if err := rows.Scan(&m.ID, &m.Operator, &m.Station, &m.Message, &m.CreatedAt); err != nil {
				continue
			}
			if m.ID > lastID {
				lastID = m.ID
			}
			if a.ctx != nil {
				wruntime.EventsEmit(a.ctx, "chat:message", m)
			}
		}
		rows.Close()
		// Purge old messages occasionally (hourly).
		if now.Sub(lastPurge) >= time.Hour {
			_, err := a.logDb.ExecContext(a.ctx,
				"DELETE FROM chat_messages WHERE created_at < UTC_TIMESTAMP() - INTERVAL ? DAY", chatRetentionDays)
			if err != nil {
				applog.Printf("chat: purge failed: %v", err)
			}
			lastPurge = now
		}
	}
}
