// Package timeline is the SSOT for alert progress. IM is the interaction
// surface; every discussion message in an alert's bound topic and every
// comment written via web/API is recorded here. Structured actions live in
// alert_audit; readers merge both by time.
package timeline

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/jmoiron/sqlx"

	"github.com/tidbcloud/alertstore/pkg/jsonutil"
)

const (
	KindIMMessage = "im_message" // captured from the bound IM topic
	KindComment   = "comment"    // written via web/API, mirrored to IM
	// KindAgentNote is an agent's finding. It is a distinct kind rather than a
	// comment with a robot's name on it so that a reader — and every query
	// behind a scorecard — can separate what a person said from what a machine
	// suggested without parsing the actor string.
	KindAgentNote = "agent_note"
)

type Entry struct {
	ID         int64                       `db:"id" json:"id" validate:"required"`
	AlertID    string                      `db:"alert_id" json:"alert_id" validate:"required"`
	Kind       string                      `db:"kind" json:"kind" validate:"required"`
	Actor      string                      `db:"actor" json:"actor" validate:"required"`
	Source     string                      `db:"source" json:"source" validate:"required"`
	Text       string                      `db:"text" json:"text" validate:"required"`
	RefID      *string                     `db:"ref_id" json:"ref_id,omitempty"`
	Detail     jsonutil.NullableRawMessage `db:"detail" json:"detail,omitempty" swaggertype:"object"`
	CreatedAt  time.Time                   `db:"created_at" json:"created_at" validate:"required"`
	RecalledAt *time.Time                  `db:"recalled_at" json:"recalled_at,omitempty"`
}

type Store struct {
	db *sqlx.DB
}

func NewStore(db *sqlx.DB) *Store {
	return &Store{db: db}
}

// Insert records a timeline entry and returns its ID. If refID is
// non-empty it is unique, so redelivered IM events are no-ops (returning
// ID 0).
func (s *Store) Insert(ctx context.Context, alertID, kind, actor, source, text, refID string, detail map[string]interface{}) (int64, error) {
	var refVal interface{}
	if refID != "" {
		refVal = refID
	}
	var detailJSON interface{}
	if detail != nil {
		b, err := json.Marshal(detail)
		if err != nil {
			return 0, fmt.Errorf("marshal timeline detail: %w", err)
		}
		detailJSON = b
	}

	const q = `
		INSERT INTO alert_timeline (alert_id, kind, actor, source, text, ref_id, detail)
		VALUES (?, ?, ?, ?, ?, ?, ?)
		ON DUPLICATE KEY UPDATE id = id
	`
	res, err := s.db.ExecContext(ctx, q, alertID, kind, actor, source, text, refVal, detailJSON)
	if err != nil {
		return 0, fmt.Errorf("insert timeline entry: %w", err)
	}
	id, _ := res.LastInsertId()
	return id, nil
}

// MarkRecalled flags the entry for an IM message as recalled. The text is
// kept — the record is the SSOT, recall only annotates it.
func (s *Store) MarkRecalled(ctx context.Context, refID string) error {
	const q = `UPDATE alert_timeline SET recalled_at = NOW(3) WHERE ref_id = ? AND recalled_at IS NULL`
	_, err := s.db.ExecContext(ctx, q, refID)
	return err
}

func (s *Store) ListByAlertID(ctx context.Context, alertID string) ([]Entry, error) {
	const q = `SELECT * FROM alert_timeline WHERE alert_id = ? ORDER BY created_at ASC, id ASC`
	var entries []Entry
	if err := s.db.SelectContext(ctx, &entries, q, alertID); err != nil {
		return nil, fmt.Errorf("list timeline: %w", err)
	}
	return entries, nil
}
