package prediction

import (
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/rose_cat707/luminary/internal/model"
)

type WorkflowNode struct {
	ID               string   `json:"id"`
	ClassType        string   `json:"class_type"`
	Title            string   `json:"title"`
	InjectableFields []string `json:"injectable_fields"`
}

type BindingCandidate struct {
	Node  string `json:"node"`
	Field string `json:"field"`
}

type BindingSuggestion struct {
	Param               string             `json:"param"`
	Node                string             `json:"node,omitempty"`
	Field               string             `json:"field,omitempty"`
	Type                ParamType          `json:"type"`
	DefaultFromWorkflow any                `json:"default_from_workflow,omitempty"`
	Confidence          string             `json:"confidence"`
	Candidates          []BindingCandidate `json:"candidates"`
}

type AnalyzeResult struct {
	VersionHashPreview string              `json:"version_hash_preview"`
	Nodes              []WorkflowNode      `json:"nodes"`
	Suggestions        []BindingSuggestion `json:"suggestions"`
}

func HashWorkflowJSON(raw string) string {
	sum := sha256.Sum256([]byte(raw))
	return hex.EncodeToString(sum[:])
}

func AnalyzeWorkflow(workflowJSON string, wt model.WorkflowType) (*AnalyzeResult, error) {
	var workflow map[string]any
	if err := json.Unmarshal([]byte(workflowJSON), &workflow); err != nil {
		return nil, fmt.Errorf("invalid workflow json: %w", err)
	}
	nodes := parseNodes(workflow)
	suggestions := suggestBindings(workflow, nodes, wt)
	return &AnalyzeResult{
		VersionHashPreview: HashWorkflowJSON(workflowJSON),
		Nodes:              nodes,
		Suggestions:        suggestions,
	}, nil
}

func parseNodes(workflow map[string]any) []WorkflowNode {
	var out []WorkflowNode
	for id, raw := range workflow {
		node, ok := raw.(map[string]any)
		if !ok {
			continue
		}
		classType, _ := node["class_type"].(string)
		title := ""
		if meta, ok := node["_meta"].(map[string]any); ok {
			title, _ = meta["title"].(string)
		}
		var fields []string
		if inputs, ok := node["inputs"].(map[string]any); ok {
			for k, v := range inputs {
				if isInjectableValue(v) {
					fields = append(fields, k)
				}
			}
		}
		out = append(out, WorkflowNode{
			ID: id, ClassType: classType, Title: title, InjectableFields: fields,
		})
	}
	return out
}

func isInjectableValue(v any) bool {
	switch v.(type) {
	case string, float64, bool, int, int64:
		return true
	default:
		return false
	}
}

func suggestBindings(workflow map[string]any, nodes []WorkflowNode, wt model.WorkflowType) []BindingSuggestion {
	defs := ParamsForWorkflowType(wt)
	var clipNodes []WorkflowNode
	var samplerNodes []WorkflowNode
	var latentNodes []WorkflowNode
	var loadImageNodes []WorkflowNode
	for _, n := range nodes {
		switch n.ClassType {
		case "CLIPTextEncode":
			clipNodes = append(clipNodes, n)
		case "KSampler", "KSamplerAdvanced", "RandomNoise":
			samplerNodes = append(samplerNodes, n)
		case "EmptyLatentImage":
			latentNodes = append(latentNodes, n)
		case "LoadImage":
			loadImageNodes = append(loadImageNodes, n)
		}
	}
	var out []BindingSuggestion
	for _, d := range defs {
		s := BindingSuggestion{Param: d.Name, Type: d.Type, Confidence: "low"}
		switch d.Name {
		case "prompt":
			s = suggestCLIP(clipNodes, workflow, false)
		case "negative_prompt":
			s = suggestCLIP(clipNodes, workflow, true)
		case "seed":
			s = suggestSamplerField(samplerNodes, workflow, "seed", d.Type)
			if s.Node == "" {
				s = suggestSamplerField(samplerNodes, workflow, "noise_seed", d.Type)
			}
		case "steps":
			s = suggestSamplerField(samplerNodes, workflow, "steps", d.Type)
		case "cfg_scale":
			s = suggestSamplerField(samplerNodes, workflow, "cfg", d.Type)
		case "denoise":
			s = suggestSamplerField(samplerNodes, workflow, "denoise", d.Type)
		case "width":
			s = suggestLatentField(latentNodes, workflow, "width", d.Type)
		case "height":
			s = suggestLatentField(latentNodes, workflow, "height", d.Type)
		case "image":
			s = suggestLoadImage(loadImageNodes, workflow, d.Type)
		default:
			s.Param = d.Name
			s.Type = d.Type
		}
		if s.Param == "" {
			s.Param = d.Name
			s.Type = d.Type
		}
		if s.Node != "" {
			s.Confidence = "high"
		}
		out = append(out, s)
	}
	return out
}

func suggestCLIP(nodes []WorkflowNode, workflow map[string]any, negative bool) BindingSuggestion {
	s := BindingSuggestion{Param: "prompt", Type: ParamString, Confidence: "low"}
	if negative {
		s.Param = "negative_prompt"
	}
	var candidates []BindingCandidate
	var picked *WorkflowNode
	for i := range nodes {
		n := nodes[i]
		titleLower := strings.ToLower(n.Title)
		isNeg := strings.Contains(titleLower, "negative") || strings.Contains(titleLower, "neg")
		if !hasField(n, "text") {
			continue
		}
		candidates = append(candidates, BindingCandidate{Node: n.ID, Field: "text"})
		if negative && isNeg && picked == nil {
			picked = &nodes[i]
		}
		if !negative && !isNeg && picked == nil {
			picked = &nodes[i]
		}
	}
	if picked == nil && len(candidates) > 0 {
		idx := 0
		if negative && len(candidates) > 1 {
			idx = 1
		}
		for i := range nodes {
			if nodes[i].ID == candidates[idx].Node {
				picked = &nodes[i]
				break
			}
		}
	}
	s.Candidates = candidates
	if picked != nil {
		s.Node = picked.ID
		s.Field = "text"
		s.DefaultFromWorkflow = nodeFieldValue(workflow, picked.ID, "text")
	}
	return s
}

func suggestSamplerField(nodes []WorkflowNode, workflow map[string]any, field string, pt ParamType) BindingSuggestion {
	paramName := field
	if field == "cfg" {
		paramName = "cfg_scale"
	}
	s := BindingSuggestion{Param: paramName, Type: pt, Confidence: "low"}
	var candidates []BindingCandidate
	for _, n := range nodes {
		if hasField(n, field) {
			candidates = append(candidates, BindingCandidate{Node: n.ID, Field: field})
			if s.Node == "" {
				s.Node = n.ID
				s.Field = field
				s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, field)
			}
		}
	}
	s.Candidates = candidates
	return s
}

func suggestLatentField(nodes []WorkflowNode, workflow map[string]any, field string, pt ParamType) BindingSuggestion {
	s := BindingSuggestion{Param: field, Type: pt, Confidence: "low"}
	for _, n := range nodes {
		if hasField(n, field) {
			s.Candidates = append(s.Candidates, BindingCandidate{Node: n.ID, Field: field})
			if s.Node == "" {
				s.Node = n.ID
				s.Field = field
				s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, field)
			}
		}
	}
	return s
}

func suggestLoadImage(nodes []WorkflowNode, workflow map[string]any, pt ParamType) BindingSuggestion {
	s := BindingSuggestion{Param: "image", Type: pt, Confidence: "low"}
	for _, n := range nodes {
		if hasField(n, "image") {
			s.Candidates = append(s.Candidates, BindingCandidate{Node: n.ID, Field: "image"})
			if s.Node == "" {
				s.Node = n.ID
				s.Field = "image"
				s.DefaultFromWorkflow = nodeFieldValue(workflow, n.ID, "image")
			}
		}
	}
	return s
}

func hasField(n WorkflowNode, field string) bool {
	for _, f := range n.InjectableFields {
		if f == field {
			return true
		}
	}
	return false
}

func nodeFieldValue(workflow map[string]any, nodeID, field string) any {
	node, ok := workflow[nodeID].(map[string]any)
	if !ok {
		return nil
	}
	inputs, ok := node["inputs"].(map[string]any)
	if !ok {
		return nil
	}
	return inputs[field]
}
