package webhook

import (
	"fmt"
	"sort"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"
)

const providerEventBodyExcerptMaxBytes = 4096

// ProviderEvent is the normalized provider event produced from a verified
// provider webhook payload. Persistence packages convert this core
// representation into their owning store types.
type ProviderEvent struct {
	Provider           string
	WorkspaceID        string
	EventKind          string
	EventID            string
	OccurredAt         time.Time
	ConnectionRef      string
	ResourceIdentifier string
	ResourceURL        string
	ActorLogin         string
	ActionKind         string
	BodyExcerpt        string
	ChangedPaths       []string
	Extra              map[string]string
}

// NormalizeGitHubProviderEvent normalizes a verified GitHub webhook into the
// provider event shape. It is pure: callers provide routing context, delivery
// key, and fallback receive time after signature/persistence lookup.
func NormalizeGitHubProviderEvent(workspaceID string, connectionRef string, deliveryKey string, eventType string, event GitHubPayload, now time.Time) (ProviderEvent, bool, error) {
	resource, eventKind, actionKind, actorLogin, bodyExcerpt, extra, ok := normalizeGitHubProviderEventPayload(eventType, event)
	if !ok {
		return ProviderEvent{}, false, nil
	}
	occurredAt := githubProviderEventOccurredAt(eventType, event, now)
	return ProviderEvent{
		Provider:           "github",
		WorkspaceID:        strings.TrimSpace(workspaceID),
		EventKind:          eventKind,
		EventID:            providerEventID("github", deliveryKey),
		OccurredAt:         occurredAt,
		ConnectionRef:      strings.TrimSpace(connectionRef),
		ResourceIdentifier: "github:" + resource.ResourceIdentifier,
		ResourceURL:        strings.TrimSpace(resource.ResourceURL),
		ActorLogin:         actorLogin,
		ActionKind:         actionKind,
		BodyExcerpt:        providerEventBodyExcerpt(bodyExcerpt),
		ChangedPaths:       githubProviderEventChangedPaths(eventType, event),
		Extra:              extra,
	}, true, nil
}

func normalizeGitHubProviderEventPayload(eventType string, event GitHubPayload) (PullRequestResource, string, string, string, string, map[string]string, bool) {
	switch strings.TrimSpace(eventType) {
	case "pull_request_review_submitted":
		resource, ok := GitHubPullRequestResourceFromPullRequest(event)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Review.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		return resource,
			"github_pull_request.review_submitted",
			NormalizeGitHubReviewState(event.Review.State),
			strings.TrimSpace(event.Review.User.Login),
			event.Review.Body,
			nil,
			true
	case "pull_request_review_comment_created":
		resource, ok := GitHubPullRequestResourceFromPullRequest(event)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Comment.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		extra := map[string]string{}
		if path := strings.TrimSpace(event.Comment.Path); path != "" {
			extra["path"] = path
		}
		if line := strings.TrimSpace(event.Comment.Line.String()); line != "" {
			extra["line"] = line
		}
		return resource,
			"github_pull_request.review_comment_created",
			"created",
			strings.TrimSpace(event.Comment.User.Login),
			event.Comment.Body,
			extra,
			true
	case "issue_comment_created":
		resource, ok, isPullRequestComment := GitHubPullRequestResourceFromIssueComment(event)
		if !isPullRequestComment || !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Comment.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		return resource,
			"github_pull_request.issue_comment_created",
			"created",
			strings.TrimSpace(event.Comment.User.Login),
			event.Comment.Body,
			nil,
			true
	case "pull_request_synchronize":
		resource, ok := GitHubPullRequestResourceFromPullRequest(event)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		return resource,
			"github_pull_request.synchronized",
			"synchronized",
			strings.TrimSpace(event.Sender.Login),
			"",
			providerPullRequestHeadSHAExtra(event.PullRequest.Head.SHA),
			true
	case "pull_request_closed":
		resource, ok := GitHubPullRequestResourceFromPullRequest(event)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		actionKind := "closed_without_merge"
		if event.PullRequest.Merged {
			actionKind = "merged"
		}
		return resource,
			"github_pull_request.closed",
			actionKind,
			strings.TrimSpace(event.Sender.Login),
			"",
			nil,
			true
	case "pull_request_reopened":
		resource, ok := GitHubPullRequestResourceFromPullRequest(event)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		return resource,
			"github_pull_request.reopened",
			"reopened",
			strings.TrimSpace(event.Sender.Login),
			"",
			nil,
			true
	case "push":
		repositoryFullName := strings.ToLower(strings.TrimSpace(event.Repository.FullName))
		branch := providerPushBranchFromRef(event.Ref)
		if repositoryFullName == "" || branch == "" {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		extra := map[string]string{
			"repository":     repositoryFullName,
			"ref":            strings.TrimSpace(event.Ref),
			"branch":         branch,
			"default_branch": strings.TrimSpace(event.Repository.DefaultBranch),
			"before":         strings.TrimSpace(event.Before),
			"after":          strings.TrimSpace(event.After),
		}
		return PullRequestResource{
				ResourceType:       "github_pull_request",
				ResourceIdentifier: repositoryFullName,
				ResourceURL:        strings.TrimSpace(event.Compare),
				RepositoryFullName: repositoryFullName,
			},
			"github_pull_request.base_branch_pushed",
			"base_branch_pushed",
			strings.TrimSpace(event.Sender.Login),
			"",
			compactProviderExtra(extra),
			true
	default:
		return PullRequestResource{}, "", "", "", "", nil, false
	}
}

func githubProviderEventChangedPaths(eventType string, event GitHubPayload) []string {
	if strings.TrimSpace(eventType) != "push" {
		return nil
	}
	return githubPushChangedPaths(event)
}

// NormalizeGiteaProviderEvent normalizes a verified Gitea webhook into the
// provider event shape. It is pure; store/app owns verification and
// persistence-bound connection/config lookup.
func NormalizeGiteaProviderEvent(workspaceID string, connectionRef string, deliveryKey string, eventType string, event GiteaPayload, now time.Time, instanceURL string) (ProviderEvent, bool, error) {
	resource, eventKind, actionKind, actorLogin, bodyExcerpt, extra, ok := normalizeGiteaProviderEventPayload(eventType, event, instanceURL)
	if !ok {
		return ProviderEvent{}, false, nil
	}
	occurredAt := giteaProviderEventOccurredAt(eventType, event, now)
	return ProviderEvent{
		Provider:           "gitea",
		WorkspaceID:        strings.TrimSpace(workspaceID),
		EventKind:          eventKind,
		EventID:            providerEventID("gitea", deliveryKey),
		OccurredAt:         occurredAt,
		ConnectionRef:      strings.TrimSpace(connectionRef),
		ResourceIdentifier: "gitea:" + resource.ResourceIdentifier,
		ResourceURL:        strings.TrimSpace(resource.ResourceURL),
		ActorLogin:         actorLogin,
		ActionKind:         actionKind,
		BodyExcerpt:        providerEventBodyExcerpt(bodyExcerpt),
		ChangedPaths:       giteaProviderEventChangedPaths(eventType, event),
		Extra:              extra,
	}, true, nil
}

func normalizeGiteaProviderEventPayload(eventType string, event GiteaPayload, instanceURL string) (PullRequestResource, string, string, string, string, map[string]string, bool) {
	switch strings.TrimSpace(eventType) {
	case "pull_request_review_submitted":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Review.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		return resource,
			"gitea_pull_request.review_submitted",
			NormalizeGiteaReviewState(event.Review.Type),
			strings.TrimSpace(event.Review.User.Login),
			event.Review.Body,
			giteaPullRequestBranchExtra(event),
			true
	case "pull_request_review_comment_created":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Comment.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		extra := map[string]string{}
		if path := strings.TrimSpace(event.Comment.Path); path != "" {
			extra["path"] = path
		}
		if event.Comment.Line > 0 {
			extra["line"] = fmt.Sprintf("%d", event.Comment.Line)
		}
		addGiteaPullRequestBranchExtra(extra, event)
		return resource,
			"gitea_pull_request.review_comment_created",
			"created",
			strings.TrimSpace(event.Comment.User.Login),
			event.Comment.Body,
			compactProviderExtra(extra),
			true
	case "issue_comment_created":
		resource, ok, isPullRequestComment := GiteaPullRequestResourceFromIssueComment(event, instanceURL)
		if !isPullRequestComment || !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		permalink := providerFirstNonEmpty(event.Comment.HTMLURL, resource.ResourceURL)
		resource.ResourceURL = permalink
		return resource,
			"gitea_pull_request.issue_comment_created",
			"created",
			strings.TrimSpace(event.Comment.User.Login),
			event.Comment.Body,
			giteaPullRequestBranchExtra(event),
			true
	case "pull_request_synchronize":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		return resource,
			"gitea_pull_request.synchronized",
			"synchronized",
			strings.TrimSpace(event.Sender.Login),
			"",
			giteaPullRequestBranchExtra(event),
			true
	case "pull_request_closed":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		actionKind := "closed_without_merge"
		if event.PullRequest.Merged {
			actionKind = "merged"
		}
		return resource,
			"gitea_pull_request.closed",
			actionKind,
			strings.TrimSpace(event.Sender.Login),
			"",
			giteaPullRequestBranchExtra(event),
			true
	case "pull_request_reopened":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		return resource,
			"gitea_pull_request.reopened",
			"reopened",
			strings.TrimSpace(event.Sender.Login),
			"",
			giteaPullRequestBranchExtra(event),
			true
	case "push":
		repositoryFullName := strings.ToLower(strings.TrimSpace(event.Repository.FullName))
		branch := providerPushBranchFromRef(event.Ref)
		if repositoryFullName == "" || branch == "" {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		extra := map[string]string{
			"repository":     repositoryFullName,
			"ref":            strings.TrimSpace(event.Ref),
			"branch":         branch,
			"default_branch": strings.TrimSpace(event.Repository.DefaultBranch),
			"before":         strings.TrimSpace(event.Before),
			"after":          strings.TrimSpace(event.After),
		}
		return PullRequestResource{
				ResourceType:       "gitea_pull_request",
				ResourceIdentifier: repositoryFullName,
				ResourceURL:        strings.TrimSpace(event.CompareURL),
				RepositoryFullName: repositoryFullName,
			},
			"gitea_pull_request.base_branch_pushed",
			"base_branch_pushed",
			strings.TrimSpace(event.Sender.Login),
			"",
			compactProviderExtra(extra),
			true
	case "status":
		repositoryFullName := strings.ToLower(strings.TrimSpace(event.Repository.FullName))
		sha := strings.TrimSpace(event.SHA)
		if repositoryFullName == "" || sha == "" || !giteaCIResultIsFailed(event.State) {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		extra := map[string]string{
			"repository":  repositoryFullName,
			"sha":         sha,
			"state":       strings.TrimSpace(event.State),
			"context":     strings.TrimSpace(event.Context),
			"target_url":  strings.TrimSpace(event.TargetURL),
			"description": strings.TrimSpace(event.Description),
		}
		return PullRequestResource{
				ResourceType:       "gitea_pull_request",
				ResourceIdentifier: repositoryFullName,
				ResourceURL:        strings.TrimSpace(event.TargetURL),
				RepositoryFullName: repositoryFullName,
			},
			"gitea_pull_request.ci_status",
			providerFirstNonEmpty(event.State, "updated"),
			strings.TrimSpace(event.Sender.Login),
			event.Description,
			compactProviderExtra(extra),
			true
	case "workflow_run":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok || !giteaCIResultIsFailed(event.WorkflowRun.Conclusion, event.WorkflowRun.Status, event.Action) {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		resource.ResourceURL = providerFirstNonEmpty(event.WorkflowRun.HTMLURL, resource.ResourceURL)
		extra := giteaWorkflowExtra("workflow", event.Workflow.Name, event.WorkflowRun.Name, event.WorkflowRun.Status, event.WorkflowRun.Conclusion, event.WorkflowRun.HeadSHA)
		return resource,
			"gitea_pull_request.workflow_run_status",
			giteaWorkflowActionKind(event.Action, event.WorkflowRun.Status, event.WorkflowRun.Conclusion),
			strings.TrimSpace(event.Sender.Login),
			"",
			extra,
			true
	case "workflow_job":
		resource, ok := GiteaPullRequestResourceFromPullRequest(event, instanceURL)
		if !ok || !giteaCIResultIsFailed(event.WorkflowJob.Conclusion, event.WorkflowJob.Status, event.Action) {
			return PullRequestResource{}, "", "", "", "", nil, false
		}
		resource.ResourceURL = providerFirstNonEmpty(event.WorkflowJob.HTMLURL, resource.ResourceURL)
		extra := giteaWorkflowExtra("job", event.Workflow.Name, event.WorkflowJob.Name, event.WorkflowJob.Status, event.WorkflowJob.Conclusion, event.WorkflowJob.HeadSHA)
		return resource,
			"gitea_pull_request.workflow_job_status",
			giteaWorkflowActionKind(event.Action, event.WorkflowJob.Status, event.WorkflowJob.Conclusion),
			strings.TrimSpace(event.Sender.Login),
			"",
			extra,
			true
	default:
		return PullRequestResource{}, "", "", "", "", nil, false
	}
}

func giteaProviderEventChangedPaths(eventType string, event GiteaPayload) []string {
	if strings.TrimSpace(eventType) != "push" {
		return nil
	}
	return giteaPushChangedPaths(event)
}

func giteaCIResultIsFailed(values ...string) bool {
	for _, value := range values {
		switch strings.ToLower(strings.TrimSpace(value)) {
		case "failure", "failed", "error":
			return true
		}
	}
	return false
}

func giteaWorkflowActionKind(action string, status string, conclusion string) string {
	return providerFirstNonEmpty(conclusion, status, action, "updated")
}

func providerPullRequestHeadSHAExtra(headSHA string) map[string]string {
	return compactProviderExtra(map[string]string{
		"head_sha": strings.TrimSpace(headSHA),
	})
}

func giteaWorkflowExtra(kind string, workflowName string, itemName string, status string, conclusion string, headSHA string) map[string]string {
	extra := map[string]string{
		"ci_kind":    strings.TrimSpace(kind),
		"workflow":   strings.TrimSpace(workflowName),
		"name":       strings.TrimSpace(itemName),
		"status":     strings.TrimSpace(status),
		"conclusion": strings.TrimSpace(conclusion),
		"head_sha":   strings.TrimSpace(headSHA),
	}
	return compactProviderExtra(extra)
}

func githubPushChangedPaths(event GitHubPayload) []string {
	if len(event.Commits) == 0 {
		return nil
	}
	seen := map[string]struct{}{}
	for _, commit := range event.Commits {
		addProviderChangedPaths(seen, commit.Added)
		addProviderChangedPaths(seen, commit.Modified)
		addProviderChangedPaths(seen, commit.Removed)
	}
	if len(seen) == 0 {
		return nil
	}
	paths := make([]string, 0, len(seen))
	for path := range seen {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	return paths
}

func giteaPushChangedPaths(event GiteaPayload) []string {
	if len(event.Commits) == 0 {
		return nil
	}
	seen := map[string]struct{}{}
	for _, commit := range event.Commits {
		addProviderChangedPaths(seen, commit.Added)
		addProviderChangedPaths(seen, commit.Modified)
		addProviderChangedPaths(seen, commit.Removed)
	}
	if len(seen) == 0 {
		return nil
	}
	paths := make([]string, 0, len(seen))
	for path := range seen {
		paths = append(paths, path)
	}
	sort.Strings(paths)
	return paths
}

func providerPushBranchFromRef(ref string) string {
	trimmed := strings.TrimSpace(ref)
	const branchPrefix = "refs/heads/"
	if !strings.HasPrefix(trimmed, branchPrefix) {
		return ""
	}
	return strings.TrimSpace(strings.TrimPrefix(trimmed, branchPrefix))
}

func addProviderChangedPaths(seen map[string]struct{}, values []string) {
	for _, value := range values {
		path := strings.TrimSpace(value)
		if path == "" {
			continue
		}
		seen[path] = struct{}{}
	}
}

func giteaPullRequestBranchExtra(event GiteaPayload) map[string]string {
	extra := map[string]string{}
	addGiteaPullRequestBranchExtra(extra, event)
	return compactProviderExtra(extra)
}

func addGiteaPullRequestBranchExtra(extra map[string]string, event GiteaPayload) {
	if extra == nil {
		return
	}
	extra["base_branch"] = strings.TrimSpace(event.PullRequest.Base.Ref)
	extra["base_sha"] = strings.TrimSpace(event.PullRequest.Base.SHA)
	extra["head_branch"] = strings.TrimSpace(event.PullRequest.Head.Ref)
	extra["head_sha"] = strings.TrimSpace(event.PullRequest.Head.SHA)
}

func compactProviderExtra(extra map[string]string) map[string]string {
	for key, value := range extra {
		if strings.TrimSpace(value) == "" {
			delete(extra, key)
		}
	}
	if len(extra) == 0 {
		return nil
	}
	return extra
}

func providerEventID(provider string, deliveryKey string) string {
	return strings.TrimSpace(strings.ToLower(provider)) + ":" + strings.TrimSpace(deliveryKey)
}

func githubProviderEventOccurredAt(eventType string, event GitHubPayload, fallback time.Time) time.Time {
	switch strings.TrimSpace(strings.ToLower(eventType)) {
	case "pull_request_review_submitted":
		return providerOccurredAt(fallback, event.Review.SubmittedAt, event.PullRequest.UpdatedAt)
	case "pull_request_review_comment_created":
		return providerOccurredAt(fallback, event.Comment.CreatedAt, event.Comment.UpdatedAt, event.PullRequest.UpdatedAt)
	case "issue_comment_created":
		return providerOccurredAt(fallback, event.Comment.CreatedAt, event.Comment.UpdatedAt, event.PullRequest.UpdatedAt)
	case "pull_request_synchronize", "pull_request_reopened":
		return providerOccurredAt(fallback, event.PullRequest.UpdatedAt)
	case "pull_request_closed":
		return providerOccurredAt(fallback, event.PullRequest.MergedAt, event.PullRequest.ClosedAt, event.PullRequest.UpdatedAt)
	default:
		return providerOccurredAt(fallback)
	}
}

func giteaProviderEventOccurredAt(eventType string, event GiteaPayload, fallback time.Time) time.Time {
	switch strings.TrimSpace(strings.ToLower(eventType)) {
	case "pull_request_review_submitted":
		return providerOccurredAt(fallback, event.Review.SubmittedAt, event.PullRequest.UpdatedAt)
	case "pull_request_review_comment_created":
		return providerOccurredAt(fallback, event.Comment.CreatedAt, event.Comment.UpdatedAt, event.PullRequest.UpdatedAt)
	case "issue_comment_created":
		return providerOccurredAt(fallback, event.Comment.CreatedAt, event.Comment.UpdatedAt, event.PullRequest.UpdatedAt)
	case "pull_request_synchronize", "pull_request_reopened":
		return providerOccurredAt(fallback, event.PullRequest.UpdatedAt)
	case "pull_request_closed":
		return providerOccurredAt(fallback, event.PullRequest.MergedAt, event.PullRequest.ClosedAt, event.PullRequest.UpdatedAt)
	case "status":
		return providerOccurredAt(fallback, event.UpdatedAt, event.CreatedAt)
	default:
		return providerOccurredAt(fallback)
	}
}

func providerOccurredAt(fallback time.Time, candidates ...time.Time) time.Time {
	for _, candidate := range candidates {
		if !candidate.IsZero() {
			return candidate.UTC()
		}
	}
	if fallback.IsZero() {
		return time.Time{}
	}
	return fallback.UTC()
}

func providerFirstNonEmpty(values ...string) string {
	for _, value := range values {
		if trimmed := strings.TrimSpace(value); trimmed != "" {
			return trimmed
		}
	}
	return ""
}

func providerEventBodyExcerpt(value string) string {
	return TruncateProviderText(strings.TrimSpace(value), providerEventBodyExcerptMaxBytes)
}

// TruncateProviderText caps provider-controlled text to maxBytes without
// splitting UTF-8 runes, adding an ASCII ellipsis marker when truncated.
func TruncateProviderText(value string, maxBytes int) string {
	if maxBytes <= 0 || len(value) <= maxBytes {
		return value
	}
	const suffix = "..."
	if maxBytes <= len(suffix) {
		return suffix[:maxBytes]
	}
	limit := maxBytes - len(suffix)
	cut := 0
	for index, r := range value {
		next := index + utf8.RuneLen(r)
		if next > limit {
			break
		}
		cut = next
	}
	if cut == 0 {
		return suffix
	}
	return strings.TrimRightFunc(value[:cut], unicode.IsSpace) + suffix
}
