package app

import (
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/url"
	"sort"
	"strconv"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/store/model"
)

func NormalizePullRequestSubscriptionRef(provider string, value string) (PullRequestSubscriptionResource, error) {
	normalizedProvider := normalizePullRequestSubscriptionProvider(provider)
	if _, err := pullRequestSubscriptionProviderAppID(normalizedProvider); err != nil {
		return PullRequestSubscriptionResource{}, err
	}
	raw := strings.TrimSpace(value)
	if raw == "" {
		return PullRequestSubscriptionResource{}, badRequest("pull request reference is required")
	}
	parseValue, err := stripPullRequestSubscriptionResourceProvider(normalizedProvider, raw)
	if err != nil {
		return PullRequestSubscriptionResource{}, err
	}
	owner, repo, number, ok := parsePullRequestSubscriptionShorthand(parseValue)
	if !ok {
		owner, repo, number, err = parsePullRequestSubscriptionURL(parseValue)
		if err != nil {
			return PullRequestSubscriptionResource{}, err
		}
	}
	owner = strings.ToLower(strings.TrimSpace(owner))
	repo = strings.ToLower(strings.TrimSpace(repo))
	return PullRequestSubscriptionResource{
		Provider:    normalizedProvider,
		ResourceRef: raw,
		ResourceKey: fmt.Sprintf("%s:%s/%s#%d", normalizedProvider, owner, repo, number),
		Owner:       owner,
		Repo:        repo,
		Number:      number,
	}, nil
}

func stripPullRequestSubscriptionResourceProvider(provider string, value string) (string, error) {
	prefix, remainder, found := strings.Cut(strings.TrimSpace(value), ":")
	if !found || !isPullRequestSubscriptionProviderSlug(prefix) {
		return value, nil
	}
	if normalizePullRequestSubscriptionProvider(prefix) != provider {
		return "", badRequest("pull request resource provider does not match selected provider")
	}
	return strings.TrimSpace(remainder), nil
}

func parsePullRequestSubscriptionShorthand(value string) (owner string, repo string, number int, ok bool) {
	repoPart, numberPart, found := strings.Cut(strings.TrimSpace(value), "#")
	if !found {
		return "", "", 0, false
	}
	segments := strings.Split(strings.Trim(repoPart, "/"), "/")
	if len(segments) != 2 {
		return "", "", 0, false
	}
	parsed, err := strconv.Atoi(strings.TrimSpace(numberPart))
	if err != nil || parsed <= 0 {
		return "", "", 0, false
	}
	owner = strings.TrimSpace(segments[0])
	repo = strings.TrimSpace(segments[1])
	if owner == "" || repo == "" {
		return "", "", 0, false
	}
	return owner, repo, parsed, true
}

func parsePullRequestSubscriptionURL(value string) (owner string, repo string, number int, err error) {
	parsedURL, err := url.Parse(strings.TrimSpace(value))
	if err != nil || parsedURL.Scheme == "" || parsedURL.Host == "" {
		return "", "", 0, badRequest("pull request reference must be a URL or owner/repo#number")
	}
	segments := strings.Split(strings.Trim(parsedURL.EscapedPath(), "/"), "/")
	if len(segments) < 4 {
		return "", "", 0, badRequest("pull request URL is not supported")
	}
	marker := strings.ToLower(strings.TrimSpace(segments[2]))
	if marker != "pull" && marker != "pulls" {
		return "", "", 0, badRequest("pull request URL is not supported")
	}
	parsedNumber, err := strconv.Atoi(strings.TrimSpace(segments[3]))
	if err != nil || parsedNumber <= 0 {
		return "", "", 0, badRequest("pull request URL number is invalid")
	}
	decodedOwner, err := url.PathUnescape(segments[0])
	if err != nil {
		return "", "", 0, badRequest("pull request URL owner is invalid")
	}
	decodedRepo, err := url.PathUnescape(segments[1])
	if err != nil {
		return "", "", 0, badRequest("pull request URL repo is invalid")
	}
	decodedOwner = strings.TrimSpace(decodedOwner)
	decodedRepo = strings.TrimSpace(decodedRepo)
	if decodedOwner == "" || decodedRepo == "" {
		return "", "", 0, badRequest("pull request URL is not supported")
	}
	return decodedOwner, decodedRepo, parsedNumber, nil
}

func pullRequestSubscriptionProviderDisplayName(provider string) string {
	switch strings.ToLower(strings.TrimSpace(provider)) {
	case PullRequestSubscriptionProviderGitHub:
		return "GitHub"
	case PullRequestSubscriptionProviderGitea:
		return "Gitea"
	default:
		return strings.TrimSpace(provider)
	}
}

func isPullRequestSubscriptionProviderSlug(value string) bool {
	switch normalizePullRequestSubscriptionProvider(value) {
	case PullRequestSubscriptionProviderGitHub, PullRequestSubscriptionProviderGitea:
		return true
	default:
		return false
	}
}

func pullRequestSubscriptionSourceType(provider string, resourceKind string) string {
	return normalizePullRequestSubscriptionProvider(provider) + "." + strings.TrimSpace(resourceKind)
}

func serviceEventSubscriptionResultRecord(result ServiceEventSubscriptionResult) ServiceEventSubscriptionRecord {
	return result.Subscription
}

func normalizePullRequestSubscriptionProvider(provider string) string {
	return strings.ToLower(strings.TrimSpace(provider))
}

func pullRequestSubscriptionProviderAppID(provider string) (string, error) {
	switch normalizePullRequestSubscriptionProvider(provider) {
	case PullRequestSubscriptionProviderGitHub:
		return "app_github", nil
	case PullRequestSubscriptionProviderGitea:
		return "app_gitea", nil
	default:
		return "", badRequest("provider is not supported for pull request subscriptions")
	}
}

func validPullRequestSubscriptionCreatedVia(createdVia string) bool {
	switch normalizeCreatedVia(createdVia) {
	case "app_command", "app_page", "mcp", "migration":
		return true
	default:
		return false
	}
}

func marshalPullRequestSubscriptionEventFilters(value map[string]any) ([]byte, error) {
	events := canonicalPullRequestSubscriptionEvents(pullRequestSubscriptionFilterEventsFromMap(value))
	if len(events) == 0 {
		return []byte(`{}`), nil
	}
	payload, err := model.MarshalJSONObject("pull request subscription event filters", model.WatchEventFilters{Events: events})
	if err != nil {
		return nil, badRequest("event filters must be a JSON object")
	}
	return payload, nil
}

func pullRequestSubscriptionFilterEventsFromMap(value map[string]any) []string {
	if len(value) == 0 {
		return nil
	}
	if rawEvents, ok := value["events"]; ok {
		return pullRequestSubscriptionEventsFromAny(rawEvents)
	}
	events := make([]string, 0, len(value))
	for key, raw := range value {
		if enabled, ok := raw.(bool); ok && enabled {
			events = append(events, key)
		}
	}
	return events
}

func pullRequestSubscriptionEventsFromAny(value any) []string {
	switch typed := value.(type) {
	case []string:
		return typed
	case []any:
		events := make([]string, 0, len(typed))
		for _, item := range typed {
			if text, ok := item.(string); ok {
				events = append(events, text)
			}
		}
		return events
	case string:
		return strings.Split(typed, ",")
	default:
		return nil
	}
}

func defaultPullRequestSubscriptionEventFilters() []string {
	// Base-branch push reminders are intentionally opt-in because they can be
	// noisy when a repository's target branch moves frequently.
	return append([]string(nil), pullRequestSubscriptionEventVocabulary[1:]...)
}

var pullRequestSubscriptionEventVocabulary = []string{
	"base_branch_push",
	"close",
	"comments",
	"merge",
	"reopen",
	"reviews",
	"status",
	"synchronize",
}

// NormalizePullRequestSubscriptionEvents validates and canonicalizes the
// provider-neutral event vocabulary accepted by pull-request watch clients.
func NormalizePullRequestSubscriptionEvents(values []string) ([]string, error) {
	events := canonicalPullRequestSubscriptionEvents(values)
	if len(events) == 0 {
		return nil, badRequest("events must include at least one supported event")
	}
	for _, event := range events {
		if !supportedPullRequestSubscriptionEvent(event) {
			return nil, badRequest("events contains an unsupported event")
		}
	}
	return events, nil
}

func supportedPullRequestSubscriptionEvent(event string) bool {
	index := sort.SearchStrings(pullRequestSubscriptionEventVocabulary, event)
	return index < len(pullRequestSubscriptionEventVocabulary) &&
		pullRequestSubscriptionEventVocabulary[index] == event
}

func canonicalPullRequestSubscriptionEvents(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := map[string]struct{}{}
	for _, value := range values {
		canonical := canonicalPullRequestSubscriptionEventFilter(value)
		if canonical == "" {
			continue
		}
		seen[canonical] = struct{}{}
	}
	events := make([]string, 0, len(seen))
	for event := range seen {
		events = append(events, event)
	}
	sort.Strings(events)
	return events
}

func canonicalPullRequestSubscriptionEventFilter(value string) string {
	normalized := strings.ToLower(strings.TrimSpace(value))
	normalized = strings.ReplaceAll(normalized, "-", "_")
	normalized = strings.TrimPrefix(normalized, "pull_request_")
	switch normalized {
	case "comment", "comments", "issue_comment", "issue_comments", "issue_comment_created", "review_comment", "review_comments", "review_comment_created", "inline_comment", "inline_comments":
		return "comments"
	case "review", "reviews", "review_submitted", "review_submissions", "submitted":
		return "reviews"
	case "base_branch", "base_branch_push", "base_branch_pushed", "target_branch", "target_branch_push", "target_branch_pushed":
		return "base_branch_push"
	case "status", "statuses", "check", "checks", "ci", "ci_status", "commit_status", "workflow", "workflow_run", "workflow_job":
		return "status"
	case "sync", "synchronize", "synchronized", "push", "pushes":
		return "synchronize"
	case "merge", "merged":
		return "merge"
	case "close", "closed":
		return "close"
	case "reopen", "reopened":
		return "reopen"
	default:
		return normalized
	}
}

func pullRequestSubscriptionRepositoryFullName(provider string, resourceRef string, resourceKey string) (string, error) {
	resource, err := NormalizePullRequestSubscriptionRef(provider, resourceRef)
	if err != nil {
		return "", err
	}
	if strings.TrimSpace(resourceKey) != resource.ResourceKey {
		return "", badRequest("resourceKey does not match resourceRef")
	}
	return strings.ToLower(resource.Owner + "/" + resource.Repo), nil
}

func cloneServiceEventSubscriptionMetadata(metadata map[string]any) map[string]any {
	cloned := make(map[string]any, len(metadata)+1)
	for key, value := range metadata {
		trimmedKey := strings.TrimSpace(key)
		if trimmedKey == "" {
			continue
		}
		cloned[trimmedKey] = value
	}
	return cloned
}

func serviceEventSubscriptionExpectedConnectionRef(metadata map[string]any) string {
	for _, key := range []string{"provider_connection_ref", "connection_ref"} {
		if value, ok := metadata[key]; ok {
			switch typed := value.(type) {
			case string:
				if trimmed := strings.TrimSpace(typed); trimmed != "" {
					return trimmed
				}
			case fmt.Stringer:
				if trimmed := strings.TrimSpace(typed.String()); trimmed != "" {
					return trimmed
				}
			}
		}
	}
	return ""
}

func marshalServiceEventSubscriptionJSONMap(value map[string]any) ([]byte, error) {
	if len(value) == 0 {
		return []byte(`{}`), nil
	}
	payload, err := model.MarshalJSONObject("service event subscription metadata", value)
	if err != nil {
		return nil, badRequest("metadata must be a JSON object")
	}
	return payload, nil
}

type serviceEventSubscriptionDeliveryOptions struct {
	NotifyPrincipal bool
}

func normalizeServiceEventSubscriptionDeliveryOptions(notifyPrincipalValue *bool) (serviceEventSubscriptionDeliveryOptions, error) {
	return serviceEventSubscriptionDeliveryOptions{NotifyPrincipal: pullRequestSubscriptionBoolDefault(notifyPrincipalValue, true)}, nil
}

func pullRequestSubscriptionBoolDefault(value *bool, fallback bool) bool {
	if value == nil {
		return fallback
	}
	return *value
}

func pullRequestSubscriptionSourceFilterKey(eventFilters []byte) string {
	return pullRequestSubscriptionKey(string(eventFilters))
}

func pullRequestSubscriptionKey(parts ...string) string {
	trimmed := make([]string, 0, len(parts))
	for _, part := range parts {
		trimmed = append(trimmed, strings.TrimSpace(part))
	}
	payload, _ := model.MarshalJSONObject("pull request subscription key", trimmed)
	sum := sha256.Sum256(payload)
	return hex.EncodeToString(sum[:])
}
