package forgejo

import (
	"context"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"sort"
	"strings"
	"time"

	"git.toki-labs.com/toki/gito/services/core/internal/core"
	"git.toki-labs.com/toki/gito/services/core/internal/provider"
)

var ErrInvalidPushPayload = errors.New("invalid forgejo push payload")

// WebhookAdapter implements provider.WebhookAdapter for Forgejo.
type WebhookAdapter struct {
	Secret string
}

// VerifyWebhook implements provider.WebhookVerifier.
// If secret is empty, verification is skipped (nil).
// Otherwise it verifies the Forgejo HMAC-SHA256 signature.
func (a WebhookAdapter) VerifyWebhook(ctx context.Context, req provider.WebhookRequest) error {
	signature := headerValue(req.Headers, "X-Forgejo-Signature")
	if signature == "" && strings.TrimSpace(a.Secret) != "" {
		return errors.New("missing forgejo signature header")
	}
	if strings.TrimSpace(a.Secret) == "" {
		return nil
	}
	if !VerifySignature(a.Secret, req.Payload, signature) {
		return errors.New("invalid forgejo signature")
	}
	return nil
}

// NormalizeWebhook implements provider.WebhookNormalizer.
// It converts Forgejo push events into branch.updated candidates.
func (a WebhookAdapter) NormalizeWebhook(ctx context.Context, req provider.WebhookRequest) ([]provider.WebhookCandidate, error) {
	eventType := req.EventType
	// X-Forgejo-Event is recommended but only "push" produces candidates.
	// Missing/empty event header is treated as push for backward compatibility.
	if eventType != "" && eventType != "push" {
		// Unsupported event types produce empty candidates.
		return []provider.WebhookCandidate{}, nil
	}

	payload, err := ParsePushPayload(req.Payload)
	if err != nil {
		// Malformed payload must return error for 400 response.
		return nil, err
	}

	branch, ok := BranchFromRef(payload.Ref)
	if !ok {
		// Tag refs produce empty candidates.
		return []provider.WebhookCandidate{}, nil
	}

	// Repo ID resolution: query repo_id > repository.full_name > repository.name
	repoID := queryValue(req.Query, "repo_id")
	if repoID == "" {
		repoID = payload.Repository.FullName
	}
	if repoID == "" {
		repoID = payload.Repository.Name
	}
	if repoID == "" {
		return nil, ErrInvalidPushPayload
	}

	candidate := provider.WebhookCandidate{
		Type:       "branch.updated",
		Provider:   provider.ProviderForgejo,
		ExternalID: req.ExternalID,
		Revision: &core.RevisionEvent{
			RepoID:       repoID,
			Branch:       branch,
			Before:       strings.TrimSpace(payload.Before),
			After:        strings.TrimSpace(payload.After),
			ChangedFiles: changedFiles(payload.Commits),
			ObservedAt:   req.ReceivedAt,
		},
	}

	return []provider.WebhookCandidate{candidate}, nil
}

type PushPayload struct {
	Ref        string     `json:"ref"`
	Before     string     `json:"before"`
	After      string     `json:"after"`
	CompareURL string     `json:"compare_url"`
	Commits    []Commit   `json:"commits"`
	Repository Repository `json:"repository"`
	Pusher     User       `json:"pusher"`
	Sender     User       `json:"sender"`
}

type Repository struct {
	ID            int64  `json:"id"`
	Name          string `json:"name"`
	FullName      string `json:"full_name"`
	HTMLURL       string `json:"html_url"`
	SSHURL        string `json:"ssh_url"`
	CloneURL      string `json:"clone_url"`
	DefaultBranch string `json:"default_branch"`
}

type User struct {
	Login    string `json:"login"`
	UserName string `json:"username"`
}

type Commit struct {
	ID       string   `json:"id"`
	Message  string   `json:"message"`
	URL      string   `json:"url"`
	Added    []string `json:"added"`
	Removed  []string `json:"removed"`
	Modified []string `json:"modified"`
}

func ParsePushPayload(body []byte) (PushPayload, error) {
	var payload PushPayload
	if len(body) == 0 {
		return payload, ErrInvalidPushPayload
	}
	if err := json.Unmarshal(body, &payload); err != nil {
		return payload, err
	}
	if strings.TrimSpace(payload.Ref) == "" ||
		strings.TrimSpace(payload.Before) == "" ||
		strings.TrimSpace(payload.After) == "" {
		return payload, ErrInvalidPushPayload
	}
	return payload, nil
}

func VerifySignature(secret string, body []byte, signature string) bool {
	secret = strings.TrimSpace(secret)
	signature = normalizeSignature(signature)
	if secret == "" || signature == "" {
		return false
	}
	mac := hmac.New(sha256.New, []byte(secret))
	_, _ = mac.Write(body)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signature))
}

func NormalizePush(payload PushPayload, repoID string, observedAt time.Time) (core.RevisionEvent, error) {
	branch, ok := BranchFromRef(payload.Ref)
	if !ok {
		return core.RevisionEvent{}, ErrInvalidPushPayload
	}
	repoID = strings.TrimSpace(repoID)
	if repoID == "" {
		repoID = strings.TrimSpace(payload.Repository.FullName)
	}
	if repoID == "" {
		repoID = strings.TrimSpace(payload.Repository.Name)
	}
	if repoID == "" {
		return core.RevisionEvent{}, ErrInvalidPushPayload
	}
	if observedAt.IsZero() {
		observedAt = time.Now().UTC()
	}
	return core.RevisionEvent{
		RepoID:       repoID,
		Branch:       branch,
		Before:       strings.TrimSpace(payload.Before),
		After:        strings.TrimSpace(payload.After),
		ChangedFiles: changedFiles(payload.Commits),
		ObservedAt:   observedAt.UTC(),
	}, nil
}

func BranchFromRef(ref string) (string, bool) {
	ref = strings.TrimSpace(ref)
	const prefix = "refs/heads/"
	if !strings.HasPrefix(ref, prefix) {
		return "", false
	}
	branch := strings.TrimPrefix(ref, prefix)
	return branch, branch != ""
}

func normalizeSignature(signature string) string {
	signature = strings.TrimSpace(signature)
	signature = strings.TrimPrefix(signature, "sha256=")
	return strings.ToLower(signature)
}

func queryValue(q map[string][]string, key string) string {
	if q == nil {
		return ""
	}
	vals := q[key]
	if len(vals) == 0 {
		return ""
	}
	return vals[0]
}

func headerValue(h map[string][]string, key string) string {
	if h == nil {
		return ""
	}
	vals := h[key]
	if len(vals) == 0 {
		return ""
	}
	return vals[0]
}

func changedFiles(commits []Commit) []core.ChangedFile {
	files := make([]core.ChangedFile, 0)
	seen := make(map[string]string)
	record := func(path, changeType string) {
		path = strings.TrimSpace(path)
		if path == "" {
			return
		}
		seen[path] = changeType
	}
	for _, commit := range commits {
		for _, path := range commit.Added {
			record(path, "added")
		}
		for _, path := range commit.Modified {
			record(path, "modified")
		}
		for _, path := range commit.Removed {
			record(path, "deleted")
		}
	}
	for path, changeType := range seen {
		files = append(files, core.ChangedFile{Path: path, ChangeType: changeType})
	}
	sort.Slice(files, func(i, j int) bool {
		return files[i].Path < files[j].Path
	})
	return files
}
