// Package job provides cron-based scheduling for autonomous LLM tasks.
//
// Jobs are defined in the YAML configuration with a cron expression,
// a system message, and a task description. When the service fires a
// job, it constructs messages and calls the LLM agent loop to execute the
// task.
package job

import (
	"context"
	"fmt"
	"log/slog"
	"time"

	"github.com/robfig/cron/v3"
	"github.com/sashabaranov/go-openai"

	"code.chimeric.al/chimerical/odidere/internal/llm"
)

// Config holds the user-facing configuration for a job.
// It is unmarshaled from YAML and passed to New, which validates and
// parses it into an immutable Job.
type Config struct {
	// Enabled controls whether the job is active. Defaults to true.
	Enabled *bool `yaml:"enabled,omitempty"`
	// MaxIterations caps the number of agent loop iterations.
	// A value of 0 means unlimited (no cap).
	MaxIterations int `yaml:"max_iterations,omitempty"`
	// Name uniquely identifies the job within the config.
	Name string `yaml:"name"`
	// Schedule is a cron expression (e.g., "0 9 * * *").
	Schedule string `yaml:"schedule"`
	// SystemMessage overrides the global system message for this job.
	SystemMessage string `yaml:"system_message,omitempty"`
	// Task is the user-level instruction to give the LLM.
	Task string `yaml:"task"`
	// Timeout limits the job execution time (e.g., "10m").
	// Defaults to the global LLM timeout if empty.
	Timeout string `yaml:"timeout,omitempty"`
}

// IsEnabled returns whether the job should be scheduled.
// If Enabled is nil, the job is considered enabled by default.
func (cfg Config) IsEnabled() bool {
	if cfg.Enabled == nil {
		return true
	}
	return *cfg.Enabled
}

// Validate checks that required fields are present and constraints hold.
// It does not parse values — that is the responsibility of New.
func (cfg Config) Validate() error {
	if cfg.Name == "" {
		return fmt.Errorf("missing name")
	}
	if cfg.Task == "" {
		return fmt.Errorf("missing task")
	}
	if cfg.Schedule == "" {
		return fmt.Errorf("missing schedule")
	}
	if cfg.MaxIterations < 0 {
		return fmt.Errorf("max_iterations must be non-negative")
	}
	return nil
}

// Job defines a scheduled autonomous task.
//
// Job is immutable after construction via New. All fields are private and
// accessed through getter methods. This ensures that a Job's configuration
// cannot change once it is handed to the service.
type Job struct {
	// cron is the cron.Schedule derived from schedule during
	// construction. This is what the robfig/cron package uses to determine
	// when the job should fire.
	cron cron.Schedule

	// enabled indicates whether the job should be registered with the cron
	// scheduler.
	enabled bool

	// log is the structured logger used for job execution.
	log *slog.Logger

	// llm is the LLM client used to execute the job.
	llm *llm.Client

	// maxIterations caps the agent loop iterations for this job. Zero means
	// unlimited.
	maxIterations int

	// name uniquely identifies the job within the config. It is used for
	// logging, scheduling, and lookup.
	name string

	// schedule is the original cron expression string from the config
	// (e.g. "0 9 * * *"). Stored for logging and display purposes.
	schedule string

	// systemMessage is the per-job system prompt that overrides the global
	// LLM system message. Empty string means use the default.
	systemMessage string

	// task is the user-level instruction passed to the LLM when the job
	// fires. This is the core of what the job does.
	task string

	// timeout is the parsed execution time limit. Zero means no per-job
	// timeout (falls back to the global LLM timeout).
	timeout time.Duration
}

// New creates a new Job from a Config.
// log is the structured logger used for job execution.
// llm is the LLM client used to execute the job.
func New(cfg Config, log *slog.Logger, llmClient *llm.Client) (*Job, error) {
	if err := cfg.Validate(); err != nil {
		return nil, fmt.Errorf("invalid config: %w", err)
	}

	sched, err := cron.ParseStandard(cfg.Schedule)
	if err != nil {
		return nil, fmt.Errorf(
			"invalid schedule %q: %w", cfg.Schedule, err,
		)
	}

	var timeout time.Duration
	if cfg.Timeout != "" {
		timeout, err = time.ParseDuration(cfg.Timeout)
		if err != nil {
			return nil, fmt.Errorf(
				"invalid timeout %q: %w", cfg.Timeout, err,
			)
		}
	}

	enabled := true
	if cfg.Enabled != nil {
		enabled = *cfg.Enabled
	}

	return &Job{
		cron:          sched,
		enabled:       enabled,
		log:           log.With(slog.String("job", cfg.Name)),
		llm:           llmClient,
		maxIterations: cfg.MaxIterations,
		name:          cfg.Name,
		schedule:      cfg.Schedule,
		systemMessage: cfg.SystemMessage,
		task:          cfg.Task,
		timeout:       timeout,
	}, nil
}

// Cron returns the parsed cron schedule.
func (j *Job) Cron() cron.Schedule {
	return j.cron
}

// IsEnabled returns whether the job should be scheduled.
func (j *Job) IsEnabled() bool {
	return j.enabled
}

// MaxIterations returns the configured max iterations. A value of 0
// means unlimited (no cap on agent loop iterations).
func (j *Job) MaxIterations() int {
	return j.maxIterations
}

// Name returns the job name.
func (j *Job) Name() string {
	return j.name
}

// Schedule returns the cron schedule string.
func (j *Job) Schedule() string {
	return j.schedule
}

// SystemMessage returns the job's system message.
func (j *Job) SystemMessage() string {
	return j.systemMessage
}

// Task returns the job's task description.
func (j *Job) Task() string {
	return j.task
}

// Timeout returns the job's timeout duration.
func (j *Job) Timeout() time.Duration {
	return j.timeout
}

// Result holds the outcome of a job execution.
type Result struct {
	// Name is the job name.
	Name string
	// Duration is how long the job took to run.
	Duration time.Duration
	// Messages is the full conversation returned by the LLM agent loop,
	// including tool calls and tool results. Empty if the job failed.
	Messages []openai.ChatCompletionMessage
	// Error is set if the job failed.
	Error error
}

// Run executes the job: builds messages from the job config, calls the LLM,
// and logs progress. The agent uses tools for any additional output.
// Uses j.log and j.llm internally.
func (j *Job) Run(ctx context.Context) Result {
	start := time.Now()
	log := j.log.With(slog.String("job", j.Name()))
	log.Info("starting job")

	// Build messages from job config.
	messages := []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleUser,
			Content: j.Task(),
		},
	}

	// Apply per-job timeout if set.
	if j.Timeout() > 0 {
		var cancel context.CancelFunc
		ctx, cancel = context.WithTimeout(ctx, j.Timeout())
		defer cancel()
	}

	msgs, err := j.llm.Query(
		ctx,
		messages,
		"",
		j.SystemMessage(),
		j.MaxIterations(),
	)
	duration := time.Since(start)
	if err != nil {
		log.Error("job failed",
			slog.Any("error", err),
			slog.Duration("duration", duration),
		)
		return Result{
			Name:     j.Name(),
			Duration: duration,
			Error:    fmt.Errorf("job %q: %w", j.Name(), err),
		}
	}
	if len(msgs) == 0 {
		log.Error("job returned no messages",
			slog.Duration("duration", duration),
		)
		return Result{
			Name:     j.Name(),
			Duration: duration,
			Error: fmt.Errorf(
				"job %q: no response from LLM", j.Name(),
			),
		}
	}
	log.Info("job completed",
		slog.Duration("duration", duration),
		slog.Int("messages", len(msgs)),
	)

	return Result{
		Name:     j.Name(),
		Duration: duration,
		Messages: msgs,
	}
}
