package delivery

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

	"github.com/tidbcloud/alertstore/pkg/alert"
	"github.com/tidbcloud/alertstore/pkg/jira"
	"github.com/tidbcloud/alertstore/pkg/metrics"
)

// The Jira mirror. This file plus pkg/jira is the whole integration: deleting
// both, the three task kinds in task.go and the dispatcher's gate removes it.

// runJiraCreate creates the issue and records its key, which is both the link
// people follow and the idempotency guard — a retry after a successful create
// finds the key and stops.
func (w *Worker) runJiraCreate(ctx context.Context, a *alert.Alert) error {
	if !w.jira.Enabled() {
		return permanent(errors.New("jira sync is not configured"))
	}
	if a.ExternalIssueKey != "" {
		return nil // already mirrored
	}

	var attrs alert.AlertAttrs
	if err := json.Unmarshal(a.Attrs, &attrs); err != nil {
		return permanent(fmt.Errorf("unmarshal alert attrs: %w", err))
	}

	instances, err := w.instanceStore.ListByAlertID(ctx, a.ID)
	if err != nil {
		return fmt.Errorf("list instances for jira issue: %w", err)
	}

	issue := jira.BuildIssue(w.jira.Config(), a, &attrs, instances, alertWebURL(w.webUIBaseURL, a.ID))
	key, err := w.jira.CreateIssue(ctx, issue)
	if err != nil {
		metrics.JiraIssues.WithLabelValues("error").Inc()
		return classifyJiraError(err)
	}
	metrics.JiraIssues.WithLabelValues("created").Inc()

	if err := w.alertStore.SetExternalIssueKey(ctx, a.ID, key); err != nil {
		// The issue exists; failing here would create a second one on retry. Log
		// loudly and carry on — the audit entry below is then the only trace.
		w.logger.Error("store jira issue key", "err", err, "alert_id", a.ID, "issue_key", key)
	}
	a.ExternalIssueKey = key
	w.auditStore.Log(ctx, a.ID, "jira_created", "", "", "system", "system", map[string]interface{}{
		"issue_key": key,
		"url":       w.jira.IssueURL(key),
		"project":   w.jira.Config().Project,
	})

	// The card was posted before the issue existed, so this is what makes the
	// mirror visible where people are looking.
	if err := w.tasks.Enqueue(ctx, a.ID, KindIMCardRefresh, "jira:"+key, nil); err != nil {
		return fmt.Errorf("queue card refresh for jira link: %w", err)
	}
	return nil
}

// runJiraComment reproduces jiralert's changelog comments for whoever still
// reads the issue.
func (w *Worker) runJiraComment(ctx context.Context, t *Task, a *alert.Alert) error {
	var p jiraCommentPayload
	if err := json.Unmarshal(t.Payload.Raw(), &p); err != nil {
		return permanent(fmt.Errorf("unmarshal jira comment payload: %w", err))
	}
	if a.ExternalIssueKey == "" {
		return errors.New("alert has no jira issue yet") // create has not run; retry
	}
	if err := w.jira.AddComment(ctx, a.ExternalIssueKey, timestamped(p.Text)); err != nil {
		return classifyJiraError(err)
	}
	return nil
}

// runJiraTransition moves the issue to match an alertstore transition and
// comments on why, so it reads as a record rather than a status that changed by
// itself.
//
// The move goes first: a workflow that does not offer it is a real answer, not a
// failure, and the comment still goes in. That ordering also keeps the retry
// honest, since the comment is only written once the move is settled.
func (w *Worker) runJiraTransition(ctx context.Context, t *Task, a *alert.Alert) error {
	var p jiraTransitionPayload
	if err := json.Unmarshal(t.Payload.Raw(), &p); err != nil {
		return permanent(fmt.Errorf("unmarshal jira transition payload: %w", err))
	}
	if a.ExternalIssueKey == "" {
		return errors.New("alert has no jira issue yet")
	}

	transitionName := jira.TransitionFor(p.State)
	if transitionName == "" {
		return nil // a state with no Jira meaning; see jira.TransitionFor
	}
	var comment string
	switch p.State {
	case alert.StateAcked:
		comment = fmt.Sprintf("Acknowledged in alertstore by %s.", actorOr(a.AckedBy, a.AssignedTo))
	case alert.StateResolved:
		comment = fmt.Sprintf("Resolved in alertstore (resolution: %s).", orDash(a.Resolution))
		if a.ResolutionNote != "" {
			comment += " " + a.ResolutionNote + "."
		}
	}
	if url := alertWebURL(w.webUIBaseURL, a.ID); url != "" {
		comment += " " + url
	}

	moved, err := w.jira.Transition(ctx, a.ExternalIssueKey, transitionName)
	if err != nil {
		return classifyJiraError(err)
	}
	if !moved && transitionName != "" {
		w.logger.Warn("jira transition unavailable",
			"alert_id", a.ID, "issue_key", a.ExternalIssueKey, "transition", transitionName)
	}

	if err := w.jira.AddComment(ctx, a.ExternalIssueKey, timestamped(comment)); err != nil {
		return classifyJiraError(err)
	}
	w.auditStore.Log(ctx, a.ID, "jira_synced", "", "", "system", "system", map[string]interface{}{
		"issue_key":  a.ExternalIssueKey,
		"state":      p.State,
		"transition": transitionName,
		"moved":      moved,
	})
	return nil
}

// timestamped prefixes a comment the way jiralert did, in both offices' local
// time.
func timestamped(text string) string {
	now := time.Now()
	beijing, err := time.LoadLocation("Asia/Shanghai")
	if err != nil {
		return text
	}
	pacific, err := time.LoadLocation("America/Los_Angeles")
	if err != nil {
		return text
	}
	return fmt.Sprintf("[%s | %s]\n%s",
		now.In(beijing).Format("2006-01-02 15:04:05 -07"),
		now.In(pacific).Format("2006-01-02 15:04:05 -07"),
		text,
	)
}

// classifyJiraError stops the outbox retrying a rejection Jira will repeat, so a
// malformed field surfaces now rather than in forty-five minutes.
func classifyJiraError(err error) error {
	if errors.Is(err, jira.ErrPermanent) {
		return permanent(err)
	}
	return err
}

func actorOr(candidates ...string) string {
	for _, candidate := range candidates {
		if candidate != "" {
			return candidate
		}
	}
	return "system"
}

func orDash(value string) string {
	if value == "" {
		return "-"
	}
	return value
}
