package delivery

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

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

const fireMasterTeam = "fire_master"

// escalation steps — hardcoded, one sequence for all page-class alerts.
// Offsets are from step 0, which a rule's page_delay can hold (see
// escalationStartDelay); the sequence chains off it, so the whole ladder shifts
// with it and keeps this spacing.
//
// step0    oncaller
// step0+8  oncaller
// step0+9  oncaller          (1min gap defeats Do-Not-Disturb)
// step0+17 backup or oncaller
// step0+25 leader
// step0+33 leader
// step0+34 leader            (1min gap, same DND pattern)
// step0+42 fire_master
//
// The sequence was sized against a one-minute wait before the first call, which
// step 0 did not implement — it went out immediately, so every later step ran a
// minute later than it was meant to. That wait is now a rule's own to ask for,
// so the offsets here are the intended ones and `page_delay: 1m` reproduces the
// absolute times the sequence was designed for.
func escalationDelay(step int) time.Duration {
	switch step {
	case 0:
		return 0
	case 1:
		return 8 * time.Minute
	case 2:
		return 1 * time.Minute
	case 3:
		return 8 * time.Minute
	case 4:
		return 8 * time.Minute
	case 5:
		return 8 * time.Minute
	case 6:
		return 1 * time.Minute
	case 7:
		return 8 * time.Minute
	default:
		return 0
	}
}

const escalationStepCount = 8

func (w *Worker) resolvePageTarget(ctx context.Context, step int, oncallTeam string, alertAssignee string) (string, error) {
	switch step {
	case 0, 1, 2:
		return alertAssignee, nil

	case 3:
		roster, err := w.oncall.Roster(ctx, oncallTeam)
		if err != nil {
			return alertAssignee, nil
		}
		if len(roster.Backup) > 0 {
			return roster.Backup[0], nil
		}
		return alertAssignee, nil

	case 4, 5, 6:
		roster, err := w.oncall.Roster(ctx, oncallTeam)
		if err != nil {
			return "", fmt.Errorf("resolve leader for team %s: %w", oncallTeam, err)
		}
		if len(roster.Managers) > 0 {
			return roster.Managers[0], nil
		}
		return "", fmt.Errorf("team %s has no managers", oncallTeam)

	case 7:
		roster, err := w.oncall.Roster(ctx, fireMasterTeam)
		if err != nil {
			return "", fmt.Errorf("resolve fire_master oncall: %w", err)
		}
		if len(roster.Primary) > 0 {
			return roster.Primary[0], nil
		}
		if len(roster.Managers) > 0 {
			return roster.Managers[0], nil
		}
		return "", fmt.Errorf("fire_master team has no oncall")

	default:
		return "", fmt.Errorf("unknown escalation step %d", step)
	}
}

func (w *Worker) runEscalation(ctx context.Context, t *Task, a *alert.Alert) error {
	var p escalationPayload
	if err := json.Unmarshal(t.Payload.Raw(), &p); err != nil {
		return permanent(fmt.Errorf("unmarshal escalation payload: %w", err))
	}

	if a.State == alert.StateAcked || a.State == alert.StateResolved {
		return nil
	}

	if a.AssignedTo == "" {
		return nil
	}

	target, err := w.resolvePageTarget(ctx, p.Step, p.OncallTeam, a.AssignedTo)
	if err != nil {
		w.logger.Error("resolve escalation target",
			"err", err, "step", p.Step, "alert_id", a.ID, "team", p.OncallTeam)
		target = a.AssignedTo
	}

	if target != "" {
		var attrs alert.AlertAttrs
		_ = json.Unmarshal(a.Attrs, &attrs)

		reason := escalationReason(p.Step, &attrs, a, w.webUIBaseURL)
		result, err := w.oncall.Page(ctx, oncall.PageRequest{
			UserEmail: target,
			Reason:    reason,
		})
		if err != nil {
			w.logger.Error("page failed",
				"err", err, "target", target, "step", p.Step, "alert_id", a.ID)
		} else {
			w.logger.Info("paged",
				"target", target, "step", p.Step, "alert_id", a.ID,
				"call_method", result.CallMethod, "call_error", result.CallError)
		}

		w.auditStore.Log(ctx, a.ID, "escalation_page", "", "", "system", "system", map[string]interface{}{
			"step":        p.Step,
			"generation":  p.Generation,
			"target":      target,
			"call_method": callMethod(result),
			"call_error":  callError(result),
			"oncall_team": p.OncallTeam,
		})
	}

	nextStep := p.Step + 1
	if nextStep >= escalationStepCount {
		return nil
	}

	delay := escalationDelay(nextStep)
	dedupKey := fmt.Sprintf("esc:%d:%d", p.Generation, nextStep)
	return w.tasks.EnqueueAt(ctx, a.ID, KindEscalation, dedupKey, escalationPayload{
		Step:       nextStep,
		Generation: p.Generation,
		OncallTeam: p.OncallTeam,
	}, delay)
}

// escalationStartDelay reports how long to hold the first call, for a rule that
// asked for a window before anyone is woken.
//
// The window is measured from the alert and not from the moment this runs: the
// caller sits behind ingest, classification, the tenant lookup and the oncall
// lookup, and a slow pipeline must not extend a delay someone sized against the
// condition itself. Whatever the pipeline already spent counts against the
// window, so a window that has run out by the time we arrive pages
// immediately — as does a rule that asked for nothing, which is every rule
// today.
//
// Only the first step is held. The rest of the sequence chains off it, so the
// ladder shifts whole and keeps its own spacing.
func escalationStartDelay(a *alert.Alert, attrs *alert.AlertAttrs, now time.Time) time.Duration {
	window := alert.PageDelay(attrs.Annotations)
	if window == 0 {
		return 0
	}
	return a.CreatedAt.Add(window).Sub(now)
}

// startEscalation enqueues the first escalation task for a page-class alert.
// generation is 0 for the initial assignment, incremented on each reassign.
func (w *Worker) startEscalation(ctx context.Context, a *alert.Alert, attrs *alert.AlertAttrs, generation int) {
	team := w.oncall.TeamRef(attrs.Labels)
	if team == "" {
		return
	}

	dedupKey := fmt.Sprintf("esc:%d:0", generation)
	payload := escalationPayload{
		Step:       0,
		Generation: generation,
		OncallTeam: team,
	}

	var err error
	if delay := escalationStartDelay(a, attrs, time.Now()); delay > 0 {
		err = w.tasks.EnqueueAt(ctx, a.ID, KindEscalation, dedupKey, payload, delay)
	} else {
		err = w.tasks.Enqueue(ctx, a.ID, KindEscalation, dedupKey, payload)
	}
	if err != nil {
		w.logger.Error("enqueue escalation start", "err", err, "alert_id", a.ID)
	}
}

func escalationReason(step int, attrs *alert.AlertAttrs, a *alert.Alert, webUIBaseURL string) string {
	header := escalationHeader(step)
	info := attrs.Alertname
	if attrs.ClusterID != "" {
		info += " | " + attrs.ClusterID
	}
	if attrs.TenantID != "" {
		info += " | tenant:" + attrs.TenantID
	}
	link := alertWebURL(webUIBaseURL, a.ID)
	if link != "" {
		return fmt.Sprintf("%s\n%s (%s)\n%s", header, info, a.ID, link)
	}
	return fmt.Sprintf("%s\n%s (%s)", header, info, a.ID)
}

func escalationHeader(step int) string {
	switch step {
	case 0:
		return "Page alert, please respond immediately"
	case 1, 2:
		return "Alert still unacked, please respond"
	case 3:
		return "Oncaller did not respond, paging backup"
	case 4:
		return "Oncaller and backup did not respond, escalating to team leader"
	case 5, 6:
		return "Alert still unacked, please follow up"
	case 7:
		return "Multiple escalations with no response, Fire Master please intervene"
	default:
		return "Alert escalation"
	}
}

func callMethod(r *oncall.PageResult) string {
	if r == nil {
		return ""
	}
	return r.CallMethod
}

func callError(r *oncall.PageResult) string {
	if r == nil {
		return ""
	}
	return r.CallError
}
