package webhook

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"strings"
	"time"
)

// ErrWebhookSecretNotFound indicates credentials do not contain a webhook secret.
var ErrWebhookSecretNotFound = errors.New("webhook secret not found")

// PullRequestResource describes a normalized provider pull request resource.
type PullRequestResource struct {
	ResourceType       string
	ResourceIdentifier string
	ResourceURL        string
	RepositoryFullName string
	Number             int
}

type GitHubUser struct {
	Login string `json:"login"`
}

type GitHubCommit struct {
	Added    []string `json:"added"`
	Modified []string `json:"modified"`
	Removed  []string `json:"removed"`
}

type GitHubPullRequestRef struct {
	Number  json.Number `json:"number"`
	HTMLURL string      `json:"html_url"`
}

type GitHubPayload struct {
	Action       string     `json:"action"`
	SHA          string     `json:"sha"`
	State        string     `json:"state"`
	Context      string     `json:"context"`
	TargetURL    string     `json:"target_url"`
	Description  string     `json:"description"`
	Sender       GitHubUser `json:"sender"`
	Installation struct {
		ID json.Number `json:"id"`
	} `json:"installation"`
	Repository struct {
		FullName      string `json:"full_name"`
		DefaultBranch string `json:"default_branch"`
	} `json:"repository"`
	PullRequest struct {
		Number    json.Number `json:"number"`
		HTMLURL   string      `json:"html_url"`
		Merged    bool        `json:"merged"`
		UpdatedAt time.Time   `json:"updated_at"`
		ClosedAt  time.Time   `json:"closed_at"`
		MergedAt  time.Time   `json:"merged_at"`
		Head      struct {
			SHA string `json:"sha"`
		} `json:"head"`
	} `json:"pull_request"`
	Issue struct {
		Number      json.Number `json:"number"`
		HTMLURL     string      `json:"html_url"`
		PullRequest *struct {
			HTMLURL string `json:"html_url"`
		} `json:"pull_request"`
	} `json:"issue"`
	Review struct {
		ID          json.Number `json:"id"`
		State       string      `json:"state"`
		Body        string      `json:"body"`
		HTMLURL     string      `json:"html_url"`
		SubmittedAt time.Time   `json:"submitted_at"`
		User        GitHubUser  `json:"user"`
	} `json:"review"`
	Comment struct {
		ID        json.Number `json:"id"`
		Body      string      `json:"body"`
		Path      string      `json:"path"`
		Line      json.Number `json:"line"`
		HTMLURL   string      `json:"html_url"`
		CreatedAt time.Time   `json:"created_at"`
		UpdatedAt time.Time   `json:"updated_at"`
		User      GitHubUser  `json:"user"`
	} `json:"comment"`
	CheckSuite struct {
		Conclusion   string                 `json:"conclusion"`
		Status       string                 `json:"status"`
		PullRequests []GitHubPullRequestRef `json:"pull_requests"`
	} `json:"check_suite"`
	CheckRun struct {
		Name         string                 `json:"name"`
		Conclusion   string                 `json:"conclusion"`
		HTMLURL      string                 `json:"html_url"`
		PullRequests []GitHubPullRequestRef `json:"pull_requests"`
	} `json:"check_run"`
	Commits []GitHubCommit `json:"commits"`
	Ref     string         `json:"ref"`
	Before  string         `json:"before"`
	After   string         `json:"after"`
	Compare string         `json:"compare"`
}

func ExtractGitHubInstallationRef(payload []byte) (string, bool) {
	if len(payload) == 0 {
		return "", false
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var envelope struct {
		Installation struct {
			ID json.Number `json:"id"`
		} `json:"installation"`
	}
	if err := decoder.Decode(&envelope); err != nil {
		return "", false
	}
	return strings.TrimSpace(envelope.Installation.ID.String()), true
}

func ExtractWebhookSecret(credentials []byte) (string, error) {
	if len(credentials) == 0 {
		return "", ErrWebhookSecretNotFound
	}

	var decoded map[string]any
	if err := json.Unmarshal(credentials, &decoded); err != nil {
		return "", fmt.Errorf("decode app installation credentials: %w", err)
	}

	candidates := []string{
		StringValue(decoded["webhookSecret"]),
		StringValue(decoded["webhook_secret"]),
	}
	for _, candidate := range candidates {
		if candidate != "" {
			return candidate, nil
		}
	}
	return "", ErrWebhookSecretNotFound
}

func VerifyGitHubWebhookSignature(signatureHeader string, payload []byte, secret string) bool {
	trimmedSignature := strings.TrimSpace(signatureHeader)
	trimmedSecret := strings.TrimSpace(secret)
	if trimmedSignature == "" || trimmedSecret == "" {
		return false
	}

	mac := hmac.New(sha256.New, []byte(trimmedSecret))
	_, _ = mac.Write(payload)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(trimmedSignature), []byte(expected))
}

func GitHubWebhookDeliveryKey(deliveryHeader string, eventType string, payload []byte) string {
	return DeliveryKey(deliveryHeader, eventType, payload)
}

func PayloadExcerptForLog(payload []byte) string {
	if len(payload) <= 16*1024 {
		return string(payload)
	}
	return string(payload[:16*1024])
}

func ParseGitHubWebhookPayload(payload []byte) (GitHubPayload, bool, error) {
	if len(payload) == 0 {
		return GitHubPayload{}, false, nil
	}
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.UseNumber()
	var event GitHubPayload
	if err := decoder.Decode(&event); err != nil {
		return GitHubPayload{}, false, fmt.Errorf("decode github webhook payload: %w", err)
	}
	var trailing struct{}
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return GitHubPayload{}, false, fmt.Errorf("decode github webhook payload: multiple JSON values")
		}
		return GitHubPayload{}, false, fmt.Errorf("decode github webhook payload: %w", err)
	}
	return event, true, nil
}

func NormalizeGitHubWebhookEventType(eventHeader string, action string) string {
	switch strings.TrimSpace(strings.ToLower(eventHeader)) {
	case "pull_request_review":
		if strings.EqualFold(strings.TrimSpace(action), "submitted") {
			return "pull_request_review_submitted"
		}
		return "pull_request_review"
	case "pull_request_review_comment":
		if strings.EqualFold(strings.TrimSpace(action), "created") {
			return "pull_request_review_comment_created"
		}
		return "pull_request_review_comment"
	case "issue_comment":
		if strings.EqualFold(strings.TrimSpace(action), "created") {
			return "issue_comment_created"
		}
		return "issue_comment"
	case "pull_request":
		switch strings.TrimSpace(strings.ToLower(action)) {
		case "synchronize":
			return "pull_request_synchronize"
		case "closed":
			return "pull_request_closed"
		case "reopened":
			return "pull_request_reopened"
		default:
			return "pull_request"
		}
	case "check_suite":
		if strings.EqualFold(strings.TrimSpace(action), "completed") {
			return "check_suite_completed"
		}
		return "check_suite"
	case "check_run":
		if strings.EqualFold(strings.TrimSpace(action), "completed") {
			return "check_run_completed"
		}
		return "check_run"
	default:
		return strings.TrimSpace(strings.ToLower(eventHeader))
	}
}

func GitHubPullRequestResourceFromPullRequest(event GitHubPayload) (PullRequestResource, bool) {
	return GitHubPullRequestResourceFromNumber(
		event.Repository.FullName,
		event.PullRequest.Number,
		event.PullRequest.HTMLURL,
	)
}

func GitHubPullRequestResourceFromIssueComment(event GitHubPayload) (PullRequestResource, bool, bool) {
	if event.Issue.PullRequest == nil {
		return PullRequestResource{}, false, false
	}
	resource, ok := GitHubPullRequestResourceFromNumber(
		event.Repository.FullName,
		event.Issue.Number,
		event.Issue.HTMLURL,
	)
	return resource, ok, true
}

func GitHubPullRequestResourceFromNumber(repositoryFullName string, numberValue json.Number, resourceURL string) (PullRequestResource, bool) {
	normalizedRepository := strings.ToLower(strings.TrimSpace(repositoryFullName))
	if normalizedRepository == "" {
		return PullRequestResource{}, false
	}

	number, ok := PositiveJSONNumber(numberValue)
	if !ok {
		return PullRequestResource{}, false
	}

	trimmedResourceURL := strings.TrimSpace(resourceURL)
	if trimmedResourceURL == "" {
		trimmedResourceURL = fmt.Sprintf("https://github.com/%s/pull/%d", normalizedRepository, number)
	}

	return PullRequestResource{
		ResourceType:       "github_pull_request",
		ResourceIdentifier: fmt.Sprintf("%s#%d", normalizedRepository, number),
		ResourceURL:        trimmedResourceURL,
		RepositoryFullName: normalizedRepository,
		Number:             number,
	}, true
}

func GitHubPullRequestResourceFromNumberString(repositoryFullName string, numberValue string, resourceURL string) (PullRequestResource, bool) {
	return GitHubPullRequestResourceFromNumber(repositoryFullName, json.Number(strings.TrimSpace(numberValue)), resourceURL)
}

func PositiveJSONNumber(value json.Number) (int, bool) {
	numberText := strings.TrimSpace(value.String())
	if numberText == "" {
		return 0, false
	}
	number, err := value.Int64()
	if err != nil || number <= 0 {
		return 0, false
	}
	return int(number), true
}

func NormalizeGitHubReviewState(value string) string {
	switch strings.TrimSpace(strings.ToLower(value)) {
	case "approved":
		return "approved"
	case "changes_requested":
		return "changes_requested"
	case "commented":
		return "commented"
	default:
		return strings.TrimSpace(strings.ToLower(value))
	}
}
