// 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"

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

// 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.
	cron cron.Schedule
	// 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
	// model is the model ID to pass to the LLM client (without provider
	// prefix, which is resolved at construction time).
	model string
	// 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.
// llmc is the LLM client used to execute the job.
// model is the model ID (without provider prefix) to pass to the client.
func New(cfg config.JobConfig, log *slog.Logger, llmc *llm.Client, model string) (*Job, error) {
	// Validation is handled by config.Config.Validate().
	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,
			)
		}
	}

	return &Job{
		cron:          sched,
		log:           log.With(slog.String("job", cfg.Name)),
		llm:           llmc,
		model:         model,
		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 }

// MaxIterations returns the configured max iterations.
func (j *Job) MaxIterations() int { return j.maxIterations }

// Model returns the model ID used by the job.
func (j *Job) Model() string { return j.model }

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

// Schedule returns the job 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 []llm.Message
	// Error is set if the job failed.
	Error error
}

// Run executes the job: builds messages from the job config, calls the LLM,
// and logs the result. 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 := []llm.Message{
		{
			Role: llm.RoleUser,
			ContentParts: []llm.ContentPart{
				{
					Text: j.Task(),
					Type: llm.ContentTypeText,
				},
			},
		},
	}

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

	res, err := j.llm.Completions(
		ctx, llm.CompletionsParams{
			Model:         j.model,
			SystemMessage: j.SystemMessage(),
			MaxIterations: j.MaxIterations(),
			Messages:      messages,
		},
	)

	duration := time.Since(start)
	if err != nil {
		log.ErrorContext(
			ctx,
			"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(res.Messages) == 0 {
		log.ErrorContext(
			ctx,
			"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.InfoContext(
		ctx,
		"job completed",
		slog.Duration("duration", duration),
		slog.Int("messages", len(res.Messages)),
	)
	return Result{
		Name:     j.Name(),
		Duration: duration,
		Messages: res.Messages,
	}
}
