package mill

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

	"tangled.org/core/api/tangled"
	"tangled.org/core/spindle/engine"
	"tangled.org/core/spindle/models"
	"tangled.org/core/spindle/secrets"
)

// millWorkflowState is stashed in Workflow.Data (which is `any`, like both real
// engines use it). InitWorkflow parses nothing here: it just carries the raw
// pipeline/workflow forward so the executor can run the real InitWorkflow later.
type millWorkflowState struct {
	TargetEngine         string
	RawWorkflow          tangled.Pipeline_Workflow
	RawPipeline          tangled.Pipeline
	RequiredCapabilities []string
	Wid                  models.WorkflowId // stamped at placement (InitWorkflow can't see it)
	Lease                *RemoteLease
}

// Engine is the mill's stand-in for a real engine, registered under the real
// engine names ("microvm", "nixery"). All registered names share one Mill.
type Engine struct {
	name    string
	mill    *Mill
	planner engine.PlacementPlanner
	l       *slog.Logger
}

// NewEngine returns a mill engine view for one engine name without
// engine-derived placement requirements.
func NewEngine(name string, mill *Mill) *Engine {
	return NewEngineWithPlanner(name, mill, nil)
}

// NewEngineWithPlanner returns a mill engine view whose trusted planner
// derives requirements for capability-aware placement.
func NewEngineWithPlanner(name string, mill *Mill, planner engine.PlacementPlanner) *Engine {
	return &Engine{name: name, mill: mill, planner: planner, l: mill.l.With("engine", "mill:"+name)}
}

// InitWorkflow returns a synthetic one-step workflow so processPipeline injects
// TANGLED_* env and marks pending normally. The real engine's InitWorkflow runs
// exactly once, on the executor inside ReserveSeat; the commit path reuses that
// workflow rather than re-initializing.
func (e *Engine) InitWorkflow(twf tangled.Pipeline_Workflow, tpl tangled.Pipeline) (*models.Workflow, error) {
	var required []string
	if e.planner != nil {
		var err error
		required, err = e.planner.Requirements(twf, tpl)
		if err != nil {
			return nil, fmt.Errorf("derive %s placement requirements: %w", e.name, err)
		}
		required, err = engine.NormalizeCapabilities(required)
		if err != nil {
			return nil, fmt.Errorf("derive %s placement requirements: %w", e.name, err)
		}
	}
	return &models.Workflow{
		Name:        twf.Name,
		Environment: map[string]string{},
		Steps:       []models.Step{remoteStep{}},
		Data: &millWorkflowState{
			TargetEngine:         e.name,
			RawWorkflow:          twf,
			RawPipeline:          tpl,
			RequiredCapabilities: required,
		},
	}, nil
}

// WorkflowLogger gives StartWorkflows a no-op logger for the mill's synthetic
// workflow: the executor's real log lines are relayed into this wid's log file
// directly (see Loom.onLogRelay), so a local logger here would only write
// competing, step-id-colliding lines. This is engine's optional logger-provider
// seam.
func (e *Engine) WorkflowLogger(wid models.WorkflowId) models.WorkflowLogger {
	return models.NullLogger{}
}

// AcquireWorkflowSlot is the placement seam: it blocks doing remote placement,
// which the user sees as "pending". mode is irrelevant here (only StartWorkflows
// calls this, with Wait); the mill always places.
func (e *Engine) AcquireWorkflowSlot(ctx context.Context, wid models.WorkflowId, wf *models.Workflow, _ engine.AcquireMode) (engine.WorkflowSlot, error) {
	return e.mill.place(ctx, e.name, wid, wf)
}

// SetupWorkflow is a no-op info log; the real setup happens on the executor.
func (e *Engine) SetupWorkflow(ctx context.Context, wid models.WorkflowId, wf *models.Workflow, wfLogger models.WorkflowLogger) error {
	e.l.Info("remote job placed, awaiting commit", "wid", wid)
	return nil
}

// RunStep is commit-and-wait: hand over secrets and block on the terminal
// result relayed over the session.
func (e *Engine) RunStep(ctx context.Context, wid models.WorkflowId, w *models.Workflow, idx int, unlocked []secrets.UnlockedSecret, wfLogger models.WorkflowLogger) error {
	return e.mill.commitAndWait(ctx, w, unlocked)
}

// WorkflowTimeout is deliberately generous: the executor enforces the real
// per-engine timeout and relays it. The mill only caps a hung/silent
// executor, with a true death caught separately by reconnect grace.
func (e *Engine) WorkflowTimeout() time.Duration {
	return e.mill.cfg.JobTimeout
}

// DestroyWorkflow cancels a still-running attempt; no-op if already terminal.
func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error {
	e.mill.destroy(wid)
	return nil
}
