package workflow

import (
	"encoding/json"
	"fmt"
	"sort"
	"strconv"
	"strings"

	"cloud.o-forge.io/core/oc-lib/models/resources"
	"cloud.o-forge.io/core/oc-lib/models/workflow/graph"
)

// ---------------------------------------------------------------------------
// PlantUML export
// ---------------------------------------------------------------------------

// plantUMLProcedures defines !procedure blocks for each resource type.
// Parameters use the $var/$name convention of PlantUML preprocessor v2.
// Calls are written WITHOUT inline comments (comment on the following line)
// to avoid the "assumed sequence diagram" syntax error.
const plantUMLProcedures = `!procedure Processing($var, $name)
component "$name" as $var <<Processing>>
!endprocedure

!procedure Data($var, $name)
file "$name" as $var <<Data>>
!endprocedure

!procedure Storage($var, $name)
database "$name" as $var <<Storage>>
!endprocedure

!procedure ComputeUnit($var, $name)
node "$name" as $var <<ComputeUnit>>
!endprocedure

!procedure WorkflowEvent($var, $name)
usecase "$name" as $var <<WorkflowEvent>>
!endprocedure

!procedure Workflow($var, $name)
frame "$name" as $var <<Workflow>>
!endprocedure
`

// ToPlantUML serializes the workflow graph to a valid, renderable PlantUML file
// that is also compatible with ExtractFromPlantUML (round-trip).
// Resource and instance attributes are written as human-readable comments:
//
//	Processing(p1, "NDVI") ' access.container.image: myrepo/ndvi:1.2, infrastructure: 0
func (w *Workflow) ToPlantUML() string {
	var sb strings.Builder
	sb.WriteString("@startuml\n\n")
	sb.WriteString(plantUMLProcedures)
	sb.WriteByte('\n')

	varNames := plantUMLVarNames(w.Graph.Items)

	// --- resource declarations ---
	for id, item := range w.Graph.Items {
		if line := plantUMLItemLine(varNames[id], item); line != "" {
			sb.WriteString(line + "\n")
		}
	}

	sb.WriteByte('\n')

	// --- links ---
	for _, link := range w.Graph.Links {
		src := varNames[link.Source.ID]
		dst := varNames[link.Destination.ID]
		if src == "" || dst == "" {
			continue
		}
		sb.WriteString(fmt.Sprintf("%s --> %s\n", src, dst))
		if comment := plantUMLLinkComment(link); comment != "" {
			sb.WriteString("' " + comment + "\n")
		}
	}

	sb.WriteString("\n@enduml\n")
	return sb.String()
}

// plantUMLVarNames assigns short, deterministic variable names to each graph
// item (d1, d2, p1, s1, c1, e1, wf1 …).
func plantUMLVarNames(items map[string]graph.GraphItem) map[string]string {
	counters := map[string]int{}
	varNames := map[string]string{}
	// Sort IDs for deterministic output
	ids := make([]string, 0, len(items))
	for id := range items {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	for _, id := range ids {
		prefix := plantUMLPrefix(items[id])
		counters[prefix]++
		varNames[id] = fmt.Sprintf("%s%d", prefix, counters[prefix])
	}
	return varNames
}

func plantUMLPrefix(item graph.GraphItem) string {
	switch {
	case item.ItemResource.NativeTool != nil:
		return "e"
	case item.ItemResource.Data != nil:
		return "d"
	case item.ItemResource.Processing != nil:
		return "p"
	case item.ItemResource.Storage != nil:
		return "s"
	case item.ItemResource.Compute != nil:
		return "c"
	case item.ItemResource.Workflow != nil:
		return "wf"
	}
	return "u"
}

// plantUMLItemLine builds the PlantUML declaration line for one graph item.
func plantUMLItemLine(varName string, item graph.GraphItem) string {
	switch {
	case item.ItemResource.NativeTool != nil:
		// WorkflowEvent has no instance and no configurable attributes.
		return fmt.Sprintf("WorkflowEvent(%s, \"%s\")", varName, item.ItemResource.NativeTool.GetName())

	case item.ItemResource.Data != nil:
		return plantUMLResourceLine("Data", varName, item.ItemResource.Data)

	case item.ItemResource.Processing != nil:
		return plantUMLResourceLine("Processing", varName, item.ItemResource.Processing)

	case item.ItemResource.Storage != nil:
		return plantUMLResourceLine("Storage", varName, item.ItemResource.Storage)

	case item.ItemResource.Compute != nil:
		return plantUMLResourceLine("ComputeUnit", varName, item.ItemResource.Compute)

	case item.ItemResource.Workflow != nil:
		return plantUMLResourceLine("Workflow", varName, item.ItemResource.Workflow)
	}
	return ""
}

func plantUMLResourceLine(macro, varName string, res resources.ResourceInterface) string {
	decl := fmt.Sprintf("%s(%s, \"%s\")", macro, varName, res.GetName())
	if comment := plantUMLResourceComment(res); comment != "" {
		// Comment on the line AFTER the declaration. ExtractFromPlantUML uses
		// look-ahead to merge it back. No inline comment = no !procedure conflict.
		return decl + "\n' " + comment
	}
	return decl
}

// plantUMLResourceComment merges resource-level fields with the first instance
// fields (instance overrides resource) and formats them as human-readable pairs.
func plantUMLResourceComment(res resources.ResourceInterface) string {
	m := plantUMLToFlatMap(res)
	if inst := res.GetSelectedInstance(nil); inst != nil {
		for k, v := range plantUMLToFlatMap(inst) {
			m[k] = v
		}
	}
	return plantUMLFlatMapToComment(m)
}

// plantUMLLinkComment serializes StorageLinkInfos (first entry) as flat
// human-readable pairs prefixed with "storage_link_infos.".
func plantUMLLinkComment(link graph.GraphLink) string {
	if len(link.StorageLinkInfos) == 0 {
		return ""
	}
	infoFlat := plantUMLToFlatMap(link.StorageLinkInfos[0])
	prefixed := make(map[string]string, len(infoFlat))
	for k, v := range infoFlat {
		prefixed["storage_link_infos."+k] = v
	}
	return plantUMLFlatMapToComment(prefixed)
}

// ---------------------------------------------------------------------------
// Flat-map helpers (shared by import & export)
// ---------------------------------------------------------------------------

// plantUMLSkipFields lists JSON field names (root keys) that must never appear
// in human-readable comments. All names are the actual JSON tags, not Go field names.
var plantUMLSkipFields = map[string]bool{
	// AbstractObject — identity & audit (json tags)
	"id": true, "name": true, "is_draft": true, "access_mode": true, "signature": true,
	"creator_id": true, "user_creator_id": true,
	"creation_date": true, "update_date": true,
	"updater_id": true, "user_updater_id": true,
	// internal resource type identifier (AbstractResource.Type / GetType())
	"type": true,
	// relationships / pricing
	"instances": true, "partnerships": true,
	"allowed_booking_modes": true, "usage_restrictions": true,
	// display / admin
	"logo": true, "description": true, "short_description": true, "owners": true,
	// runtime params
	"env": true, "inputs": true, "outputs": true,
	// NativeTool internals
	"kind": true, "params": true,
}

// zeroTimeStr is the JSON representation of Go's zero time.Time value.
// encoding/json does not treat it as "empty" for omitempty, so we filter it explicitly.
const zeroTimeStr = "0001-01-01T00:00:00Z"

// plantUMLToFlatMap marshals v to JSON and flattens the resulting object into
// a map[string]string using dot notation for nested keys, skipping zero values
// and known meta fields.
func plantUMLToFlatMap(v interface{}) map[string]string {
	b, err := json.Marshal(v)
	if err != nil {
		return nil
	}
	var raw map[string]interface{}
	if err := json.Unmarshal(b, &raw); err != nil {
		return nil
	}
	result := map[string]string{}
	plantUMLFlattenJSON(raw, "", result)
	return result
}

// plantUMLFlattenJSON recursively walks a JSON object and writes scalar leaf
// values into result using dot-notation keys.
func plantUMLFlattenJSON(m map[string]interface{}, prefix string, result map[string]string) {
	for k, v := range m {
		fullKey := k
		if prefix != "" {
			fullKey = prefix + "." + k
		}
		// Skip fields whose root key is in the deny-list
		if plantUMLSkipFields[strings.SplitN(fullKey, ".", 2)[0]] {
			continue
		}
		switch val := v.(type) {
		case map[string]interface{}:
			plantUMLFlattenJSON(val, fullKey, result)
		case []interface{}:
			// Arrays are not representable in flat human-readable format; skip.
		case float64:
			if val != 0 {
				if val == float64(int64(val)) {
					result[fullKey] = strconv.FormatInt(int64(val), 10)
				} else {
					result[fullKey] = strconv.FormatFloat(val, 'f', -1, 64)
				}
			}
		case bool:
			if val {
				result[fullKey] = "true"
			}
		case string:
			if val != "" && val != zeroTimeStr {
				result[fullKey] = val
			}
		}
	}
}

// plantUMLFlatMapToComment converts a flat map to a sorted "key: value, …" string.
func plantUMLFlatMapToComment(m map[string]string) string {
	if len(m) == 0 {
		return ""
	}
	keys := make([]string, 0, len(m))
	for k := range m {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	parts := make([]string, 0, len(keys))
	for _, k := range keys {
		parts = append(parts, k+": "+m[k])
	}
	return strings.Join(parts, ", ")
}
