// Package git implements the "git" trigger: matches inbound git push or
// tag-create events from Gitea, GitHub, or GitLab against a repo + ref
// filter.
package git

import (
	"context"
	"encoding/json"
	"fmt"
	"path"
	"strings"
	"time"

	"github.com/alexei/tinyforge/internal/workload/plugin"
)

// Config is the per-workload trigger config. Repo is "owner/name" (must
// match the event repo). Mode controls whether branch pushes or tag
// pushes fire the deploy. Branch is exact-matched when Mode=="push";
// TagPattern is glob-matched when Mode=="tag".
//
// BranchPattern is the preview-deploy escape hatch: when non-empty in
// "push" mode it overrides Branch and matches the event branch as a glob
// (`feat/*`, `release-*`, `*` for "any branch"). The trigger returns an
// intent whose Metadata["preview_branch"] holds the matched branch — the
// dispatcher uses that signal to materialize an ephemeral per-branch
// child workload rather than redeploying the parent.
type Config struct {
	Repo          string `json:"repo"`
	Mode          string `json:"mode"` // "push" | "tag"
	Branch        string `json:"branch"`
	BranchPattern string `json:"branch_pattern"`
	TagPattern    string `json:"tag_pattern"`
}

type trigger struct{}

func init() { plugin.RegisterTrigger(&trigger{}) }

func (*trigger) Kind() string { return "git" }

func (*trigger) SchemaSample() any {
	return Config{
		Repo:   "owner/repo",
		Mode:   "push",
		Branch: "main",
	}
}

func (*trigger) Validate(cfg json.RawMessage) error {
	var c Config
	if len(cfg) == 0 {
		return fmt.Errorf("git trigger: config is required")
	}
	if err := json.Unmarshal(cfg, &c); err != nil {
		return fmt.Errorf("git trigger: invalid json: %w", err)
	}
	switch c.Mode {
	case "push":
		// Branch is optional ("" means any branch). BranchPattern is
		// validated as a path.Match glob if present; misconfigured
		// patterns are rejected at the boundary rather than letting them
		// fail silently inside Match.
		if c.BranchPattern != "" {
			if _, err := path.Match(c.BranchPattern, "probe"); err != nil {
				return fmt.Errorf("git trigger: invalid branch_pattern %q: %w", c.BranchPattern, err)
			}
		}
	case "tag":
		pattern := c.TagPattern
		if pattern == "" {
			pattern = "*"
		}
		if _, err := path.Match(pattern, "probe"); err != nil {
			return fmt.Errorf("git trigger: invalid tag_pattern %q: %w", pattern, err)
		}
	default:
		return fmt.Errorf("git trigger: mode must be \"push\" or \"tag\"")
	}
	return nil
}

func (*trigger) Match(ctx context.Context, deps plugin.Deps, w plugin.Workload, evt plugin.InboundEvent) (*plugin.DeploymentIntent, error) {
	if evt.Git == nil {
		return nil, nil
	}
	cfg, err := plugin.TriggerConfigOf[Config](w)
	if err != nil {
		return nil, fmt.Errorf("git trigger: decode config: %w", err)
	}
	if cfg.Repo != "" && !strings.EqualFold(cfg.Repo, evt.Git.Repo) {
		return nil, nil
	}
	if !refMatches(cfg, evt.Git.Ref) {
		return nil, nil
	}
	meta := map[string]string{
		"repo":   evt.Git.Repo,
		"vendor": evt.Git.Vendor,
		"ref":    evt.Git.Ref,
		"pusher": evt.Git.Pusher,
	}
	if evt.Git.Branch != "" {
		meta["branch"] = evt.Git.Branch
	}
	if evt.Git.Tag != "" {
		meta["tag"] = evt.Git.Tag
	}
	// Preview-deploy signal: when BranchPattern is set AND the matched
	// branch is NOT the configured baseline Branch, flag this dispatch
	// for materialization as a per-branch child workload. The dispatcher
	// reads preview_branch and decides whether to spawn a preview row;
	// a baseline-branch push falls through to a normal redeploy of the
	// template itself.
	if cfg.Mode == "push" && cfg.BranchPattern != "" && evt.Git.Branch != "" && evt.Git.Branch != cfg.Branch {
		meta["preview_branch"] = evt.Git.Branch
		if evt.Git.Deleted {
			meta["preview_deleted"] = "1"
		}
	}
	reason := "git-push"
	if meta["preview_deleted"] == "1" {
		reason = "git-branch-deleted"
	}
	return &plugin.DeploymentIntent{
		Reason:      reason,
		Reference:   evt.Git.CommitSHA,
		Metadata:    meta,
		TriggeredAt: time.Now().UTC(),
		TriggeredBy: "git-webhook",
	}, nil
}

func refMatches(cfg Config, ref string) bool {
	switch cfg.Mode {
	case "push":
		branch, ok := strings.CutPrefix(ref, "refs/heads/")
		if !ok {
			return false
		}
		// Pattern-mode preview filter: any branch whose name matches the
		// glob is in scope. The baseline `cfg.Branch` is also allowed so
		// pushes to the template's primary branch keep redeploying the
		// template itself.
		if cfg.BranchPattern != "" {
			if cfg.Branch != "" && cfg.Branch == branch {
				return true
			}
			matched, err := path.Match(cfg.BranchPattern, branch)
			return err == nil && matched
		}
		return cfg.Branch == "" || cfg.Branch == branch
	case "tag":
		tag, ok := strings.CutPrefix(ref, "refs/tags/")
		if !ok {
			return false
		}
		pattern := cfg.TagPattern
		if pattern == "" {
			pattern = "*"
		}
		matched, err := path.Match(pattern, tag)
		return err == nil && matched
	}
	return false
}
