package main

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

// Notification kinds, ordered by priority (highest first). When a single
// message triggers multiple kinds for one user, the highest-priority kind
// is the one persisted.
const (
	NotifKindMention      = "mention"
	NotifKindAssignedChat = "assigned_chat"
	NotifKindReply        = "reply"
)

func notifKindPriority(k string) int {
	switch k {
	case NotifKindMention:
		return 3
	case NotifKindAssignedChat:
		return 2
	case NotifKindReply:
		return 1
	}
	return 0
}

type Notification struct {
	ID         string  `json:"id"`
	UserID     string  `json:"user_id"`
	CardID     string  `json:"card_id"`
	MessageID  string  `json:"message_id"`
	Kind       string  `json:"kind"`
	ActorID    string  `json:"actor_id"`
	CreatedAt  string  `json:"created_at"`
	ReadAt     *string `json:"read_at"`
	CardTitle  string  `json:"card_title"`
	CardSeqNum int     `json:"card_seq_num"`
	ActorName  string  `json:"actor_name"`
	Snippet    string  `json:"snippet"`
}

type CardMention struct {
	ID        string `json:"id"`
	CardID    string `json:"card_id"`
	MessageID string `json:"message_id"`
	UserID    string `json:"user_id"`
	CreatedAt string `json:"created_at"`
}

var mentionRe = regexp.MustCompile(`(?i)@([a-z0-9][a-z0-9_.-]{0,63})`)

// extractMentions returns the set of @usernames referenced in body, lowercased.
// The leading '@' is not included. Each username is returned at most once.
func extractMentions(body string) []string {
	matches := mentionRe.FindAllStringSubmatch(body, -1)
	if len(matches) == 0 {
		return nil
	}
	seen := map[string]struct{}{}
	out := make([]string, 0, len(matches))
	for _, m := range matches {
		u := strings.ToLower(m[1])
		if _, ok := seen[u]; ok {
			continue
		}
		seen[u] = struct{}{}
		out = append(out, u)
	}
	return out
}

// CreateCardMessageAndNotify wraps CreateCardMessage with mention parsing,
// notification fan-out and pub/sub publication. The returned slice contains
// the user_ids that received a notification (useful for tests).
func (db *DB) CreateCardMessageAndNotify(cardID, authorID, body string, hub *EventHub) (*CardMessage, []Notification, []CardMention, error) {
	msg, err := db.CreateCardMessage(cardID, authorID, body)
	if err != nil {
		return nil, nil, nil, err
	}

	mentions, err := db.resolveAndStoreMentions(cardID, msg.ID, body)
	if err != nil {
		return msg, nil, nil, err
	}

	notifs, err := db.fanoutNotifications(cardID, msg, authorID, mentions)
	if err != nil {
		return msg, nil, mentions, err
	}

	if hub != nil {
		hub.PublishJSON("message.created", cardID, "", msg)
		for _, n := range notifs {
			hub.PublishJSON("notification.created", cardID, n.UserID, n)
		}
	}
	return msg, notifs, mentions, nil
}

// resolveAndStoreMentions parses @usernames from body, resolves them to
// existing user_ids (silently ignoring unknowns) and persists the matches
// in card_mentions.
func (db *DB) resolveAndStoreMentions(cardID, messageID, body string) ([]CardMention, error) {
	usernames := extractMentions(body)
	if len(usernames) == 0 {
		return nil, nil
	}
	placeholders := strings.Repeat("?,", len(usernames))
	placeholders = placeholders[:len(placeholders)-1]
	args := make([]interface{}, 0, len(usernames))
	for _, u := range usernames {
		args = append(args, u)
	}
	rows, err := db.conn.Query(
		fmt.Sprintf(`SELECT id, username FROM users WHERE username IN (%s)`, placeholders),
		args...,
	)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	resolved := map[string]string{}
	for rows.Next() {
		var id, uname string
		if err := rows.Scan(&id, &uname); err != nil {
			return nil, err
		}
		resolved[uname] = id
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	if len(resolved) == 0 {
		return nil, nil
	}
	now := time.Now().UTC().Format(time.RFC3339)
	out := make([]CardMention, 0, len(resolved))
	for _, userID := range resolved {
		m := CardMention{ID: newID(), CardID: cardID, MessageID: messageID, UserID: userID, CreatedAt: now}
		if _, err := db.conn.Exec(
			`INSERT INTO card_mentions (id, card_id, message_id, user_id, created_at) VALUES (?, ?, ?, ?, ?)`,
			m.ID, m.CardID, m.MessageID, m.UserID, m.CreatedAt,
		); err != nil {
			return out, err
		}
		out = append(out, m)
	}
	return out, nil
}

// fanoutNotifications computes the recipient set for a new message and
// inserts one notification row per recipient with the highest-priority kind.
//
// Recipients = {assignee_id of card} ∪ {previous authors of card_messages
// on this card} ∪ {users mentioned in this message} \ {author}.
//
// Kind precedence: mention > assigned_chat > reply.
func (db *DB) fanoutNotifications(cardID string, msg *CardMessage, authorID string, mentions []CardMention) ([]Notification, error) {
	recipients := map[string]string{} // userID -> kind

	upgrade := func(userID, kind string) {
		if userID == "" || userID == authorID {
			return
		}
		existing, ok := recipients[userID]
		if !ok || notifKindPriority(kind) > notifKindPriority(existing) {
			recipients[userID] = kind
		}
	}

	// Previous authors on this card.
	rows, err := db.conn.Query(
		`SELECT DISTINCT author_id FROM card_messages
		 WHERE card_id = ? AND author_id IS NOT NULL AND author_id != '' AND id != ?`,
		cardID, msg.ID,
	)
	if err != nil {
		return nil, err
	}
	for rows.Next() {
		var uid sql.NullString
		if err := rows.Scan(&uid); err != nil {
			rows.Close()
			return nil, err
		}
		if uid.Valid {
			upgrade(uid.String, NotifKindReply)
		}
	}
	rows.Close()

	// Assignee.
	var assignee sql.NullString
	if err := db.conn.QueryRow(`SELECT assignee_id FROM cards WHERE id = ?`, cardID).Scan(&assignee); err != nil && err != sql.ErrNoRows {
		return nil, err
	}
	if assignee.Valid {
		upgrade(assignee.String, NotifKindAssignedChat)
	}

	// Mentions (highest priority).
	for _, m := range mentions {
		upgrade(m.UserID, NotifKindMention)
	}

	if len(recipients) == 0 {
		return nil, nil
	}

	now := time.Now().UTC().Format(time.RFC3339)
	out := make([]Notification, 0, len(recipients))

	// Snippet for hydrated notif payload.
	snippet := msg.Body
	if len(snippet) > 140 {
		snippet = snippet[:140] + "…"
	}
	var cardTitle string
	var cardSeq int
	_ = db.conn.QueryRow(`SELECT title, seq_num FROM cards WHERE id = ?`, cardID).Scan(&cardTitle, &cardSeq)
	var actorName string
	_ = db.conn.QueryRow(`SELECT COALESCE(NULLIF(display_name, ''), username) FROM users WHERE id = ?`, authorID).Scan(&actorName)

	for userID, kind := range recipients {
		n := Notification{
			ID: newID(), UserID: userID, CardID: cardID, MessageID: msg.ID,
			Kind: kind, ActorID: authorID, CreatedAt: now,
			CardTitle: cardTitle, CardSeqNum: cardSeq, ActorName: actorName, Snippet: snippet,
		}
		if _, err := db.conn.Exec(
			`INSERT INTO notifications (id, user_id, card_id, message_id, kind, actor_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`,
			n.ID, n.UserID, n.CardID, n.MessageID, n.Kind, n.ActorID, n.CreatedAt,
		); err != nil {
			return out, err
		}
		out = append(out, n)
	}
	return out, nil
}

// ListNotifications returns notifications for userID. If onlyUnread is true,
// already-read entries are skipped. Limit defaults to 50 when <= 0.
func (db *DB) ListNotifications(userID string, onlyUnread bool, limit int) ([]Notification, error) {
	if limit <= 0 {
		limit = 50
	}
	q := `SELECT n.id, n.user_id, n.card_id, n.message_id, n.kind, n.actor_id, n.created_at, n.read_at,
	             COALESCE(c.title, ''), COALESCE(c.seq_num, 0),
	             COALESCE(NULLIF(u.display_name, ''), u.username, ''),
	             COALESCE(m.body, '')
	      FROM notifications n
	      LEFT JOIN cards c          ON c.id = n.card_id
	      LEFT JOIN users u          ON u.id = n.actor_id
	      LEFT JOIN card_messages m  ON m.id = n.message_id
	      WHERE n.user_id = ?`
	if onlyUnread {
		q += ` AND n.read_at IS NULL`
	}
	q += ` ORDER BY n.created_at DESC LIMIT ?`
	rows, err := db.conn.Query(q, userID, limit)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := []Notification{}
	for rows.Next() {
		var n Notification
		var readAt sql.NullString
		var body string
		if err := rows.Scan(&n.ID, &n.UserID, &n.CardID, &n.MessageID, &n.Kind, &n.ActorID, &n.CreatedAt,
			&readAt, &n.CardTitle, &n.CardSeqNum, &n.ActorName, &body); err != nil {
			return nil, err
		}
		if readAt.Valid {
			s := readAt.String
			n.ReadAt = &s
		}
		if len(body) > 140 {
			n.Snippet = body[:140] + "…"
		} else {
			n.Snippet = body
		}
		out = append(out, n)
	}
	return out, rows.Err()
}

func (db *DB) CountUnreadNotifications(userID string) (int, error) {
	var n int
	err := db.conn.QueryRow(
		`SELECT COUNT(*) FROM notifications WHERE user_id = ? AND read_at IS NULL`, userID,
	).Scan(&n)
	return n, err
}

func (db *DB) MarkNotificationRead(userID, notifID string) error {
	now := time.Now().UTC().Format(time.RFC3339)
	res, err := db.conn.Exec(
		`UPDATE notifications SET read_at = ? WHERE id = ? AND user_id = ? AND read_at IS NULL`,
		now, notifID, userID,
	)
	if err != nil {
		return err
	}
	if n, _ := res.RowsAffected(); n == 0 {
		// Not an error: idempotent.
		return nil
	}
	return nil
}

func (db *DB) MarkAllNotificationsRead(userID string) (int, error) {
	now := time.Now().UTC().Format(time.RFC3339)
	res, err := db.conn.Exec(
		`UPDATE notifications SET read_at = ? WHERE user_id = ? AND read_at IS NULL`,
		now, userID,
	)
	if err != nil {
		return 0, err
	}
	n, _ := res.RowsAffected()
	return int(n), nil
}
