package delivery

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

	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/im"
)

// ackReminderDelay reports how long to wait before firing a given step of the
// ack-reminder ladder. Step 0's delay is measured from the ack; every later
// step chains off the previous one, so the returned value is the gap to the
// next reminder, not an absolute offset from the ack.
//
// The ladder is hardcoded, one sequence per classification, and deliberately
// never returns 0 for a worked class: it nags until the alert is resolved. A
// resolve (or a reassign that unacks) cancels the pending task; there is no
// terminal step to reach.
//
// page   : ack+1h, +2h, +4h, then every 8h
// ticket : ack+4h, +8h,      then every 24h
//
// A page that has been claimed but left unresolved is chased harder than a
// ticket, mirroring the same gap in urgency the phone-call ladder draws before
// the ack. Reminders start only after the ack, so the pre-ack window — where
// pages have their own escalation and tickets sit silently — is untouched.
func ackReminderDelay(classification string, step int) time.Duration {
	switch classification {
	case alert.ClassPage:
		switch step {
		case 0, 1:
			return time.Hour
		case 2:
			return 2 * time.Hour
		default:
			return 8 * time.Hour
		}
	case alert.ClassTicket:
		switch step {
		case 0, 1:
			return 4 * time.Hour
		default:
			return 24 * time.Hour
		}
	default:
		// logging and anything else is not a worked alert here — no reminder.
		return 0
	}
}

// runAckReminder nudges the person who acked an alert that is still unresolved,
// then schedules the next nudge. It posts into the alert's own topic and
// @-mentions the acker: the IM provider has no primitive for a private message
// carrying our own text, and a topic reply is recorded on the timeline besides.
func (w *Worker) runAckReminder(ctx context.Context, t *Task, a *alert.Alert) error {
	var p ackReminderPayload
	if err := json.Unmarshal(t.Payload.Raw(), &p); err != nil {
		return permanent(fmt.Errorf("unmarshal ack reminder payload: %w", err))
	}

	// Stop the moment the alert leaves the acked state, or a newer ack has
	// superseded this chain. Self-cancelling like the escalation ladder
	// (escalation.go): a resolve that races the poll never nags, even before
	// CancelPending has run.
	if a.State != alert.StateAcked || a.AckedAt == nil || a.AckedAt.UnixMilli() != p.AckGen {
		return nil
	}
	if a.IMMessageID == "" {
		// The topic task has not run yet; retry until it has.
		return errors.New("alert has no IM topic yet")
	}

	mention := a.AckedBy
	if a.AckedBy != "" {
		if uid, err := w.im.GetUserIDByEmail(ctx, a.AckedBy); err == nil {
			mention = im.MentionTag(uid, a.AckedBy)
		}
	}

	firing, err := w.instanceStore.CountFiringByAlertID(ctx, a.ID)
	if err != nil {
		return fmt.Errorf("count firing for ack reminder: %w", err)
	}

	content, _ := json.Marshal(map[string]string{
		"text": ackReminderText(mention, time.Since(*a.AckedAt), firing),
	})
	if err := w.im.ReplyToThread(ctx, a.IMMessageID, string(content)); err != nil {
		return fmt.Errorf("send ack reminder: %w", err)
	}
	w.auditStore.Log(ctx, a.ID, "ack_reminder", "", "", "system", "system", map[string]interface{}{
		"step":   p.Step,
		"firing": firing,
	})

	next := p.Step + 1
	gap := ackReminderDelay(a.Classification, next)
	if gap <= 0 {
		return nil
	}
	dedupKey := fmt.Sprintf("ackrem:%d:%d", p.AckGen, next)
	return w.tasks.EnqueueAt(ctx, a.ID, KindAckReminder, dedupKey, ackReminderPayload{
		Step:   next,
		AckGen: p.AckGen,
	}, gap)
}

// ackReminderText renders one reminder. Whether the underlying condition is
// still firing rides in the copy — to help the acker decide — but never in the
// logic: the alert is nagged because it is unresolved, not because it is
// firing, so a recovered-but-unclosed alert is reminded to be closed out.
func ackReminderText(mention string, sinceAck time.Duration, firing int) string {
	text := fmt.Sprintf("%s this alert was acked %s ago and is still unresolved.",
		mention, humanizeSince(sinceAck))
	if firing > 0 {
		text += fmt.Sprintf(" %d instance(s) are still firing — please keep handling it, or reassign if you can't.", firing)
	} else {
		text += " All instances have recovered — please close it out with a resolution (mark it noise if it should not have fired)."
	}
	return text
}

// humanizeSince renders an ack age in whole hours, the resolution the ladder
// works at (its shortest gap is an hour).
func humanizeSince(d time.Duration) string {
	h := int(d.Round(time.Hour) / time.Hour)
	if h < 1 {
		h = 1
	}
	return fmt.Sprintf("%dh", h)
}
