package main

import (
	"encoding/json"
	"regexp"
	"strings"
)

// Subset of the Gitea webhook payload we actually consume. Gitea reuses the
// same top-level structure across event types; fields are populated per event.
type GiteaEvent struct {
	Action            string       `json:"action"`
	Sender            User         `json:"sender"`
	Repository        Repository   `json:"repository"`
	Issue             *Issue       `json:"issue,omitempty"`
	Comment           *Comment     `json:"comment,omitempty"`
	PullRequest       *PullRequest `json:"pull_request,omitempty"`
	Review            *Review      `json:"review,omitempty"`
	RequestedReviewer *User        `json:"requested_reviewer,omitempty"`
	Assignee          *User        `json:"assignee,omitempty"`
	Changes           *Changes     `json:"changes,omitempty"`
}

// Changes is populated for action=="edited" events. We use it to detect when
// an edit *introduced* an @Klaus mention versus an edit to a body that already
// had one (which should not re-trigger us).
type Changes struct {
	Body  *ChangedString `json:"body,omitempty"`
	Title *ChangedString `json:"title,omitempty"`
}

type ChangedString struct {
	From string `json:"from"`
}

type User struct {
	Login    string `json:"login"`
	Username string `json:"username"`
	ID       int64  `json:"id"`
}

func (u User) Name() string {
	if u.Login != "" {
		return u.Login
	}
	return u.Username
}

type Repository struct {
	FullName      string `json:"full_name"`
	Name          string `json:"name"`
	SSHURL        string `json:"ssh_url"`
	CloneURL      string `json:"clone_url"`
	HTMLURL       string `json:"html_url"`
	DefaultBranch string `json:"default_branch"`
	Owner         User   `json:"owner"`
}

type Issue struct {
	Number      int64        `json:"number"`
	Title       string       `json:"title"`
	Body        string       `json:"body"`
	User        User         `json:"user"`
	State       string       `json:"state"`
	HTMLURL     string       `json:"html_url"`
	PullRequest *PullRefStub `json:"pull_request,omitempty"`
	Assignees   []User       `json:"assignees,omitempty"`
}

// Gitea sets issue.pull_request to a small stub when an issue_comment fires on a PR.
type PullRefStub struct {
	HTMLURL string `json:"html_url"`
}

type Comment struct {
	ID      int64  `json:"id"`
	Body    string `json:"body"`
	User    User   `json:"user"`
	HTMLURL string `json:"html_url"`
}

type PullRequest struct {
	Number             int64    `json:"number"`
	Title              string   `json:"title"`
	Body               string   `json:"body"`
	User               User     `json:"user"`
	State              string   `json:"state"`
	HTMLURL            string   `json:"html_url"`
	Head               PRBranch `json:"head"`
	Base               PRBranch `json:"base"`
	Assignees          []User   `json:"assignees,omitempty"`
	RequestedReviewers []User   `json:"requested_reviewers,omitempty"`
}

type PRBranch struct {
	Ref string `json:"ref"`
	SHA string `json:"sha"`
}

type Review struct {
	Body  string `json:"body"`
	User  User   `json:"user"`
	State string `json:"state"`
}

// Trigger describes why Klaus is being woken up. nil means: do nothing.
type Trigger struct {
	Reason     string // mention | assigned | review_requested | reply_on_klaus_pr
	Source     string // issue | issue_comment | pull_request | pull_request_comment | pull_request_review
	Body       string // text that triggered us
	Number     int64  // issue or PR number
	IsPR       bool
	BranchHint string // for PR-related triggers, the PR head ref to check out
	HTMLURL    string
}

var mentionRE = regexp.MustCompile(`(?i)(^|[^a-z0-9_])@klaus\b`)

func mentionsKlaus(s string) bool { return mentionRE.MatchString(s) }

func isKlaus(u User) bool {
	return strings.EqualFold(u.Login, "klaus") || strings.EqualFold(u.Username, "klaus")
}

// shouldFireOnBody returns true when body changes warrant waking Klaus.
// For action=="created" / "opened": fire when the body mentions Klaus.
// For action=="edited": fire only when the edit *introduced* the mention
// (mention in new body but not in changes.body.from) — prevents repeat
// triggers when someone edits an unrelated part of a comment/issue/PR that
// already mentioned Klaus.
func shouldFireOnBody(action, body string, changes *Changes) bool {
	if !mentionsKlaus(body) {
		return false
	}
	if action == "edited" {
		if changes == nil || changes.Body == nil {
			// Body wasn't part of the edit (only title/labels/etc changed).
			return false
		}
		if mentionsKlaus(changes.Body.From) {
			return false
		}
	}
	return true
}

// DecideTrigger parses the raw payload and returns a Trigger only when Klaus
// should respond. Returns (event, nil, nil) when the event is benign.
func DecideTrigger(eventType string, raw []byte) (*GiteaEvent, *Trigger, error) {
	var ev GiteaEvent
	if err := json.Unmarshal(raw, &ev); err != nil {
		return nil, nil, err
	}

	// Never react to our own events — prevents feedback loops.
	if isKlaus(ev.Sender) {
		return &ev, nil, nil
	}

	switch eventType {
	case "issues":
		if ev.Issue == nil {
			return &ev, nil, nil
		}
		if ev.Action == "assigned" && ev.Assignee != nil && isKlaus(*ev.Assignee) {
			return &ev, &Trigger{
				Reason: "assigned", Source: "issue",
				Body: ev.Issue.Body, Number: ev.Issue.Number, HTMLURL: ev.Issue.HTMLURL,
			}, nil
		}
		if (ev.Action == "opened" || ev.Action == "edited") &&
			shouldFireOnBody(ev.Action, ev.Issue.Body, ev.Changes) {
			return &ev, &Trigger{
				Reason: "mention", Source: "issue",
				Body: ev.Issue.Body, Number: ev.Issue.Number, HTMLURL: ev.Issue.HTMLURL,
			}, nil
		}

	case "issue_comment":
		if ev.Comment == nil || ev.Issue == nil {
			return &ev, nil, nil
		}
		if ev.Action != "created" && ev.Action != "edited" {
			return &ev, nil, nil
		}
		isPR := ev.Issue.PullRequest != nil
		src := "issue_comment"
		if isPR {
			src = "pull_request_comment"
		}
		if shouldFireOnBody(ev.Action, ev.Comment.Body, ev.Changes) {
			return &ev, &Trigger{
				Reason: "mention", Source: src,
				Body: ev.Comment.Body, Number: ev.Issue.Number, IsPR: isPR, HTMLURL: ev.Comment.HTMLURL,
			}, nil
		}
		if ev.Action == "created" && isPR && isKlaus(ev.Issue.User) {
			return &ev, &Trigger{
				Reason: "reply_on_klaus_pr", Source: src,
				Body: ev.Comment.Body, Number: ev.Issue.Number, IsPR: true, HTMLURL: ev.Comment.HTMLURL,
			}, nil
		}

	case "pull_request":
		if ev.PullRequest == nil {
			return &ev, nil, nil
		}
		if ev.Action == "assigned" && ev.Assignee != nil && isKlaus(*ev.Assignee) {
			return &ev, &Trigger{
				Reason: "assigned", Source: "pull_request",
				Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
				IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
			}, nil
		}
		if ev.Action == "review_requested" && ev.RequestedReviewer != nil && isKlaus(*ev.RequestedReviewer) {
			return &ev, &Trigger{
				Reason: "review_requested", Source: "pull_request",
				Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
				IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
			}, nil
		}
		if (ev.Action == "opened" || ev.Action == "edited") &&
			shouldFireOnBody(ev.Action, ev.PullRequest.Body, ev.Changes) {
			return &ev, &Trigger{
				Reason: "mention", Source: "pull_request",
				Body: ev.PullRequest.Body, Number: ev.PullRequest.Number,
				IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
			}, nil
		}

	case "pull_request_review":
		if ev.Review == nil || ev.PullRequest == nil || ev.Action != "submitted" {
			return &ev, nil, nil
		}
		if mentionsKlaus(ev.Review.Body) {
			return &ev, &Trigger{
				Reason: "mention", Source: "pull_request_review",
				Body: ev.Review.Body, Number: ev.PullRequest.Number,
				IsPR: true, BranchHint: ev.PullRequest.Head.Ref, HTMLURL: ev.PullRequest.HTMLURL,
			}, nil
		}
	}
	return &ev, nil, nil
}
