package forgejo

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

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

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

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 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
}
