package semantic

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

	"github.com/tidbcloud/AgentTeam/kits/agentteam-insight/internal/graphbuilder/schema"
)

type rawLayer struct {
	ID          string          `json:"id"`
	Name        string          `json:"name"`
	Description string          `json:"description"`
	NodeIDs     json.RawMessage `json:"nodeIds"`
	Nodes       json.RawMessage `json:"nodes"`
}

type rawTour struct {
	Order          int             `json:"order"`
	Title          string          `json:"title"`
	Description    string          `json:"description"`
	WhyItMatters   string          `json:"whyItMatters"`
	NodeIDs        json.RawMessage `json:"nodeIds"`
	NodesToInspect json.RawMessage `json:"nodesToInspect"`
	LanguageLesson string          `json:"languageLesson"`
}

func NormalizeLayers(data []byte, graph schema.KnowledgeGraph) ([]schema.Layer, error) {
	var raws []rawLayer
	if err := decodeArray(data, "layers", &raws); err != nil {
		return nil, fmt.Errorf("parse layers: %w", err)
	}
	if len(raws) < 3 || len(raws) > 10 {
		return nil, fmt.Errorf("layers must contain 3-10 entries, got %d", len(raws))
	}
	valid, pathToID := fileLevelIndex(graph)
	assigned := map[string]string{}
	out := make([]schema.Layer, 0, len(raws))
	for _, raw := range raws {
		name, desc := strings.TrimSpace(raw.Name), strings.TrimSpace(raw.Description)
		if name == "" || desc == "" {
			return nil, fmt.Errorf("layer name and description are required")
		}
		id := strings.TrimSpace(raw.ID)
		if id == "" {
			id = "layer:" + slug(name)
		}
		ids, err := nodeIDs(firstRaw(raw.NodeIDs, raw.Nodes), valid, pathToID)
		if err != nil {
			return nil, err
		}
		if len(ids) == 0 {
			return nil, fmt.Errorf("layer %s has no valid node IDs", id)
		}
		for _, nodeID := range ids {
			if previous, ok := assigned[nodeID]; ok {
				return nil, fmt.Errorf("node %s assigned to both %s and %s", nodeID, previous, id)
			}
			assigned[nodeID] = id
		}
		out = append(out, schema.Layer{ID: id, Name: name, Description: desc, NodeIDs: ids})
	}
	if len(assigned) != len(valid) {
		missing := []string{}
		for id := range valid {
			if _, ok := assigned[id]; !ok {
				missing = append(missing, id)
			}
		}
		sort.Strings(missing)
		return nil, fmt.Errorf("architecture omitted %d file-level nodes: %s", len(missing), strings.Join(missing, ", "))
	}
	return out, nil
}

func NormalizeTour(data []byte, graph schema.KnowledgeGraph) ([]schema.TourStep, error) {
	var raws []rawTour
	if err := decodeArray(data, "steps", &raws); err != nil {
		return nil, fmt.Errorf("parse tour: %w", err)
	}
	if len(raws) < 5 || len(raws) > 15 {
		return nil, fmt.Errorf("tour must contain 5-15 steps, got %d", len(raws))
	}
	valid, pathToID := fileLevelIndex(graph)
	out := make([]schema.TourStep, 0, len(raws))
	for _, raw := range raws {
		title, desc := strings.TrimSpace(raw.Title), strings.TrimSpace(raw.Description)
		if desc == "" {
			desc = strings.TrimSpace(raw.WhyItMatters)
		}
		if title == "" || desc == "" {
			return nil, fmt.Errorf("tour title and description are required")
		}
		ids, err := nodeIDs(firstRaw(raw.NodeIDs, raw.NodesToInspect), valid, pathToID)
		if err != nil {
			return nil, err
		}
		if len(ids) == 0 {
			return nil, fmt.Errorf("tour step %q has no valid node IDs", title)
		}
		out = append(out, schema.TourStep{Order: raw.Order, Title: title, Description: desc, NodeIDs: ids, LanguageLesson: strings.TrimSpace(raw.LanguageLesson)})
	}
	sort.SliceStable(out, func(i, j int) bool { return out[i].Order < out[j].Order })
	for i := range out {
		if out[i].Order != i+1 {
			return nil, fmt.Errorf("tour orders must be sequential from 1")
		}
	}
	return out, nil
}

func decodeArray(data []byte, envelope string, target any) error {
	data = []byte(strings.TrimSpace(string(data)))
	if len(data) == 0 {
		return fmt.Errorf("empty output")
	}
	if data[0] == '{' {
		var object map[string]json.RawMessage
		if err := json.Unmarshal(data, &object); err != nil {
			return err
		}
		inner, ok := object[envelope]
		if !ok {
			return fmt.Errorf("missing %s array", envelope)
		}
		data = inner
	}
	return json.Unmarshal(data, target)
}

func fileLevelIndex(graph schema.KnowledgeGraph) (map[string]struct{}, map[string]string) {
	valid := map[string]struct{}{}
	paths := map[string]string{}
	for _, node := range fileLevelNodes(graph) {
		valid[node.ID] = struct{}{}
		if node.FilePath != "" {
			paths[strings.ReplaceAll(node.FilePath, `\`, "/")] = node.ID
		}
	}
	return valid, paths
}
func firstRaw(values ...json.RawMessage) json.RawMessage {
	for _, value := range values {
		if len(value) > 0 && string(value) != "null" {
			return value
		}
	}
	return nil
}
func nodeIDs(raw json.RawMessage, valid map[string]struct{}, paths map[string]string) ([]string, error) {
	if len(raw) == 0 {
		return nil, fmt.Errorf("nodeIds are required")
	}
	var values []any
	if err := json.Unmarshal(raw, &values); err != nil {
		return nil, fmt.Errorf("nodeIds must be an array")
	}
	out := []string{}
	seen := map[string]struct{}{}
	for _, value := range values {
		var id string
		switch typed := value.(type) {
		case string:
			id = typed
		case map[string]any:
			id, _ = typed["id"].(string)
		}
		id = strings.TrimSpace(strings.ReplaceAll(id, `\`, "/"))
		if _, ok := valid[id]; !ok {
			if mapped, found := paths[id]; found {
				id = mapped
			} else {
				continue
			}
		}
		if _, ok := seen[id]; ok {
			continue
		}
		seen[id] = struct{}{}
		out = append(out, id)
	}
	return out, nil
}
func slug(value string) string {
	var b strings.Builder
	dash := false
	for _, r := range strings.ToLower(strings.TrimSpace(value)) {
		if unicode.IsLetter(r) || unicode.IsDigit(r) {
			b.WriteRune(r)
			dash = false
		} else if !dash {
			b.WriteByte('-')
			dash = true
		}
	}
	return strings.Trim(b.String(), "-")
}
