// Package schedule implements the "schedule" trigger: fires a deploy on
// a recurring time interval driven by the internal scheduler.
//
// v1 is interval-based ("every 24h"). A future revision can add a cron
// expression field; the plugin keeps the JSON shape forward-compatible
// by ignoring unknown keys.
package schedule

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

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

// Config is the per-trigger schedule config.
//
// Interval is a Go duration string ("1h", "24h", "168h"). The scheduler
// rejects intervals below MinInterval so a misconfigured trigger cannot
// run away and saturate the docker daemon.
//
// Reference is an optional string passed through to DeploymentIntent so
// the operator can pin a specific tag/sha to redeploy on every tick
// (e.g. always re-pull "stable"). Empty means the Source uses whatever
// it normally would.
type Config struct {
	Interval  string `json:"interval"`
	Reference string `json:"reference,omitempty"`
}

// MinInterval is the floor enforced at Validate time. One minute is a
// pragmatic lower bound: shorter intervals would mostly serve as
// accidental denial-of-service against the deployer.
const MinInterval = time.Minute

type trigger struct{}

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

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

func (*trigger) SchemaSample() any {
	return Config{Interval: "24h"}
}

func (*trigger) Validate(cfg json.RawMessage) error {
	if len(cfg) == 0 {
		return fmt.Errorf("schedule trigger: config is required")
	}
	var c Config
	if err := json.Unmarshal(cfg, &c); err != nil {
		return fmt.Errorf("schedule trigger: invalid json: %w", err)
	}
	if strings.TrimSpace(c.Interval) == "" {
		return fmt.Errorf("schedule trigger: interval is required (e.g. \"24h\")")
	}
	d, err := ParseInterval(c.Interval)
	if err != nil {
		return fmt.Errorf("schedule trigger: %w", err)
	}
	if d < MinInterval {
		return fmt.Errorf("schedule trigger: interval %s is below minimum %s",
			d, MinInterval)
	}
	return nil
}

// ParseInterval parses a duration string with the same syntax as
// time.ParseDuration ("90s", "5m", "2h45m"). Exported so the scheduler
// can reuse the same parser and stay consistent with Validate.
func ParseInterval(s string) (time.Duration, error) {
	d, err := time.ParseDuration(strings.TrimSpace(s))
	if err != nil {
		return 0, fmt.Errorf("invalid interval %q: %w", s, err)
	}
	if d <= 0 {
		return 0, fmt.Errorf("invalid interval %q: must be positive", s)
	}
	return d, nil
}

// IntervalOf reads the interval out of a workload-effective trigger
// config. Returns the parsed duration; an error if the config is
// missing the interval or malformed. Used by the scheduler when it
// already knows the merged config and wants to schedule next fire.
func IntervalOf(cfg json.RawMessage) (time.Duration, error) {
	var c Config
	if len(cfg) == 0 {
		return 0, fmt.Errorf("schedule trigger: empty config")
	}
	if err := json.Unmarshal(cfg, &c); err != nil {
		return 0, fmt.Errorf("schedule trigger: invalid json: %w", err)
	}
	return ParseInterval(c.Interval)
}

// IntervalOfRaw is a convenience that decodes the raw trigger.config
// blob from store.Trigger (a JSON string). Wrapper kept tiny because
// the scheduler holds the raw string, not a parsed shape.
func IntervalOfRaw(raw string) (time.Duration, error) {
	return IntervalOf(json.RawMessage(raw))
}

func (*trigger) Match(ctx context.Context, deps plugin.Deps, w plugin.Workload, evt plugin.InboundEvent) (*plugin.DeploymentIntent, error) {
	if evt.Kind != "schedule" || evt.Schedule == nil {
		return nil, nil
	}
	cfg, err := plugin.TriggerConfigOf[Config](w)
	if err != nil {
		return nil, fmt.Errorf("schedule trigger: decode config: %w", err)
	}
	firedAt := evt.Schedule.FiredAt
	if firedAt.IsZero() {
		firedAt = time.Now().UTC()
	}
	meta := map[string]string{
		"interval": cfg.Interval,
	}
	return &plugin.DeploymentIntent{
		Reason:      "schedule",
		Reference:   strings.TrimSpace(cfg.Reference),
		Metadata:    meta,
		TriggeredAt: firedAt,
		TriggeredBy: "scheduler",
	}, nil
}
