package jobs

import (
	"context"
	"encoding/json"
	"fmt"
	"sync"
)

// Handler defines the function signature for executing a job.
type Handler func(ctx context.Context, payload json.RawMessage) error

// WithResultHandler defines the function signature for executing a job and
// returning import evidence when available.
type WithResultHandler func(ctx context.Context, payload json.RawMessage) (Result, error)

// Result carries the import outcome from a job handler. It is the single
// source of truth for import evidence; scheduler.ItemResult.ImportSummary
// wraps this same struct so the types align across packages.
type Result struct {
	Bars          int
	Instruments   int
	MissingBars   int
	GapBars       int
	DuplicateBars int
	ProviderDelay int
}

// Runner manages the registration and execution of worker job handlers.
type Runner struct {
	mu                 sync.RWMutex
	handlers           map[Kind]Handler
	handlersWithResult map[Kind]WithResultHandler
}

// NewRunner creates a new Runner instance.
func NewRunner() *Runner {
	return &Runner{
		handlers:           make(map[Kind]Handler),
		handlersWithResult: make(map[Kind]WithResultHandler),
	}
}

// Register registers a handler for a specific job kind.
func (r *Runner) Register(kind Kind, handler Handler) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.handlers[kind] = handler
}

// RegisterWithResult registers a handler that can also return import evidence.
// Both Register and RegisterWithResult must be called for the same kind
// so that Execute and ExecuteWithResult both work.
func (r *Runner) RegisterWithResult(kind Kind, handler WithResultHandler) {
	r.mu.Lock()
	defer r.mu.Unlock()
	r.handlersWithResult[kind] = handler
}

// Len returns the number of registered handlers.
func (r *Runner) Len() int {
	r.mu.RLock()
	defer r.mu.RUnlock()
	return len(r.handlers)
}

// Execute dispatches the job to its registered handler and executes it.
// It checks context cancellation and handles panics gracefully.
func (r *Runner) Execute(ctx context.Context, job Job) error {
	r.mu.RLock()
	handler, exists := r.handlers[job.Kind]
	r.mu.RUnlock()

	if !exists {
		return fmt.Errorf("unknown job kind: %s", job.Kind)
	}

	// Check context before starting
	if err := ctx.Err(); err != nil {
		return err
	}

	return r.executeWithPanicRecovery(ctx, handler, job.Payload)
}

// ExecuteWithResult dispatches the job to its registered handler and tries to
// obtain an import Result. If a WithResultHandler is registered it returns the
// evidence along with any error; otherwise it returns (Result{}, false, nil)
// so callers can distinguish "no result handler" (hasResult=false, err=nil)
// from a real execution error (hasResult=false, err!=nil).
//
// Contract: the three-value return semantics are:
//   - (res, true, nil)     → handler ran and returned result
//   - (Result{}, false, nil) → no WithResultHandler registered (legacy fallback)
//   - (Result{}, false, err) → handler ran but returned an error
//
// Callers that only have the two-value Executor interface should use Execute()
// instead. ExecuteWithResult is the sole interface for result-returning paths.
func (r *Runner) ExecuteWithResult(ctx context.Context, job Job) (Result, bool, error) {
	r.mu.RLock()
	wr, hasWithResult := r.handlersWithResult[job.Kind]
	r.mu.RUnlock()

	if !hasWithResult {
		return Result{}, false, nil
	}

	// Check context before starting
	if err := ctx.Err(); err != nil {
		return Result{}, false, err
	}

	res, err := r.executeWithResultPanicRecovery(ctx, wr, job.Payload)
	if err != nil {
		return Result{}, false, err
	}
	return res, true, nil
}

// ExecuteWithResultFallback is a convenience wrapper that provides Execute()-level
// fallback for two-value callers. It is NOT part of the Executor interface and
// should only be used internally where the old pattern (res, ok) was consumed.
// Prefer the three-value ExecuteWithResult to preserve error information.
func (r *Runner) ExecuteWithResultFallback(ctx context.Context, job Job) (Result, bool) {
	res, hasResult, err := r.ExecuteWithResult(ctx, job)
	if err != nil {
		// Error path: treat as no-result so legacy callers can fallback to Execute.
		return Result{}, false
	}
	return res, hasResult
}

// executeWithResultPanicRecovery wraps a WithResultHandler with panic
// recovery so that panics surface as typed errors rather than collapsing
// into (Result{}, false, nil).
func (r *Runner) executeWithResultPanicRecovery(ctx context.Context, handler WithResultHandler, payload json.RawMessage) (res Result, err error) {
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("job panicked: %v", rec)
		}
	}()
	return handler(ctx, payload)
}

func (r *Runner) executeWithPanicRecovery(ctx context.Context, handler Handler, payload json.RawMessage) (err error) {
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("job panicked: %v", rec)
		}
	}()
	return handler(ctx, payload)
}
