package control

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

type Runner interface {
	Run(context.Context, *CloseRun, StepDefinition, *Attempt) (RunnerResult, error)
	Reconcile(context.Context, *CloseRun, StepDefinition, *Attempt) (RunnerResult, error)
}

type Engine struct {
	store  *Store
	runner Runner
	mu     sync.Mutex
}

func NewEngine(store *Store, runner Runner) *Engine {
	return &Engine{store: store, runner: runner}
}

func (e *Engine) Execute(closeID, stepKey string, request ActionRequest) (*Attempt, error) {
	e.mu.Lock()
	closeRun, ok := e.store.GetClose(closeID)
	if !ok {
		e.mu.Unlock()
		return nil, fmt.Errorf("close not found")
	}
	step, ok := closeRun.Steps[stepKey]
	if !ok {
		e.mu.Unlock()
		return nil, fmt.Errorf("step not found")
	}
	if request.Action != step.Action.Type {
		e.mu.Unlock()
		return nil, fmt.Errorf("action %q is not allowed; current action is %q", request.Action, step.Action.Type)
	}
	if !step.Action.Enabled {
		e.mu.Unlock()
		return nil, fmt.Errorf("step action is disabled: %s", step.Action.Reason)
	}
	if step.Action.Type == "inspect" {
		e.mu.Unlock()
		return nil, fmt.Errorf("inspect is read-only and does not create an attempt")
	}
	attempt, err := e.store.BeginAttempt(closeID, stepKey, request.Action, request.Reason)
	e.mu.Unlock()
	if err != nil {
		return nil, err
	}

	go e.runAttempt(attempt)
	return attempt, nil
}

func (e *Engine) runAttempt(attempt *Attempt) {
	closeRun, ok := e.store.GetClose(attempt.CloseRunID)
	if !ok {
		_ = e.store.FinishAttempt(attempt.ID, RunnerResult{}, fmt.Errorf("close disappeared before execution"))
		return
	}
	definition, ok := definitionByKey(attempt.StepKey)
	if !ok {
		_ = e.store.FinishAttempt(attempt.ID, RunnerResult{}, fmt.Errorf("step definition disappeared before execution"))
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
	defer cancel()
	var result RunnerResult
	var err error
	if attempt.Action == "reconcile" {
		result, err = e.runner.Reconcile(ctx, closeRun, definition, attempt)
	} else {
		result, err = e.runner.Run(ctx, closeRun, definition, attempt)
	}
	if persistErr := e.store.FinishAttempt(attempt.ID, result, err); persistErr != nil {
		slog.Error("attempt_persist_failed", "attempt_id", attempt.ID, "error_type", fmt.Sprintf("%T", persistErr))
	}
}

type DemoRunner struct{}

func (DemoRunner) Run(ctx context.Context, closeRun *CloseRun, definition StepDefinition, attempt *Attempt) (RunnerResult, error) {
	select {
	case <-ctx.Done():
		return RunnerResult{}, ctx.Err()
	case <-time.After(450 * time.Millisecond):
	}
	return RunnerResult{
		Verification:  VerificationPassed,
		ExternalRunID: "local-" + attempt.ID[:8],
		Receipt: map[string]any{
			"runtime":         "finance-portal",
			"demo":            true,
			"bill_month":      closeRun.BillMonth,
			"mode":            closeRun.Mode,
			"scope":           definition.Scope,
			"definition_hash": closeRun.DefinitionHash,
		},
	}, nil
}

func (DemoRunner) Reconcile(ctx context.Context, closeRun *CloseRun, definition StepDefinition, attempt *Attempt) (RunnerResult, error) {
	return DemoRunner{}.Run(ctx, closeRun, definition, attempt)
}
