package handlers

import (
	"context"
	"encoding/json"
	"log"

	"go.starlark.net/starlark"

	"switchboard-core/sandbox"
	"switchboard-core/store"
)

// ── on_advance Hook ─────────────────────────
//
// Fires synchronously after a stage transition succeeds.
// The hook can enrich/transform stage_data or reject the transition.
//
// Config in stage_config:
//   {"on_advance": {"package_id": "...", "entry_point": "on_advance"}}
//
// Hook receives dict: {stage_data, previous_stage, current_stage, instance_id}
// Hook returns: {stage_data: {...}} (enriched), None (no change),
//               or {error: "msg"} (reject — caller should handle rollback)

// OnAdvanceHookConfig is the on_advance section of stage_config.
type OnAdvanceHookConfig struct {
	PackageID  string `json:"package_id"`
	EntryPoint string `json:"entry_point"`
}

// StageConfigOnAdvance extracts on_advance config from stage_config JSON.
type StageConfigOnAdvance struct {
	OnAdvance *OnAdvanceHookConfig `json:"on_advance,omitempty"`
}

// OnAdvanceResult is the outcome of firing the on_advance hook.
type OnAdvanceResult struct {
	EnrichedData json.RawMessage // updated stage_data (nil = no change)
	Error        string          // non-empty = hook rejected the transition
}

// FireOnAdvanceHook fires the on_advance Starlark hook for a stage transition.
// Returns nil if no hook is configured or the runner is unavailable.
func FireOnAdvanceHook(
	ctx context.Context,
	stores store.Stores,
	runner *sandbox.Runner,
	previousStageConfig json.RawMessage,
	instanceID string,
	previousStage, currentStage int,
	stageData json.RawMessage,
) *OnAdvanceResult {
	if runner == nil || stores.Packages == nil {
		return nil
	}

	var cfg StageConfigOnAdvance
	if len(previousStageConfig) > 0 {
		_ = json.Unmarshal(previousStageConfig, &cfg)
	}
	if cfg.OnAdvance == nil || cfg.OnAdvance.PackageID == "" || cfg.OnAdvance.EntryPoint == "" {
		return nil
	}

	pkg, err := stores.Packages.Get(ctx, cfg.OnAdvance.PackageID)
	if err != nil || pkg == nil {
		log.Printf("[workflow-hooks] on_advance: package %s not found", cfg.OnAdvance.PackageID)
		return nil
	}

	// Build context dict for the hook
	ctxDict := starlark.NewDict(4)
	_ = ctxDict.SetKey(starlark.String("instance_id"), starlark.String(instanceID))
	_ = ctxDict.SetKey(starlark.String("previous_stage"), starlark.MakeInt(previousStage))
	_ = ctxDict.SetKey(starlark.String("current_stage"), starlark.MakeInt(currentStage))

	// Parse stage_data into Starlark dict
	var dataMap map[string]interface{}
	if json.Unmarshal(stageData, &dataMap) == nil {
		_ = ctxDict.SetKey(starlark.String("stage_data"), jsonToStarlark(dataMap))
	} else {
		_ = ctxDict.SetKey(starlark.String("stage_data"), starlark.NewDict(0))
	}

	val, _, err := runner.CallEntryPoint(ctx, pkg, cfg.OnAdvance.EntryPoint,
		starlark.Tuple{ctxDict}, nil, nil)
	if err != nil {
		log.Printf("[workflow-hooks] on_advance hook error: %v", err)
		return nil
	}

	return parseOnAdvanceResult(val)
}

// parseOnAdvanceResult extracts enriched data or error from the Starlark return value.
func parseOnAdvanceResult(val starlark.Value) *OnAdvanceResult {
	if val == nil || val == starlark.None {
		return nil
	}

	d, ok := val.(*starlark.Dict)
	if !ok {
		return nil
	}

	result := &OnAdvanceResult{}

	// Check for error
	if errVal, found, _ := d.Get(starlark.String("error")); found {
		if s, ok := errVal.(starlark.String); ok {
			result.Error = string(s)
			return result
		}
	}

	// Check for enriched stage_data
	if sdVal, found, _ := d.Get(starlark.String("stage_data")); found {
		if sd, ok := sdVal.(*starlark.Dict); ok {
			goMap := starlarkDictToMap(sd)
			if data, err := json.Marshal(goMap); err == nil {
				result.EnrichedData = data
				return result
			}
		}
	}

	return nil
}

// starlarkDictToMap converts a Starlark dict to a Go map.
func starlarkDictToMap(d *starlark.Dict) map[string]any {
	result := make(map[string]any, d.Len())
	for _, item := range d.Items() {
		k, ok := item[0].(starlark.String)
		if !ok {
			continue
		}
		result[string(k)] = starlarkToGo(item[1])
	}
	return result
}

// starlarkToGo converts a Starlark value to a Go value.
func starlarkToGo(v starlark.Value) any {
	switch val := v.(type) {
	case starlark.NoneType:
		return nil
	case starlark.Bool:
		return bool(val)
	case starlark.Int:
		if i, ok := val.Int64(); ok {
			return i
		}
		return val.String()
	case starlark.Float:
		return float64(val)
	case starlark.String:
		return string(val)
	case *starlark.List:
		result := make([]any, val.Len())
		for i := 0; i < val.Len(); i++ {
			result[i] = starlarkToGo(val.Index(i))
		}
		return result
	case *starlark.Dict:
		return starlarkDictToMap(val)
	default:
		return v.String()
	}
}
